[Pkg-mozext-maintainers] Bug#1116526: trixie-pu: package quicktext/6.4.6-1~deb13u1

Mechtilde Stehmann mechtilde at debian.org
Sun Sep 28 11:18:50 BST 2025


Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: quicktext at packages.debian.org, mechtilde at debian.org
Control: affects -1 + src:quicktext
User: release.debian.org at packages.debian.org
Usertags: pu

[ Reason ]
Thunderbird will come with a new version (>=140.3) into stable. This need an
update for the Add-Ons (here: quicktext) too.

[ Impact ]
If the update isn't approved the user can't anymore use it
in recent thunderbird.

[ Tests ]
The same upstream code works with thunderbird >= 140.3 in testing.

[ Risks ]
Code is trivial so no risk

[ Checklist ]
 [X] *all* changes are documented in the d/changelog
  [X] I reviewed all changes and I approve them
  [X] attach debdiff against the package in (old)stable
  [X] the issue is verified as fixed in unstable

[ Changes ]
The new version of thunderbird needs a new version of webext-quicktext.

[ Other info ]
The only reason is the new upcoming version of the thunderbird.
-------------- next part --------------
diff -Nru quicktext-6.3.2/api/Quicktext/implementation.js quicktext-6.4.6/api/Quicktext/implementation.js
--- quicktext-6.3.2/api/Quicktext/implementation.js	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/api/Quicktext/implementation.js	2025-08-26 01:05:47.000000000 +0200
@@ -63,9 +63,18 @@
           async readBinaryFile(aFilePath) {
             return IOUtils.read(aFilePath);
           },
-          async readTextFile(aFilePath) {
+          async readTextFile(aFilePath, aBasePath) {
+            if (aBasePath) {
+              aFilePath =  PathUtils.join(aBasePath, aFilePath)
+            }
             return IOUtils.readUTF8(aFilePath);
           },
+          async writeTextFile(aFilePath, aData, aBasePath) {
+            if (aBasePath) {
+              aFilePath =  PathUtils.join(aBasePath, aFilePath)
+            }
+            return IOUtils.writeUTF8(aFilePath, aData)
+          },
         },
       };
     }
diff -Nru quicktext-6.3.2/api/Quicktext/schema.json quicktext-6.4.6/api/Quicktext/schema.json
--- quicktext-6.3.2/api/Quicktext/schema.json	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/api/Quicktext/schema.json	2025-08-26 01:05:47.000000000 +0200
@@ -38,6 +38,31 @@
           {
             "name": "aFilePath",
             "type": "string"
+          },
+          {
+            "name": "aBasePath",
+            "type": "string",
+            "optional": true
+          }
+        ]
+      },
+      {
+        "name": "writeTextFile",
+        "type": "function",
+        "async": true,
+        "parameters": [
+          {
+            "name": "aFilePath",
+            "type": "string"
+          },
+          {
+            "name": "aData",
+            "type": "string"
+          },
+          {
+            "name": "aBasePath",
+            "type": "string",
+            "optional": true
           }
         ]
       }
diff -Nru quicktext-6.3.2/api/QuicktextToolbar/composerToolbar.js quicktext-6.4.6/api/QuicktextToolbar/composerToolbar.js
--- quicktext-6.3.2/api/QuicktextToolbar/composerToolbar.js	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/api/QuicktextToolbar/composerToolbar.js	2025-08-26 01:05:47.000000000 +0200
@@ -2,14 +2,8 @@
   windowId: null,
   extension: null,
 
-  dateTimeFormat(format, timeStamp) {
-    let options = {};
-    options["date-short"] = { dateStyle: "short" };
-    options["date-long"] = { dateStyle: "long" };
-    options["date-monthname"] = { month: "long" };
-    options["time-noseconds"] = { timeStyle: "short" };
-    options["time-seconds"] = { timeStyle: "long" };
-    return new Services.intl.DateTimeFormat(undefined, options[format.toLowerCase()]).format(timeStamp)
+  async dateTimeFormat(format, timeStamp) {
+    return this.notifyTools.notifyBackground({ command: "getDateTimeFormat", data: { format, timeStamp } });
   },
 
   getPrettyKeyName(key) {
@@ -40,11 +34,11 @@
     let fields = ["date-short", "date-long", "date-monthname", "time-noseconds", "time-seconds"];
     for (let i = 0; i < fields.length; i++) {
       let field = fields[i];
-      let fieldtype = field.split("-")[0];
+      let fieldType = field.split("-")[0];
       if (document.getElementById(field)) {
         document.getElementById(field).setAttribute(
           "label",
-          this.extension.localeData.localizeMessage(fieldtype, [this.dateTimeFormat(field, timeStamp)])
+          this.extension.localeData.localizeMessage(fieldType, [await this.dateTimeFormat(field, timeStamp)])
         );
       }
     }
@@ -126,8 +120,8 @@
               this.mShortcuts[shortcut] = [i, j];
 
             var keyword = text.keyword;
-            if (keyword != "" && typeof this.mKeywords[keyword.toLowerCase()] == "undefined")
-              this.mKeywords[keyword.toLowerCase()] = [i, j];
+            if (keyword != "" && typeof this.mKeywords[keyword] == "undefined")
+              this.mKeywords[keyword] = [i, j];
           }
         }
       }
diff -Nru quicktext-6.3.2/api/QuicktextToolbar/implementation.js quicktext-6.4.6/api/QuicktextToolbar/implementation.js
--- quicktext-6.3.2/api/QuicktextToolbar/implementation.js	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/api/QuicktextToolbar/implementation.js	2025-08-26 01:05:47.000000000 +0200
@@ -237,8 +237,8 @@
       </button>
       <button type="menu" id="quicktext-other" label="__MSG_quicktext.other.label__" tabindex="-1">
         <menupopup>
-          <menuitem label="__MSG_quicktext.insertTextFromFileAsText.label__" oncommand="quicktextToolbar.insertContentFromFile(0);" />
-          <menuitem label="__MSG_quicktext.insertTextFromFileAsHTML.label__" oncommand="quicktextToolbar.insertContentFromFile(1);" />
+          <menuitem label="__MSG_quicktext.insertTextFromFileAsText.label__" oncommand="quicktextToolbar.insertContentFromFile('text/plain');" />
+          <menuitem label="__MSG_quicktext.insertTextFromFileAsHTML.label__" oncommand="quicktextToolbar.insertContentFromFile('text/html');" />
         </menupopup>
       </button>
     </hbox>
diff -Nru quicktext-6.3.2/debian/changelog quicktext-6.4.6/debian/changelog
--- quicktext-6.3.2/debian/changelog	2025-04-08 19:09:32.000000000 +0200
+++ quicktext-6.4.6/debian/changelog	2025-09-24 14:52:53.000000000 +0200
@@ -1,3 +1,40 @@
+quicktext (6.4.6-1~deb13u1) trixie; urgency=medium
+
+  * Rebuildfor trixie after upload thunderbird 140.3
+
+ -- Mechtilde Stehmann <mechtilde at debian.org>  Wed, 24 Sep 2025 14:52:53 +0200
+
+quicktext (6.4.6-1) unstable; urgency=medium
+
+  [ Mechtilde ]
+  * [5aa879f] Added d/dpb.conf for using with debian-packages-scripts
+  * [1e8d987] New upstream version 6.4.6
+
+ -- Mechtilde Stehmann <mechtilde at debian.org>  Sun, 21 Sep 2025 14:41:37 +0200
+
+quicktext (6.4.4-1~exp1) experimental; urgency=medium
+
+  [ Mechtilde ]
+  * [c2fa859] Improved d/u/metadata using Mozilla repo
+  * [747d9c9] Fixed d/dpb.conf
+  * [be3897e] New upstream version 6.4.4
+  * [c2fa859] Improved d/u/metadata using Mozilla repo
+  * [747d9c9] Fixed d/dpb.conf
+  * [be3897e] New upstream version 6.4.4
+
+ -- Mechtilde Stehmann <mechtilde at debian.org>  Sat, 23 Aug 2025 14:49:36 +0200
+
+quicktext (6.4.1-1~exp1) experimental; urgency=medium
+
+  [ Mechtilde ]
+  * [19f530b] New upstream version 6.4.1
+  * [a42d224] New upstream version 6.4
+  * [f37aa19] Bumped version of thunderbird
+  * [1fc0b84] Added d/dpb.conf for using with debian-package-scripts
+  * [a72aabf] Bumped version for thunderbird
+
+ -- Mechtilde Stehmann <mechtilde at debian.org>  Fri, 08 Aug 2025 16:41:43 +0200
+
 quicktext (6.3.2-1) unstable; urgency=medium
 
   [ Mechtilde ]
diff -Nru quicktext-6.3.2/debian/control quicktext-6.4.6/debian/control
--- quicktext-6.3.2/debian/control	2025-04-08 19:02:25.000000000 +0200
+++ quicktext-6.4.6/debian/control	2025-08-08 14:44:26.000000000 +0200
@@ -14,8 +14,8 @@
 Package: webext-quicktext
 Architecture: all
 Depends: ${misc:Depends}
- , thunderbird (>= 1:128.8)
- , thunderbird (<= 1:138.x)
+ , thunderbird (>= 1:140.1)
+ , thunderbird (<= 1:143.x)
 Description: Create templates for Thunderbird
  Quicktext is an extension for Thunderbird that lets you create templates that
  can be easily inserted into your own emails. Using Thunderbird, Quicktext is
diff -Nru quicktext-6.3.2/debian/dpb.conf quicktext-6.4.6/debian/dpb.conf
--- quicktext-6.3.2/debian/dpb.conf	1970-01-01 01:00:00.000000000 +0100
+++ quicktext-6.4.6/debian/dpb.conf	2025-09-24 14:48:17.000000000 +0200
@@ -0,0 +1,17 @@
+#!/bin/bash
+# debian/dpb.conf
+# This file is used bei the scripts from
+# debian-package-scripts
+## General parameters
+SourceName=quicktext
+PackName=webext-quicktext
+SalsaName=webext-team/quicktext.git
+## Parameters for Java packages
+JavaFlag=0
+## Parameters for Webext packages
+WebextFlag=1
+## Parameters for Python3 packages
+PythonFlag=0
+## Recent branch to build for
+RecentBranch=debian/trixie
+RecentBranchD=trixie
diff -Nru quicktext-6.3.2/debian/gbp.conf quicktext-6.4.6/debian/gbp.conf
--- quicktext-6.3.2/debian/gbp.conf	1970-01-01 01:00:00.000000000 +0100
+++ quicktext-6.4.6/debian/gbp.conf	2025-09-24 14:45:23.000000000 +0200
@@ -0,0 +1,22 @@
+# Configuration file for git-buildpackage and friends
+
+[DEFAULT]
+# use pristine-tar:
+pristine-tar = True
+# generate gz compressed orig file
+compression = gz
+debian-branch = debian/trixie
+upstream-branch = upstream
+
+[pq]
+patch-numbers = False
+
+[dch]
+id-length = 7
+debian-branch = debian/trixie
+
+[import-orig]
+# filter out unwanted files/dirs from upstream
+filter = [ '.cvsignore', '.gitignore', '.github', '.hgtags', '.hgignore', '*.orig', '*.rej' ]
+# filter the files out of the tarball passed to pristine-tar
+filter-pristine-tar = True
diff -Nru quicktext-6.3.2/debian/upstream/metadata quicktext-6.4.6/debian/upstream/metadata
--- quicktext-6.3.2/debian/upstream/metadata	2022-02-01 18:42:06.000000000 +0100
+++ quicktext-6.4.6/debian/upstream/metadata	2025-08-23 14:26:53.000000000 +0200
@@ -6,3 +6,4 @@
 Documentation: https://github.com/jobisoft/quicktext/wiki
 Repository: https://github.com/jobisoft/quicktext.git
 Repository-Browse: https://github.com/jobisoft/quicktext
+Reference: https://addons.thunderbird.net/en-US/thunderbird/addon/quicktext/versions/
diff -Nru quicktext-6.3.2/_locales/cs/messages.json quicktext-6.4.6/_locales/cs/messages.json
--- quicktext-6.3.2/_locales/cs/messages.json	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/_locales/cs/messages.json	2025-08-26 01:05:47.000000000 +0200
@@ -1,4 +1,7 @@
 {
+    "deprecated_attachment_field": {
+        "message": "This input field is deprecated. Use [[ATTACHMENT=FILE|<path>]] directly in the body of the template instead."
+    },
     "altKey": {
         "message": "Alt"
     },
@@ -63,6 +66,12 @@
     "quicktext.HTML.label": {
         "message": "HTML"
     },
+    "quicktext.advanced.label": {
+        "message": "Pokro?il?"
+    },
+    "quicktext.accesskey.advanced": {
+        "message": "a"
+    },
     "quicktext.accesskey.addGroup": {
         "message": "p"
     },
@@ -87,9 +96,6 @@
     "quicktext.accesskey.general": {
         "message": "g"
     },
-    "quicktext.accesskey.goToHomepage": {
-        "message": "w"
-    },
     "quicktext.accesskey.help": {
         "message": "p"
     },
@@ -126,6 +132,18 @@
     "quicktext.accesskeyTemplate.import": {
         "message": "i"
     },
+    "quicktext.buttons.selectStorage.label": {
+        "message": "Vyberte ?lo?i?t?"
+    },
+    "quicktext.buttons.addFolder.label": {
+        "message": "+ Slo?ka"
+    },
+    "quicktext.buttons.addFile.label": {
+        "message": "+ Soubor"
+    },
+    "quicktext.buttons.addUrl.label": {
+        "message": "+ URL"
+    },
     "quicktext.addGroup.label": {
         "message": "P?idat skupinu"
     },
@@ -181,7 +199,7 @@
         "message": "Datum/?as"
     },
     "quicktext.defaultImport.label": {
-        "message": "Importovat po spu?t?n?"
+        "message": "Importovat ?ablony a skripty p?i spu?t?n? (pouze ke ?ten?)"
     },
     "quicktext.displayname.label": {
         "message": "Zobrazovan? jm?no"
@@ -240,9 +258,6 @@
     "quicktext.getScript.label": {
         "message": "Komunitn? skripty"
     },
-    "quicktext.goToHomepage.label": {
-        "message": "P?ej?t na webov? str?nky"
-    },
     "quicktext.group.label": {
         "message": "Skupina"
     },
@@ -252,6 +267,9 @@
     "quicktext.help.label": {
         "message": "N?pov?da"
     },
+    "quicktext.scripthelp.label": {
+        "message": "Nekompatibiln? skripts"
+    },
     "quicktext.homenumber.label": {
         "message": "Telefon dom?"
     },
@@ -312,6 +330,15 @@
     "quicktext.other.label": {
         "message": "Ostatn?"
     },
+    "quicktext.storageLocations.label": {
+        "message": "Um?st?n? ?lo?i?t? pro ?ablony a skripty"
+    },
+    "quicktext.storageLocations.description": {
+        "message": "Zm?na um?st?n? ?lo?i?t? pro ?ablony a skripty ulo?? aktu?ln? seznam um?st?n? ?lo?i?? a restartuje Quicktext, aby na?etl soubory z nov?ho um?st?n?. V?echna ostatn? neulo?en? nastaven? budou ztracena."
+    },
+    "quicktext.storage.internal.local.label": {
+        "message": "Intern? ?lo?i?t? (v?choz?)"
+    },
     "quicktext.remove.label": {
         "message": "Odstranit"
     },
@@ -429,4 +456,4 @@
             }
         }
     }
-}
+}
\ Kein Zeilenumbruch am Dateiende.
diff -Nru quicktext-6.3.2/_locales/de/messages.json quicktext-6.4.6/_locales/de/messages.json
--- quicktext-6.3.2/_locales/de/messages.json	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/_locales/de/messages.json	2025-08-26 01:05:47.000000000 +0200
@@ -1,4 +1,7 @@
 {
+    "deprecated_attachment_field": {
+        "message": "Dieses Eingabefeld ist veraltet. Verwenden Sie stattdessen [[ATTACHMENT=FILE|<Pfad>]] direkt im Vorlagentext."
+    },
     "altKey": {
         "message": "Alt"
     },
@@ -63,6 +66,12 @@
     "quicktext.HTML.label": {
         "message": "HTML"
     },
+    "quicktext.advanced.label": {
+        "message": "Erweitert"
+    },
+    "quicktext.accesskey.advanced": {
+        "message": "w"
+    },
     "quicktext.accesskey.addGroup": {
         "message": "p"
     },
@@ -87,9 +96,6 @@
     "quicktext.accesskey.general": {
         "message": "a"
     },
-    "quicktext.accesskey.goToHomepage": {
-        "message": "w"
-    },
     "quicktext.accesskey.help": {
         "message": "h"
     },
@@ -126,6 +132,18 @@
     "quicktext.accesskeyTemplate.import": {
         "message": "i"
     },
+    "quicktext.buttons.selectStorage.label": {
+        "message": "Speicher ausw?hlen"
+    },
+    "quicktext.buttons.addFolder.label": {
+        "message": "+ Ordner"
+    },
+    "quicktext.buttons.addFile.label": {
+        "message": "+ Datei"
+    },
+    "quicktext.buttons.addUrl.label": {
+        "message": "+ URL"
+    },
     "quicktext.addGroup.label": {
         "message": "Gruppe hinzuf?gen"
     },
@@ -181,7 +199,7 @@
         "message": "Datum/Zeit"
     },
     "quicktext.defaultImport.label": {
-        "message": "Beim Start importieren"
+        "message": "Vorlagen & Skripte beim Start importieren (schreibgesch?tzt)"
     },
     "quicktext.displayname.label": {
         "message": "Anzeigename"
@@ -240,9 +258,6 @@
     "quicktext.getScript.label": {
         "message": "Gemeinsame Skripte"
     },
-    "quicktext.goToHomepage.label": {
-        "message": "Zur Website"
-    },
     "quicktext.group.label": {
         "message": "Gruppe"
     },
@@ -252,6 +267,9 @@
     "quicktext.help.label": {
         "message": "Hilfe"
     },
+    "quicktext.scripthelp.label": {
+        "message": "Inkompatibles Skript"
+    },
     "quicktext.homenumber.label": {
         "message": "Tel. Privat"
     },
@@ -312,6 +330,15 @@
     "quicktext.other.label": {
         "message": "Andere"
     },
+    "quicktext.storageLocations.label": {
+        "message": "Speicherort f?r Vorlagen und Skripte"
+    },
+    "quicktext.storageLocations.description": {
+        "message": "Das ?ndern des Speicherorts f?r Vorlagen und Skripte speichert Ihre aktuelle Liste der Speicherorte und startet Quicktext neu, um die Dateien vom neuen Speicherort zu laden. Alle anderen nicht gespeicherten Einstellungen gehen verloren."
+    },
+    "quicktext.storage.internal.local.label": {
+        "message": "Interner Speicher (Standard)"
+    },
     "quicktext.remove.label": {
         "message": "Entfernen"
     },
@@ -429,4 +456,4 @@
             }
         }
     }
-}
+}
\ Kein Zeilenumbruch am Dateiende.
diff -Nru quicktext-6.3.2/_locales/en-US/messages.json quicktext-6.4.6/_locales/en-US/messages.json
--- quicktext-6.3.2/_locales/en-US/messages.json	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/_locales/en-US/messages.json	2025-08-26 01:05:47.000000000 +0200
@@ -1,4 +1,7 @@
 {
+    "deprecated_attachment_field": {
+        "message": "This input field is deprecated. Use [[ATTACHMENT=FILE|<path>]] directly in the body of the template instead."
+    },
     "altKey": {
         "message": "Alt"
     },
@@ -8,6 +11,9 @@
     "controlKey": {
         "message": "Ctrl"
     },
+    "controlled-via-managed-storage": {
+        "message": "Controlled via managed storage"
+    },
     "date": {
         "message": "Date ($P1$)",
         "placeholders": {
@@ -63,6 +69,12 @@
     "quicktext.HTML.label": {
         "message": "HTML"
     },
+    "quicktext.advanced.label": {
+        "message": "Advanced"
+    },
+    "quicktext.accesskey.advanced": {
+        "message": "a"
+    },
     "quicktext.accesskey.addGroup": {
         "message": "p"
     },
@@ -87,9 +99,6 @@
     "quicktext.accesskey.general": {
         "message": "g"
     },
-    "quicktext.accesskey.goToHomepage": {
-        "message": "h"
-    },
     "quicktext.accesskey.help": {
         "message": "p"
     },
@@ -126,6 +135,18 @@
     "quicktext.accesskeyTemplate.import": {
         "message": "i"
     },
+    "quicktext.buttons.selectStorage.label": {
+        "message": "Select storage"
+    },
+    "quicktext.buttons.addFolder.label": {
+        "message": "+ Folder"
+    },
+    "quicktext.buttons.addFile.label": {
+        "message": "+ File"
+    },
+    "quicktext.buttons.addUrl.label": {
+        "message": "+ URL"
+    },
     "quicktext.addGroup.label": {
         "message": "Add group"
     },
@@ -181,7 +202,7 @@
         "message": "Date/Time"
     },
     "quicktext.defaultImport.label": {
-        "message": "Import on startup"
+        "message": "Import templates & scripts at startup (read-only)"
     },
     "quicktext.displayname.label": {
         "message": "Displayname"
@@ -240,9 +261,6 @@
     "quicktext.getScript.label": {
         "message": "Community scripts"
     },
-    "quicktext.goToHomepage.label": {
-        "message": "Go to homepage"
-    },
     "quicktext.group.label": {
         "message": "Group"
     },
@@ -252,6 +270,9 @@
     "quicktext.help.label": {
         "message": "Help"
     },
+    "quicktext.scripthelp.label": {
+        "message": "Incompatible script"
+    },
     "quicktext.homenumber.label": {
         "message": "Homenumber"
     },
@@ -312,6 +333,15 @@
     "quicktext.other.label": {
         "message": "Other"
     },
+    "quicktext.storageLocations.label": {
+        "message": "Storage location for templates and scripts"
+    },
+    "quicktext.storageLocations.description": {
+        "message": "Changing the storage location for templates and scripts will save your current list of storage locations and restart Quicktext to load the files from the new location. Any other unsaved settings will be lost."
+    },
+    "quicktext.storage.internal.local.label": {
+        "message": "Internal storage (default)"
+    },
     "quicktext.remove.label": {
         "message": "Remove"
     },
@@ -429,4 +459,4 @@
             }
         }
     }
-}
+}
\ Kein Zeilenumbruch am Dateiende.
diff -Nru quicktext-6.3.2/_locales/es/messages.json quicktext-6.4.6/_locales/es/messages.json
--- quicktext-6.3.2/_locales/es/messages.json	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/_locales/es/messages.json	2025-08-26 01:05:47.000000000 +0200
@@ -1,4 +1,7 @@
 {
+    "deprecated_attachment_field": {
+        "message": "This input field is deprecated. Use [[ATTACHMENT=FILE|<path>]] directly in the body of the template instead."
+    },
     "altKey": {
         "message": "Alt"
     },
@@ -63,6 +66,12 @@
     "quicktext.HTML.label": {
         "message": "HTML"
     },
+    "quicktext.advanced.label": {
+        "message": "Avanzado"
+    },
+    "quicktext.accesskey.advanced": {
+        "message": "a"
+    },
     "quicktext.accesskey.addGroup": {
         "message": "p"
     },
@@ -87,9 +96,6 @@
     "quicktext.accesskey.general": {
         "message": "g"
     },
-    "quicktext.accesskey.goToHomepage": {
-        "message": "h"
-    },
     "quicktext.accesskey.help": {
         "message": "y"
     },
@@ -126,6 +132,18 @@
     "quicktext.accesskeyTemplate.import": {
         "message": "i"
     },
+    "quicktext.buttons.selectStorage.label": {
+        "message": "Seleccionar almacenamiento"
+    },
+    "quicktext.buttons.addFolder.label": {
+        "message": "+ Carpeta"
+    },
+    "quicktext.buttons.addFile.label": {
+        "message": "+ Archivo"
+    },
+    "quicktext.buttons.addUrl.label": {
+        "message": "+ URL"
+    },
     "quicktext.addGroup.label": {
         "message": "Agregar grupo"
     },
@@ -181,7 +199,7 @@
         "message": "Fecha/Hora"
     },
     "quicktext.defaultImport.label": {
-        "message": "Importar al arrancar"
+        "message": "Importar plantillas y scripts al iniciar (solo lectura)"
     },
     "quicktext.displayname.label": {
         "message": "Nombre mostrado"
@@ -240,9 +258,6 @@
     "quicktext.getScript.label": {
         "message": "Community Scripts"
     },
-    "quicktext.goToHomepage.label": {
-        "message": "Ir a la p?gina de inicio"
-    },
     "quicktext.group.label": {
         "message": "Grupo"
     },
@@ -252,6 +267,9 @@
     "quicktext.help.label": {
         "message": "Ayuda"
     },
+    "quicktext.scripthelp.label": {
+        "message": "?Gui?n incompatible"
+    },
     "quicktext.homenumber.label": {
         "message": "N?mero de casa"
     },
@@ -312,6 +330,15 @@
     "quicktext.other.label": {
         "message": "Otro"
     },
+    "quicktext.storageLocations.label": {
+        "message": "Ubicaci?n de almacenamiento de plantillas y scripts"
+    },
+    "quicktext.storageLocations.description": {
+        "message": "Al cambiar la ubicaci?n de almacenamiento de plantillas y scripts, se guardar? la lista actual de ubicaciones y Quicktext se reiniciar? para cargar los archivos desde la nueva ubicaci?n. Cualquier otra configuraci?n no guardada se perder?."
+    },
+    "quicktext.storage.internal.local.label": {
+        "message": "Almacenamiento interno (predeterminado)"
+    },
     "quicktext.remove.label": {
         "message": "Eliminar"
     },
@@ -429,4 +456,4 @@
             }
         }
     }
-}
+}
\ Kein Zeilenumbruch am Dateiende.
diff -Nru quicktext-6.3.2/_locales/fr/messages.json quicktext-6.4.6/_locales/fr/messages.json
--- quicktext-6.3.2/_locales/fr/messages.json	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/_locales/fr/messages.json	2025-08-26 01:05:47.000000000 +0200
@@ -1,4 +1,7 @@
 {
+    "deprecated_attachment_field": {
+        "message": "This input field is deprecated. Use [[ATTACHMENT=FILE|<path>]] directly in the body of the template instead."
+    },
     "altKey": {
         "message": "Alt"
     },
@@ -63,6 +66,12 @@
     "quicktext.HTML.label": {
         "message": "HTML"
     },
+    "quicktext.advanced.label": {
+        "message": "Avanc?"
+    },
+    "quicktext.accesskey.advanced": {
+        "message": "a"
+    },
     "quicktext.accesskey.addGroup": {
         "message": "p"
     },
@@ -87,9 +96,6 @@
     "quicktext.accesskey.general": {
         "message": "g"
     },
-    "quicktext.accesskey.goToHomepage": {
-        "message": "w"
-    },
     "quicktext.accesskey.help": {
         "message": "d"
     },
@@ -126,6 +132,18 @@
     "quicktext.accesskeyTemplate.import": {
         "message": "i"
     },
+    "quicktext.buttons.selectStorage.label": {
+        "message": "S?lectionner le stockage"
+    },
+    "quicktext.buttons.addFolder.label": {
+        "message": "+ Dossier"
+    },
+    "quicktext.buttons.addFile.label": {
+        "message": "+ Fichier"
+    },
+    "quicktext.buttons.addUrl.label": {
+        "message": "+ URL"
+    },
     "quicktext.addGroup.label": {
         "message": "Ajouter un groupe"
     },
@@ -181,7 +199,7 @@
         "message": "Date/Heure"
     },
     "quicktext.defaultImport.label": {
-        "message": "Import? au d?marrage"
+        "message": "Importer les mod?les et scripts au d?marrage (lecture seule)"
     },
     "quicktext.displayname.label": {
         "message": "Nom ? afficher"
@@ -240,9 +258,6 @@
     "quicktext.getScript.label": {
         "message": "Scripts partag?s"
     },
-    "quicktext.goToHomepage.label": {
-        "message": "Visiter le site Web"
-    },
     "quicktext.group.label": {
         "message": "Groupe"
     },
@@ -252,6 +267,9 @@
     "quicktext.help.label": {
         "message": "Aide"
     },
+    "quicktext.scripthelp.label": {
+        "message": "Script incompatible"
+    },
     "quicktext.homenumber.label": {
         "message": "T?l. domicile"
     },
@@ -312,6 +330,15 @@
     "quicktext.other.label": {
         "message": "Autres"
     },
+    "quicktext.storageLocations.label": {
+        "message": "Emplacement de stockage des mod?les et scripts"
+    },
+    "quicktext.storageLocations.description": {
+        "message": "La modification de l?emplacement de stockage des mod?les et scripts enregistrera votre liste actuelle d?emplacements, puis red?marrera Quicktext pour charger les fichiers depuis le nouvel emplacement. Tout autre param?tre non enregistr? sera perdu."
+    },
+    "quicktext.storage.internal.local.label": {
+        "message": "Stockage interne (par d?faut)"
+    },
     "quicktext.remove.label": {
         "message": "Supprimer"
     },
@@ -429,4 +456,4 @@
             }
         }
     }
-}
+}
\ Kein Zeilenumbruch am Dateiende.
diff -Nru quicktext-6.3.2/_locales/hu/messages.json quicktext-6.4.6/_locales/hu/messages.json
--- quicktext-6.3.2/_locales/hu/messages.json	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/_locales/hu/messages.json	2025-08-26 01:05:47.000000000 +0200
@@ -1,4 +1,7 @@
 {
+    "deprecated_attachment_field": {
+        "message": "This input field is deprecated. Use [[ATTACHMENT=FILE|<path>]] directly in the body of the template instead."
+    },
     "altKey": {
         "message": "Alt"
     },
@@ -66,6 +69,12 @@
     "quicktext.HTML.label": {
         "message": "HTML"
     },
+    "quicktext.advanced.label": {
+        "message": "Halad?"
+    },
+    "quicktext.accesskey.advanced": {
+        "message": "a"
+    },
     "quicktext.accesskey.addGroup": {
         "message": "p"
     },
@@ -90,9 +99,6 @@
     "quicktext.accesskey.general": {
         "message": "n"
     },
-    "quicktext.accesskey.goToHomepage": {
-        "message": "h"
-    },
     "quicktext.accesskey.help": {
         "message": "g"
     },
@@ -129,6 +135,18 @@
     "quicktext.accesskeyTemplate.import": {
         "message": "i"
     },
+    "quicktext.buttons.selectStorage.label": {
+        "message": "T?rol? kiv?laszt?sa"
+    },
+    "quicktext.buttons.addFolder.label": {
+        "message": "+ Mappa"
+    },
+    "quicktext.buttons.addFile.label": {
+        "message": "+ F?jl"
+    },
+    "quicktext.buttons.addUrl.label": {
+        "message": "+ URL"
+    },
     "quicktext.addGroup.label": {
         "message": "Csoport hozz?ad?sa"
     },
@@ -184,7 +202,7 @@
         "message": "D?tum/id?"
     },
     "quicktext.defaultImport.label": {
-        "message": "Import?l?s ind?t?skor"
+        "message": "Sablonok ?s szkriptek ind?t?skor import?l?sa (csak olvashat?)"
     },
     "quicktext.displayname.label": {
         "message": "Megjelen?tend? n?v"
@@ -243,9 +261,6 @@
     "quicktext.getScript.label": {
         "message": "K?z?ss?gi parancsf?jlok"
     },
-    "quicktext.goToHomepage.label": {
-        "message": "Ugr?s a honlapra"
-    },
     "quicktext.group.label": {
         "message": "Csoport"
     },
@@ -255,6 +270,9 @@
     "quicktext.help.label": {
         "message": "S?g?"
     },
+    "quicktext.scripthelp.label": {
+        "message": "Nem kompatibilis szkript"
+    },
     "quicktext.homenumber.label": {
         "message": "Otthoni sz?m"
     },
@@ -315,6 +333,15 @@
     "quicktext.other.label": {
         "message": "M?s"
     },
+    "quicktext.storageLocations.label": {
+        "message": "T?rol?si hely a sablonokhoz ?s szkriptekhez"
+    },
+    "quicktext.storageLocations.description": {
+        "message": "A sablonok ?s szkriptek t?rol?si hely?nek megv?ltoztat?sa elmenti a t?rol?si helyek aktu?lis list?j?t, majd ?jraind?tja a Quicktextet, hogy bet?ltse a f?jlokat az ?j helyr?l. Minden egy?b nem mentett be?ll?t?s elv?sz."
+    },
+    "quicktext.storage.internal.local.label": {
+        "message": "Bels? t?rol? (alap?rtelmezett)"
+    },
     "quicktext.remove.label": {
         "message": "Elt?vol?t?s"
     },
@@ -432,4 +459,4 @@
             }
         }
     }
-}
+}
\ Kein Zeilenumbruch am Dateiende.
diff -Nru quicktext-6.3.2/_locales/it/messages.json quicktext-6.4.6/_locales/it/messages.json
--- quicktext-6.3.2/_locales/it/messages.json	1970-01-01 01:00:00.000000000 +0100
+++ quicktext-6.4.6/_locales/it/messages.json	2025-08-26 01:05:47.000000000 +0200
@@ -0,0 +1,462 @@
+{
+    "deprecated_attachment_field": {
+        "message": "This input field is deprecated. Use [[ATTACHMENT=FILE|<path>]] directly in the body of the template instead."
+    },
+    "altKey": {
+        "message": "Alt"
+    },
+    "attachmentFile": {
+        "message": "Scegli il file da aggiungere agli allegati"
+    },
+    "controlKey": {
+        "message": "Ctrl"
+    },
+    "controlled-via-managed-storage": {
+        "message": "Controllato tramite archiviazione gestita"
+    },
+    "date": {
+        "message": "Data ($P1$)",
+        "placeholders": {
+            "P1": {
+                "content": "$1"
+            }
+        }
+    },
+    "exportFile": {
+        "message": "Scegli il file in cui esportare (UTF-8)"
+    },
+    "extensionDescription": {
+        "message": "Aggiunge una barra degli strumenti con un numero illimitato di testi da inserire rapidamente. ? anche possibile utilizzare variabili come [[TO=firstname]]. Con impostazioni per tutto."
+    },
+    "fileNotUTF8": {
+        "message": "Il file selezionato non sembra essere codificato in UTF-8 e non verr? caricato correttamente. Seleziona un altro file."
+    },
+    "group": {
+        "message": "Gruppo"
+    },
+    "importFile": {
+        "message": "Scegli il file da importare (UTF-8)"
+    },
+    "inputText": {
+        "message": "Inserisci il valore di $P1$",
+        "placeholders": {
+            "P1": {
+                "content": "$1"
+            }
+        }
+    },
+    "inputTitle": {
+        "message": "Valore della variabile"
+    },
+    "insertFile": {
+        "message": "Scegli il file da inserire (UTF-8)"
+    },
+    "insertImage": {
+        "message": "Scegli il file immagine da inserire"
+    },
+    "metaKey": {
+        "message": "Meta"
+    },
+    "newGroup": {
+        "message": "Nuovo Gruppo"
+    },
+    "newScript": {
+        "message": "Nuovo script"
+    },
+    "newTemplate": {
+        "message": "Nuovo template"
+    },
+    "quicktext.HTML.label": {
+        "message": "HTML"
+    },
+    "quicktext.advanced.label": {
+        "message": "Avanzate"
+    },
+    "quicktext.accesskey.advanced": {
+        "message": "a"
+    },
+    "quicktext.accesskey.addGroup": {
+        "message": "p"
+    },
+    "quicktext.accesskey.addScript": {
+        "message": "t"
+    },
+    "quicktext.accesskey.addTemplate": {
+        "message": "l"
+    },
+    "quicktext.accesskey.close": {
+        "message": "c"
+    },
+    "quicktext.accesskey.communityScripts": {
+        "message": "s"
+    },
+    "quicktext.accesskey.export": {
+        "message": "o"
+    },
+    "quicktext.accesskey.file": {
+        "message": "f"
+    },
+    "quicktext.accesskey.general": {
+        "message": "g"
+    },
+    "quicktext.accesskey.help": {
+        "message": "p"
+    },
+    "quicktext.accesskey.import": {
+        "message": "r"
+    },
+    "quicktext.accesskey.remove": {
+        "message": "v"
+    },
+    "quicktext.accesskey.resetCounter": {
+        "message": "u"
+    },
+    "quicktext.accesskey.save": {
+        "message": "s"
+    },
+    "quicktext.accesskey.scripts": {
+        "message": "s"
+    },
+    "quicktext.accesskey.settings": {
+        "message": "n"
+    },
+    "quicktext.accesskey.templates": {
+        "message": "t"
+    },
+    "quicktext.accesskeyScripts.export": {
+        "message": "p"
+    },
+    "quicktext.accesskeyScripts.import": {
+        "message": "m"
+    },
+    "quicktext.accesskeyTemplate.export": {
+        "message": "e"
+    },
+    "quicktext.accesskeyTemplate.import": {
+        "message": "i"
+    },
+    "quicktext.buttons.selectStorage.label": {
+        "message": "Seleziona archivio"
+    },
+    "quicktext.buttons.addFolder.label": {
+        "message": "+ Cartella"
+    },
+    "quicktext.buttons.addFile.label": {
+        "message": "+ File"
+    },
+    "quicktext.buttons.addUrl.label": {
+        "message": "+ URL"
+    },
+    "quicktext.addGroup.label": {
+        "message": "Aggiungi gruppo"
+    },
+    "quicktext.addScript.label": {
+        "message": "Aggiungi script"
+    },
+    "quicktext.addTemplate.label": {
+        "message": "Aggiungi template"
+    },
+    "quicktext.altKey.label": {
+        "message": "Alt"
+    },
+    "quicktext.attachments.label": {
+        "message": "Allegati"
+    },
+    "quicktext.browse.label": {
+        "message": "Sfoglia"
+    },
+    "quicktext.cellularnumber.label": {
+        "message": "Numero di cellulare"
+    },
+    "quicktext.clipboard.label": {
+        "message": "Appunti"
+    },
+    "quicktext.close.label": {
+        "message": "Chiudi"
+    },
+    "quicktext.collapseSetting.label": {
+        "message": "Riduci il gruppo quando contiene un solo modello"
+    },
+    "quicktext.controlKey.label": {
+        "message": "Ctrl"
+    },
+    "quicktext.counter.label": {
+        "message": "Contatore"
+    },
+    "quicktext.cursor.label": {
+        "message": "Imposta la posizione del cursore"
+    },
+    "quicktext.custom1.label": {
+        "message": "Personalizzato 1"
+    },
+    "quicktext.custom2.label": {
+        "message": "Personalizzato 2"
+    },
+    "quicktext.custom3.label": {
+        "message": "Personalizzato 3"
+    },
+    "quicktext.custom4.label": {
+        "message": "Personalizzato 4"
+    },
+    "quicktext.dateTime.label": {
+        "message": "Data/Ora"
+    },
+    "quicktext.defaultImport.label": {
+        "message": "Importa modelli e script all?avvio (sola lettura)"
+    },
+    "quicktext.displayname.label": {
+        "message": "Nome visualizzato"
+    },
+    "quicktext.email.label": {
+        "message": "Email"
+    },
+    "quicktext.enterKey.label": {
+        "message": "Invio"
+    },
+    "quicktext.export.label": {
+        "message": "Esporta"
+    },
+    "quicktext.f11Key.label": {
+        "message": "F11"
+    },
+    "quicktext.f12Key.label": {
+        "message": "F12"
+    },
+    "quicktext.f2Key.label": {
+        "message": "F2"
+    },
+    "quicktext.f4Key.label": {
+        "message": "F4"
+    },
+    "quicktext.f5Key.label": {
+        "message": "F5"
+    },
+    "quicktext.f8Key.label": {
+        "message": "F8"
+    },
+    "quicktext.faxnumber.label": {
+        "message": "Numero di fax"
+    },
+    "quicktext.file.label": {
+        "message": "File"
+    },
+    "quicktext.filename.label": {
+        "message": "Nome del file"
+    },
+    "quicktext.filenameAndSize.label": {
+        "message": "Nome file e dimensione"
+    },
+    "quicktext.firstname.label": {
+        "message": "Nome"
+    },
+    "quicktext.from.label": {
+        "message": "Da"
+    },
+    "quicktext.fullname.label": {
+        "message": "Nome completo"
+    },
+    "quicktext.general.label": {
+        "message": "Generale"
+    },
+    "quicktext.getScript.label": {
+        "message": "Script della community"
+    },
+    "quicktext.group.label": {
+        "message": "Gruppo"
+    },
+    "quicktext.header.label": {
+        "message": "Aggiungi intestazione (A, Cc, Ccn)"
+    },
+    "quicktext.help.label": {
+        "message": "Aiuto"
+    },
+    "quicktext.scripthelp.label": {
+        "message": "Scrittura incompatibile"
+    },
+    "quicktext.homenumber.label": {
+        "message": "Numero di casa"
+    },
+    "quicktext.image.label": {
+        "message": "Immagine HTML incorporata"
+    },
+    "quicktext.import.label": {
+        "message": "Importa"
+    },
+    "quicktext.input.label": {
+        "message": "Valore immesso dall'utente tramite prompt"
+    },
+    "quicktext.insertAs.label": {
+        "message": "Inserisci come"
+    },
+    "quicktext.insertTextFromFileAsHTML.label": {
+        "message": "Inserisci file come HTML"
+    },
+    "quicktext.insertTextFromFileAsText.label": {
+        "message": "Inserisci file come testo"
+    },
+    "quicktext.insertfile.label": {
+        "message": "Contenuto dal file (le variabili quicktext saranno sostituite)"
+    },
+    "quicktext.jobtitle.label": {
+        "message": "Titolo di lavoro"
+    },
+    "quicktext.keyword.label": {
+        "message": "Parola chiave"
+    },
+    "quicktext.keywordKeySetting.label": {
+        "message": "? il tasto che si vuole utilizzare per attivare una parola chiave"
+    },
+    "quicktext.label": {
+        "message": "Quicktext"
+    },
+    "quicktext.lastname.label": {
+        "message": "Cognome"
+    },
+    "quicktext.metaKey.label": {
+        "message": "Meta"
+    },
+    "quicktext.modifierSetting.label": {
+        "message": "? il modificatore che voglio usare per le scorciatoie da tastiera"
+    },
+    "quicktext.nickname.label": {
+        "message": "Soprannome"
+    },
+    "quicktext.none.label": {
+        "message": "Nessuno"
+    },
+    "quicktext.orgatt.label": {
+        "message": "Informazioni sull'allegato originale"
+    },
+    "quicktext.orgheader.label": {
+        "message": "Informazioni dell'intestazione originale"
+    },
+    "quicktext.other.label": {
+        "message": "Altro"
+    },
+    "quicktext.storageLocations.label": {
+        "message": "Posizione di archiviazione per modelli e script"
+    },
+    "quicktext.remove.label": {
+        "message": "Rimuovi"
+    },
+    "quicktext.storageLocations.description": {
+        "message": "La modifica della posizione di archiviazione di modelli e script salver? l?elenco attuale delle posizioni di archiviazione e riavvier? Quicktext per caricare i file dalla nuova posizione. Tutte le altre impostazioni non salvate andranno perse."
+    },
+    "quicktext.storage.internal.local.label": {
+        "message": "Archiviazione interna (predefinita)"
+    },
+    "quicktext.resetcounter.label": {
+        "message": "Azzera il contatore"
+    },
+    "quicktext.showContextMenu.label": {
+        "message": "Mostra il menu Quicktext al click destro"
+    },
+    "quicktext.save.label": {
+        "message": "Salva"
+    },
+    "quicktext.script.label": {
+        "message": "Script"
+    },
+    "quicktext.scripts.label": {
+        "message": "Script"
+    },
+    "quicktext.selection.label": {
+        "message": "Selezione"
+    },
+    "quicktext.settings.label": {
+        "message": "Impostazioni"
+    },
+    "quicktext.settings.title": {
+        "message": "Impostazioni Quicktext"
+    },
+    "quicktext.sharingScripts.label": {
+        "message": "Condivisione di script"
+    },
+    "quicktext.sharingTemplates.label": {
+        "message": "Condivisione dei modelli"
+    },
+    "quicktext.shortcut.label": {
+        "message": "Scorciatoia"
+    },
+    "quicktext.shortcutTypeAdv.label": {
+        "message": "Usa la modalit? avanzata per le scorciatoie"
+    },
+    "quicktext.shortname.label": {
+        "message": "Quicktext"
+    },
+    "quicktext.spaceKey.label": {
+        "message": "Spazio"
+    },
+    "quicktext.subject.label": {
+        "message": "Oggetto"
+    },
+    "quicktext.tabKey.label": {
+        "message": "Tab"
+    },
+    "quicktext.template.label": {
+        "message": "Modello"
+    },
+    "quicktext.templates.label": {
+        "message": "Modelli"
+    },
+    "quicktext.text.label": {
+        "message": "Testo"
+    },
+    "quicktext.title.label": {
+        "message": "Titolo"
+    },
+    "quicktext.to.label": {
+        "message": "A"
+    },
+    "quicktext.url.label": {
+        "message": "Risposta dall'URL"
+    },
+    "quicktext.variables.label": {
+        "message": "Variabili"
+    },
+    "quicktext.version.label": {
+        "message": "Versione di Thunderbird"
+    },
+    "quicktext.workphone.label": {
+        "message": "Numero di lavoro"
+    },
+    "remove": {
+        "message": "Sei sicuro di voler rimuovere ?$P1$??",
+        "placeholders": {
+            "P1": {
+                "content": "$1"
+            }
+        }
+    },
+    "saveMessage": {
+        "message": "Le modifiche non sono state salvate. Vuoi salvarle?"
+    },
+    "saveMessageTitle": {
+        "message": "Salva impostazioni"
+    },
+    "scriptError": {
+        "message": "Si ? verificato un errore nello script Quicktext:"
+    },
+    "scriptLine": {
+        "message": "Linea"
+    },
+    "scriptNotFound": {
+        "message": "Script Quicktext ?$P1$? non trovato!",
+        "placeholders": {
+            "P1": {
+                "content": "$1"
+            }
+        }
+    },
+    "template": {
+        "message": "Modello"
+    },
+    "time": {
+        "message": "Ora ($P1$)",
+        "placeholders": {
+            "P1": {
+                "content": "$1"
+            }
+        }
+    }
+}
\ Kein Zeilenumbruch am Dateiende.
diff -Nru quicktext-6.3.2/_locales/ja/messages.json quicktext-6.4.6/_locales/ja/messages.json
--- quicktext-6.3.2/_locales/ja/messages.json	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/_locales/ja/messages.json	2025-08-26 01:05:47.000000000 +0200
@@ -1,4 +1,7 @@
 {
+    "deprecated_attachment_field": {
+        "message": "This input field is deprecated. Use [[ATTACHMENT=FILE|<path>]] directly in the body of the template instead."
+    },
     "altKey": {
         "message": "Alt"
     },
@@ -63,6 +66,12 @@
     "quicktext.HTML.label": {
         "message": "HTML"
     },
+    "quicktext.advanced.label": {
+        "message": "????"
+    },
+    "quicktext.accesskey.advanced": {
+        "message": "a"
+    },
     "quicktext.accesskey.addGroup": {
         "message": "p"
     },
@@ -87,9 +96,6 @@
     "quicktext.accesskey.general": {
         "message": "g"
     },
-    "quicktext.accesskey.goToHomepage": {
-        "message": "h"
-    },
     "quicktext.accesskey.help": {
         "message": "p"
     },
@@ -126,6 +132,18 @@
     "quicktext.accesskeyTemplate.import": {
         "message": "i"
     },
+    "quicktext.buttons.selectStorage.label": {
+        "message": "????????"
+    },
+    "quicktext.buttons.addFolder.label": {
+        "message": "??????"
+    },
+    "quicktext.buttons.addFile.label": {
+        "message": "?????"
+    },
+    "quicktext.buttons.addUrl.label": {
+        "message": "?URL"
+    },
     "quicktext.addGroup.label": {
         "message": "???????"
     },
@@ -181,7 +199,7 @@
         "message": "?????"
     },
     "quicktext.defaultImport.label": {
-        "message": "?????????"
+        "message": "?????????????????????????????"
     },
     "quicktext.displayname.label": {
         "message": "???"
@@ -240,9 +258,6 @@
     "quicktext.getScript.label": {
         "message": "???????????"
     },
-    "quicktext.goToHomepage.label": {
-        "message": "?????????"
-    },
     "quicktext.group.label": {
         "message": "????"
     },
@@ -252,6 +267,9 @@
     "quicktext.help.label": {
         "message": "???"
     },
+    "quicktext.scripthelp.label": {
+        "message": "??????????????"
+    },
     "quicktext.homenumber.label": {
         "message": "???????"
     },
@@ -312,9 +330,18 @@
     "quicktext.other.label": {
         "message": "???"
     },
+    "quicktext.storageLocations.label": {
+        "message": "?????????????????"
+    },
     "quicktext.remove.label": {
         "message": "??"
     },
+    "quicktext.storageLocations.description": {
+        "message": "????????????????????????????????????????Quicktext ??????????????????????????????????????????"
+    },
+    "quicktext.storage.internal.local.label": {
+        "message": "?????????????)"
+    },
     "quicktext.resetcounter.label": {
         "message": "??????????"
     },
@@ -429,4 +456,4 @@
             }
         }
     }
-}
+}
\ Kein Zeilenumbruch am Dateiende.
diff -Nru quicktext-6.3.2/_locales/pt_BR/messages.json quicktext-6.4.6/_locales/pt_BR/messages.json
--- quicktext-6.3.2/_locales/pt_BR/messages.json	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/_locales/pt_BR/messages.json	2025-08-26 01:05:47.000000000 +0200
@@ -1,4 +1,7 @@
 {
+    "deprecated_attachment_field": {
+        "message": "This input field is deprecated. Use [[ATTACHMENT=FILE|<path>]] directly in the body of the template instead."
+    },
     "altKey": {
         "message": "Alt"
     },
@@ -63,6 +66,12 @@
     "quicktext.HTML.label": {
         "message": "HTML"
     },
+    "quicktext.advanced.label": {
+        "message": "Avan?ado"
+    },
+    "quicktext.accesskey.advanced": {
+        "message": "a"
+    },
     "quicktext.accesskey.addGroup": {
         "message": "p"
     },
@@ -87,9 +96,6 @@
     "quicktext.accesskey.general": {
         "message": "g"
     },
-    "quicktext.accesskey.goToHomepage": {
-        "message": "r"
-    },
     "quicktext.accesskey.help": {
         "message": "u"
     },
@@ -126,6 +132,18 @@
     "quicktext.accesskeyTemplate.import": {
         "message": "i"
     },
+    "quicktext.buttons.selectStorage.label": {
+        "message": "Selecionar armazenamento"
+    },
+    "quicktext.buttons.addFolder.label": {
+        "message": "+ Pasta"
+    },
+    "quicktext.buttons.addFile.label": {
+        "message": "+ Arquivo"
+    },
+    "quicktext.buttons.addUrl.label": {
+        "message": "+ URL"
+    },
     "quicktext.addGroup.label": {
         "message": "Adicionar grupo"
     },
@@ -181,7 +199,7 @@
         "message": "Data/Tempo"
     },
     "quicktext.defaultImport.label": {
-        "message": "Importar na inicializa??o"
+        "message": "Importar modelos e scripts ao iniciar (somente leitura)"
     },
     "quicktext.displayname.label": {
         "message": "Nome em Exibi??o"
@@ -240,9 +258,6 @@
     "quicktext.getScript.label": {
         "message": "Scripts da comunidade"
     },
-    "quicktext.goToHomepage.label": {
-        "message": "Ir para a p?gina inicial"
-    },
     "quicktext.group.label": {
         "message": "Grupo"
     },
@@ -252,6 +267,9 @@
     "quicktext.help.label": {
         "message": "Ajuda"
     },
+    "quicktext.scripthelp.label": {
+        "message": "Script incompat?vel"
+    },
     "quicktext.homenumber.label": {
         "message": "N?mero Residencial"
     },
@@ -312,6 +330,15 @@
     "quicktext.other.label": {
         "message": "Outros"
     },
+    "quicktext.storageLocations.label": {
+        "message": "Local de armazenamento de modelos e scripts"
+    },
+    "quicktext.storageLocations.description": {
+        "message": "Alterar o local de armazenamento de modelos e scripts salvar? sua lista atual de locais e reiniciar? o Quicktext para carregar os arquivos do novo local. Quaisquer outras configura??es n?o salvas ser?o perdidas."
+    },
+    "quicktext.storage.internal.local.label": {
+        "message": "Armazenamento interno (padr?o)"
+    },
     "quicktext.remove.label": {
         "message": "Remover"
     },
@@ -429,4 +456,4 @@
             }
         }
     }
-}
+}
\ Kein Zeilenumbruch am Dateiende.
diff -Nru quicktext-6.3.2/_locales/ru/messages.json quicktext-6.4.6/_locales/ru/messages.json
--- quicktext-6.3.2/_locales/ru/messages.json	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/_locales/ru/messages.json	2025-08-26 01:05:47.000000000 +0200
@@ -1,4 +1,7 @@
 {
+    "deprecated_attachment_field": {
+        "message": "This input field is deprecated. Use [[ATTACHMENT=FILE|<path>]] directly in the body of the template instead."
+    },
     "altKey": {
         "message": "Alt"
     },
@@ -63,6 +66,12 @@
     "quicktext.HTML.label": {
         "message": "HTML"
     },
+    "quicktext.advanced.label": {
+        "message": "?????????????"
+    },
+    "quicktext.accesskey.advanced": {
+        "message": "a"
+    },
     "quicktext.accesskey.addGroup": {
         "message": "?"
     },
@@ -87,9 +96,6 @@
     "quicktext.accesskey.general": {
         "message": "?"
     },
-    "quicktext.accesskey.goToHomepage": {
-        "message": "?"
-    },
     "quicktext.accesskey.help": {
         "message": "?"
     },
@@ -126,6 +132,18 @@
     "quicktext.accesskeyTemplate.import": {
         "message": "?"
     },
+    "quicktext.buttons.selectStorage.label": {
+        "message": "??????? ?????????"
+    },
+    "quicktext.buttons.addFolder.label": {
+        "message": "+ ?????"
+    },
+    "quicktext.buttons.addFile.label": {
+        "message": "+ ????"
+    },
+    "quicktext.buttons.addUrl.label": {
+        "message": "+ URL"
+    },
     "quicktext.addGroup.label": {
         "message": "???????? ??????"
     },
@@ -181,7 +199,7 @@
         "message": "????/?????"
     },
     "quicktext.defaultImport.label": {
-        "message": "?????? ??? ???????"
+        "message": "?????? ???????? ? ???????? ??? ??????? (?????? ??? ??????)"
     },
     "quicktext.displayname.label": {
         "message": "???????????? ???"
@@ -240,9 +258,6 @@
     "quicktext.getScript.label": {
         "message": "??????? ??????????"
     },
-    "quicktext.goToHomepage.label": {
-        "message": "?? ???????? ????????"
-    },
     "quicktext.group.label": {
         "message": "??????"
     },
@@ -252,6 +267,9 @@
     "quicktext.help.label": {
         "message": "??????"
     },
+    "quicktext.scripthelp.label": {
+        "message": "????????????? ????????"
+    },
     "quicktext.homenumber.label": {
         "message": "???????? ???????"
     },
@@ -312,6 +330,15 @@
     "quicktext.other.label": {
         "message": "??????"
     },
+    "quicktext.storageLocations.label": {
+        "message": "????? ???????? ???????? ? ????????"
+    },
+    "quicktext.storageLocations.description": {
+        "message": "????????? ????? ???????? ???????? ? ???????? ???????? ??????? ?????? ???? ???????? ? ???????????? Quicktext ??? ???????? ?????? ?? ?????? ?????. ??? ????????? ????????????? ????????? ????? ????????."
+    },
+    "quicktext.storage.internal.local.label": {
+        "message": "?????????? ????????? (?? ?????????)"
+    },
     "quicktext.remove.label": {
         "message": "???????"
     },
@@ -429,4 +456,4 @@
             }
         }
     }
-}
+}
\ Kein Zeilenumbruch am Dateiende.
diff -Nru quicktext-6.3.2/_locales/sv-SE/messages.json quicktext-6.4.6/_locales/sv-SE/messages.json
--- quicktext-6.3.2/_locales/sv-SE/messages.json	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/_locales/sv-SE/messages.json	2025-08-26 01:05:47.000000000 +0200
@@ -1,4 +1,7 @@
 {
+    "deprecated_attachment_field": {
+        "message": "This input field is deprecated. Use [[ATTACHMENT=FILE|<path>]] directly in the body of the template instead."
+    },
     "altKey": {
         "message": "Alt"
     },
@@ -63,6 +66,12 @@
     "quicktext.HTML.label": {
         "message": "HTML"
     },
+    "quicktext.advanced.label": {
+        "message": "Avancerat"
+    },
+    "quicktext.accesskey.advanced": {
+        "message": "a"
+    },
     "quicktext.accesskey.addGroup": {
         "message": "g"
     },
@@ -87,9 +96,6 @@
     "quicktext.accesskey.general": {
         "message": "w"
     },
-    "quicktext.accesskey.goToHomepage": {
-        "message": "h"
-    },
     "quicktext.accesskey.help": {
         "message": "p"
     },
@@ -126,6 +132,9 @@
     "quicktext.accesskeyTemplate.import": {
         "message": "i"
     },
+    "quicktext.buttons.selectStorage.label": {
+        "message": "V?lj lagring"
+    },
     "quicktext.addGroup.label": {
         "message": "L?gg till grupp"
     },
@@ -181,7 +190,7 @@
         "message": "Datum/Tid"
     },
     "quicktext.defaultImport.label": {
-        "message": "Importeras vid uppstart"
+        "message": "Importera mallar och skript vid start (endast l?sning)"
     },
     "quicktext.displayname.label": {
         "message": "Kortnamn"
@@ -240,9 +249,6 @@
     "quicktext.getScript.label": {
         "message": "Community Scripts"
     },
-    "quicktext.goToHomepage.label": {
-        "message": "G? till hemsidan"
-    },
     "quicktext.group.label": {
         "message": "Grupp"
     },
@@ -252,6 +258,9 @@
     "quicktext.help.label": {
         "message": "Hj?lp"
     },
+    "quicktext.scripthelp.label": {
+        "message": "Inkompatibelt skript"
+    },
     "quicktext.homenumber.label": {
         "message": "Hemnummer"
     },
@@ -312,6 +321,15 @@
     "quicktext.other.label": {
         "message": "?vrigt"
     },
+    "quicktext.storageLocations.label": {
+        "message": "Lagringsplats f?r mallar och skript"
+    },
+    "quicktext.storageLocations.description": {
+        "message": "Att ?ndra lagringsplatsen f?r mallar och skript sparar din aktuella lista ?ver lagringsplatser och startar om Quicktext f?r att ladda filerna fr?n den nya platsen. Alla andra osparade inst?llningar kommer att g? f?rlorade."
+    },
+    "quicktext.storage.internal.local.label": {
+        "message": "Internt lagringsutrymme (standard)"
+    },
     "quicktext.remove.label": {
         "message": "Ta bort"
     },
@@ -429,4 +447,4 @@
             }
         }
     }
-}
+}
\ Kein Zeilenumbruch am Dateiende.
diff -Nru quicktext-6.3.2/manifest.json quicktext-6.4.6/manifest.json
--- quicktext-6.3.2/manifest.json	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/manifest.json	2025-08-26 01:05:47.000000000 +0200
@@ -1,15 +1,15 @@
 {
   "manifest_version": 2,
-  "applications": {
+  "browser_specific_settings": {
     "gecko": {
       "id": "{8845E3B3-E8FB-40E2-95E9-EC40294818C4}",
       "update_url": "https://raw.githubusercontent.com/jobisoft/quicktext/refs/heads/Main/updates.json",
       "strict_min_version": "128.0",
-      "strict_max_version": "138.*"
+      "strict_max_version": "143.*"
     }
   },
   "name": "Quicktext",
-  "version": "6.3.2",
+  "version": "6.4.6",
   "author": "John Bieling",
   "homepage_url": "https://github.com/jobisoft/quicktext",
   "default_locale": "en-US",
@@ -47,7 +47,8 @@
     "addressBooks",
     "messagesRead",
     "downloads",
-    "notifications"
+    "notifications",
+    "<all_urls>"
   ],
   "background": {
     "type": "module",
@@ -116,4 +117,4 @@
       }
     }
   }
-}
\ Kein Zeilenumbruch am Dateiende.
+}
diff -Nru quicktext-6.3.2/modules/menus.mjs quicktext-6.4.6/modules/menus.mjs
--- quicktext-6.3.2/modules/menus.mjs	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/modules/menus.mjs	2025-08-26 01:05:47.000000000 +0200
@@ -112,7 +112,10 @@
         // If this group has only a single child, and menuCollapse is true, print
         // only that.
         if (await storage.getPref("menuCollapse") && children.length == 1) {
-            menuData.push(children[0]);
+            menuData.push({
+                contexts,
+                ...children[0]
+            });
             continue;
         }
 
@@ -132,6 +135,7 @@
         });
     }
 
+    let now = Date.now();
     menuData.push(
         {
             contexts,
@@ -163,27 +167,27 @@
                     children: [
                         {
                             id: "date",
-                            title: await quicktext.parseVariable({ variable: "DATE" }),
+                            title: getDateTimeMenuTitle("date-short", now),
                             onclick: (info, tab) => quicktext.insertVariable({ tabId: tab.id, variable: "DATE" })
                         },
                         {
                             id: "date-long",
-                            title: await quicktext.parseVariable({ variable: "DATE=long" }),
+                            title: getDateTimeMenuTitle("date-long", now),
                             onclick: (info, tab) => quicktext.insertVariable({ tabId: tab.id, variable: "DATE=long" })
                         },
                         {
                             id: "date-month",
-                            title: await quicktext.parseVariable({ variable: "DATE=monthname" }),
+                            title: getDateTimeMenuTitle("date-monthname", now),
                             onclick: (info, tab) => quicktext.insertVariable({ tabId: tab.id, variable: "DATE=monthname" })
                         },
                         {
                             id: "time",
-                            title: await quicktext.parseVariable({ variable: "TIME" }),
+                            title: getDateTimeMenuTitle("time-noseconds", now),
                             onclick: (info, tab) => quicktext.insertVariable({ tabId: tab.id, variable: "TIME" })
                         },
                         {
                             id: "time-seconds",
-                            title: await quicktext.parseVariable({ variable: "TIME=seconds" }),
+                            title: getDateTimeMenuTitle("time-seconds", now),
                             onclick: (info, tab) => quicktext.insertVariable({ tabId: tab.id, variable: "TIME=seconds" })
                         }
                     ]
@@ -217,11 +221,11 @@
             children: [
                 {
                     id: "insertTextFromFileAsText",
-                    onclick: (info, tab) => quicktext.insertContentFromFile(tab.id, 0)
+                    onclick: (info, tab) => quicktext.insertContentFromFile(tab.id, "text/plain")
                 },
                 {
                     id: "insertTextFromFileAsHTML",
-                    onclick: (info, tab) => quicktext.insertContentFromFile(tab.id, 1)
+                    onclick: (info, tab) => quicktext.insertContentFromFile(tab.id, "text/html")
                 },
             ]
         },
@@ -241,6 +245,11 @@
     return menuData;
 }
 
+function getDateTimeMenuTitle(field, timeStamp) {
+    const fieldType = field.split("-")[0];
+    return messenger.i18n.getMessage(fieldType, utils.getDateTimeFormat(field, timeStamp));
+}
+
 async function updateDateTimeMenus() {
     let fields = ["date-short", "date-long", "date-monthname", "time-noseconds", "time-seconds"];
     let menus = ["variables.dateTime."];
@@ -248,7 +257,7 @@
 
     for (let menu of menus) {
         for (let field of fields) {
-            const title = messenger.i18n.getMessage("date", utils.getDateTimeFormat(field, now));
+            const title = getDateTimeMenuTitle(field, now);
             await messenger.menus.update(`${menu}${field}`, { title })
         }
     }
diff -Nru quicktext-6.3.2/modules/quicktext.mjs quicktext-6.4.6/modules/quicktext.mjs
--- quicktext-6.3.2/modules/quicktext.mjs	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/modules/quicktext.mjs	2025-08-26 01:05:47.000000000 +0200
@@ -210,49 +210,26 @@
 
 // ---- INSERT
 
-async function getQuicktextParser({ tabId, forceAsText }) {
-  let templates = await storage.getTemplates();
-  let scripts = await storage.getScripts();
-
-  // If aForceAsText is not set, but after parsing it is set, we should rerun
-  // with aForceAsText set from the beginning. 
-  let qParser = new QuicktextParser(tabId, templates, scripts, forceAsText);
-  await qParser.loadState();
-  return qParser;
+async function getQuicktextParser({ tabId }) {
+  const templates = await storage.getTemplates();
+  const scripts = await storage.getScripts();
+  return new QuicktextParser(tabId, templates, scripts);
 }
 
-
-export async function insertTemplate(tabId, groupIdx, textIdx, forceAsText) {
-  let qParser = await getQuicktextParser({ tabId, forceAsText });
-  let group = qParser.templates.groups[groupIdx];
-  let text = qParser.templates.texts[groupIdx][textIdx];
-
+export async function insertTemplate(tabId, groupIdx, textIdx) {
+  const qParser = await getQuicktextParser({ tabId });
+  const group = qParser.templates.groups[groupIdx];
+  const text = qParser.templates.texts[groupIdx][textIdx];
   await qParser.clearNonPersistentData();
-  qParser.keepStates = true;
   await insertSubject({ qParser, subject: text.subject });
   await insertAttachments({ qParser, attachments: text.attachments });
-  await insertVariable({ qParser, variable: `TEXT=${group.name}|${text.name}` });
-  qParser.keepStates = false;
-  await qParser.clearNonPersistentData();
+  await qParser.parseAndInsert(`[[TEXT=${group.name}|${text.name}]]`);
 }
 
-export async function parseVariable({ tabId, variable, forceAsText, qParser }) {
-  if (!qParser) {
-    qParser = await getQuicktextParser({ tabId, forceAsText })
-  }
-
-  return qParser.parse("[[" + variable + "]]");
-}
-
-export async function insertVariable({ tabId, variable, forceAsText, qParser }) {
-  if (!qParser) {
-    qParser = await getQuicktextParser({ tabId, forceAsText })
-  }
-
-  let parsed = await parseVariable({ tabId, variable, forceAsText, qParser })
-  if (parsed) {
-    await qParser.insertBody(parsed, { extraSpace: false });
-  }
+export async function insertVariable({ tabId, variable }) {
+  const qParser = await getQuicktextParser({ tabId })
+  await qParser.clearNonPersistentData();
+  await qParser.parseAndInsert(`[[${variable}]]`);
 }
 
 async function insertSubject({ qParser, subject }) {
@@ -267,7 +244,8 @@
 }
 
 async function insertAttachments({ qParser, attachments }) {
-  for (let attachment of attachments.split(";")) {
+  let parsedAttachments = await qParser.parse(attachments);
+  for (let attachment of parsedAttachments.split(";")) {
     if (!attachment) {
       continue;
     }
@@ -275,25 +253,33 @@
     let leafName = utils.getLeafName(attachment);
     let type = utils.getTypeFromExtension(leafName);
     let file = new File([bytes], leafName, { type });
-    qParser.addAttachment(file);
+    await qParser.addAttachment(file);
   };
 }
 
-export async function insertContentFromFile(aTabId, aType) {
-  let file = utils.pickFileFromDisc(aType);
+export async function insertContentFromFile(aTabId, insertMode) {
+  let file = await utils.pickFileFromDisc([insertMode]);
   if (file) {
-    return insertFile(aTabId, file, aType);
+    return insertFile(aTabId, file, insertMode);
   }
 }
 
-export async function insertFile(tabId, file, aType) {
+export async function insertFile(tabId, file, insertMode) {
   const content = await utils.getTextFileContent(file);
   if (!content) {
     return;
   }
 
-  let qParser = await getQuicktextParser({ tabId, forceAsText: aType == 0 })
-  await qParser.insertBody(content, { extraSpace: false });
+  let qParser = await getQuicktextParser({ tabId });
+  // The content of the file gets parsed as well. Nested templates which force
+  // text insert mode affect the entire insert operation.
+  let parsedContent = await qParser.process_file_content(content, {
+    insertMode,
+    stripHtmlComments: false,
+  });
+  await qParser.insertBody(parsedContent, {
+    extraSpace: false
+  });
 }
 
 // This function is called from outside and needs to use data of an existing
@@ -328,8 +314,8 @@
       }
 
       let keyword = text.keyword;
-      if (keyword != "" && typeof keywords[keyword.toLowerCase()] == "undefined")
-        keywords[keyword.toLowerCase()] = [i, j];
+      if (keyword != "" && typeof keywords[keyword] == "undefined")
+        keywords[keyword] = [i, j];
     }
   }
   return { keywords, shortcuts };
diff -Nru quicktext-6.3.2/modules/quicktextParser.mjs quicktext-6.4.6/modules/quicktextParser.mjs
--- quicktext-6.3.2/modules/quicktextParser.mjs	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/modules/quicktextParser.mjs	2025-08-26 01:05:47.000000000 +0200
@@ -8,39 +8,65 @@
 import * as storage from "/modules/storage.mjs";
 
 const allowedTags = [
-  'ALERT', 'ATT', 'CLIPBOARD', 'COUNTER', 'CSCRIPT', 'DATE', 'ESCRIPT', 'FILE', 'IMAGE', 'FROM', 'INPUT', 'ORGATT',
+  'ALERT', 'ATT', 'ATTACHMENT', 'CLIPBOARD', 'COUNTER', 'CSCRIPT', 'DATE', 'ESCRIPT', 'FILE', 'IMAGE', 'FROM', 'INPUT', 'ORGATT',
   'ORGHEADER', 'SCRIPT', 'SUBJECT', 'TEXT', 'TIME', 'TO', 'URL', 'VERSION', 'SELECTION', 'HEADER'
 ];
 
+// These tags do not generate content and should be collapsed with a leading line break.
+const collapsingTags = [
+  'ALERT', 'ATTACHMENT', 'HEADER'
+]
+
 // The value of these tags are persistent and only computed once per tab. All other
 // tags are computed once per template insertion and then re-use the computed value.
 // If another template is inserted (or the same template again), the state is cleared.
 const persistentTags = ['COUNTER', 'ORGATT', 'ORGHEADER', 'VERSION'];
 
+// TODO: Some tags (subject, att, from, to) are currently not cached, because they
+//       can be modified in scripts or by other tags. If we find a reliable method
+//       to update the cache using onChange events, we could cache these and declare
+//       them as persistent tags.
+
 export class QuicktextParser {
-  constructor(aTabId, templates, scripts, forceAsText = false) {
+  constructor(aTabId, templates, scripts) {
     this.mTabId = aTabId;
     this.mTemplates = templates;
     this.mScripts = scripts;
-    this.mForceAsText = forceAsText;
+    this.mStaticDetails = null;
+
+    //TODO: Evaluate if these these values SHOULD be preserved (as getters/setters
+    //      into local storage)
+
+    // Insert the content as text/plain into an html composer (verbatim).
+    // Can only be changed by the current template or nested templates which by
+    // definition use the same QuicktextParser. This value is currently not saved
+    // nor restored.
+    this.mForceAsText = false;
+    // The template insertion type (text/html or text/plain).
+    this.mInsertType = null;
 
-    this.mData = {}
-    this.mDetails = null;
+  }
 
-    this.keepStates = false;
+  async parseAndInsert(str) {
+    const parsed = await this.parse(str);
+    if (parsed) {
+      await this.insertBody(parsed, { extraSpace: false });
+    }
   }
 
   async insertBody(aStr, options = {}) {
-    let type = await this.getInsertType();
-    if (type == 0) {
+    let { isPlainText } = await this.getStaticDetails();
+    let extraSpace = options?.extraSpace !== false;
+
+    if (isPlainText || this.mForceAsText) {
       await messenger.tabs.sendMessage(this.mTabId, {
         insertText: aStr,
-        extraSpace: options.extraSpace,
+        extraSpace,
       });
     } else {
       await messenger.tabs.sendMessage(this.mTabId, {
         insertHtml: utils.removeBadHTML(aStr),
-        extraSpace: options.extraSpace,
+        extraSpace,
       });
     }
   }
@@ -55,52 +81,61 @@
     return this.mTemplates;
   }
 
+  async getStateData() {
+    return browser.storage.session
+      .get({ [`QuicktextStateData_${this.mTabId}`]: {} })
+      .then(rv => rv[`QuicktextStateData_${this.mTabId}`]);
+  }
+
+  async setStateData(value) {
+    return browser.storage.session
+      .set({ [`QuicktextStateData_${this.mTabId}`]: value });
+  }
+
   async clearNonPersistentData() {
-    for (let key of Object.keys(this.mData)) {
+    let stateData = await this.getStateData();
+    for (let key of Object.keys(stateData)) {
       if (persistentTags.includes(key)) {
         continue;
       }
-      delete this.mData[key];
+      delete stateData[key];
     }
+    await this.setStateData(stateData);
   }
 
-  async saveState() {
-    let state = {
-      mForceAsText: this.mForceAsText,
-      mData: this.mData,
+  async loadStates(itemsWithDefaults) {
+    const stateData = await this.getStateData();
+    // Shallow clone so we don?t mutate the original.
+    const result = { ...itemsWithDefaults };
+    for (const key of Object.keys(result)) {
+      if (Object.hasOwn(stateData, key)) {
+        result[key] = stateData[key];
+      }
     }
-    await browser.storage.local.set({ [`QuicktextStateData_${this.mTabId}`]: state });
+    return result;
   }
 
-  async loadState() {
-    let stateData = await browser.storage.local
-      .get({ [`QuicktextStateData_${this.mTabId}`]: null })
-      .then(rv => rv[`QuicktextStateData_${this.mTabId}`]);
-
-    if (stateData) {
-      this.mForceAsText = stateData.mForceAsText;
-      this.mData = stateData.mData;
+  async saveStates(items) {
+    let stateData = await this.getStateData();
+    for (let [item, value] of Object.entries(items)) {
+      stateData[item] = value;
     }
+    await this.setStateData(stateData);
   }
 
-  async getInsertType() {
-    let details = await this.getDetails();
-    if (details.isPlainText || this.mForceAsText) {
-      return 0;
+  async getStaticDetails() {
+    if (!this.mStaticDetails) {
+      this.mStaticDetails = await browser.compose.getComposeDetails(this.mTabId);
     }
-    return 1;
+    return this.mStaticDetails
   }
 
   async getDetails() {
-    if (!this.mDetails) {
-      this.mDetails = await browser.compose.getComposeDetails(this.mTabId);
-    }
-    return this.mDetails
+    return browser.compose.getComposeDetails(this.mTabId);
   }
 
   async setDetail(name, newValue) {
     await browser.compose.setComposeDetails(this.mTabId, { [name]: newValue });
-    this.mDetails = await browser.compose.getComposeDetails(this.mTabId);
   }
 
   async addDetail(name, newValue) {
@@ -117,16 +152,14 @@
     values.push(newValue);
 
     await browser.compose.setComposeDetails(this.mTabId, { [name]: values });
-    this.mDetails = await browser.compose.getComposeDetails(this.mTabId);
   }
 
   async addAttachment(file) {
     await browser.compose.addAttachment(this.mTabId, { file })
-    this.mDetails = await browser.compose.getComposeDetails(this.mTabId);
   }
 
-  // These process functions get the data and mostly saves it
-  // in this.mData so if the data is requested again, it is quick.
+  // These process functions get the data and mostly saves their state, 
+  // so if the data is requested again, it is quick.
   // Not all tags have a process function.
 
   // The get-functions takes the data from the process-functions and
@@ -462,43 +495,83 @@
       // Tries to open the file and returning the content.
       try {
         let content = await browser.Quicktext.readTextFile(aVariables[0]);
-        if (aVariables.length > 1 && aVariables[1].includes("force_as_text")) {
-          this.mForceAsText = true;
-        }
-        if (aVariables.length > 1 && aVariables[1].includes("strip_html_comments")) {
-          content = content.replace(/<!--[\s\S]*?(?:-->)/g, '');
-        }
-        return content;
+        let insertMode = aVariables.length > 1 && aVariables[1].includes("force_as_text")
+          ? "text/plain"
+          : "text/html";
+        let stripHtmlComments = aVariables.length > 1 && aVariables[1].includes("strip_html_comments");
+
+        return this.process_file_content(content, {
+          insertMode,
+          stripHtmlComments
+        });
       } catch (e) { console.error(e); }
     }
     return "";
   }
+  async process_file_content(content, options) {
+    let insertMode = options?.insertMode ?? "text/html";
+    let stripHtmlComments = options?.stripHtmlComments == false;
+
+    let { isPlainText } = await this.getStaticDetails();
+    if (insertMode == "text/plain" && isPlainText == false) {
+      this.mForceAsText = true;
+    }
+
+    if (stripHtmlComments) {
+      content = content.replace(/<!--[\s\S]*?(?:-->)/g, '');
+    }
+
+    return this.parse(content);
+  }
 
   async process_image_content(aVariables) {
-    let rv = "";
+    let [mode, source, type] = aVariables;
+    let mode_lc = mode.toLowerCase();
 
-    if (aVariables.length > 0 && aVariables[0] != "") {
-      let mode = (aVariables.length > 1 && "src" == aVariables[1].toString().toLowerCase()) ? "src" : "tag";
+    // The first parameter is optional, defaults to FILE.
+    if (!["url", "file"].includes(mode_lc)) {
+      type = source;
+      source = mode;
+      mode_lc = "file";
+    }
 
-      // Tries to open the file and returning the content
+    if (!type) {
+      type = "tag"
+    }
+
+    let src = "";
+    if (mode && source && type) {
+      // Tries to open the file and return the content
       try {
-        let bytes = await browser.Quicktext.readBinaryFile(aVariables[0]);
-        let leafName = utils.getLeafName(aVariables[0]);
-        let type = utils.getTypeFromExtension(leafName);
-        let binContent = utils.uint8ArrayToBase64(bytes);
-        let src = "data:" + type + ";filename=" + leafName + ";base64," + binContent;
-        rv = (mode == "tag")
-          ? "<img src='" + src + "'>"
-          : src;
+        switch (mode_lc) {
+          case "url": {
+            src = await utils.fetchFileAsDataUrl(source);
+            break;
+          }
+          case "file": {
+            let bytes = await browser.Quicktext.readBinaryFile(source);
+            let leafName = utils.getLeafName(source);
+            let type = utils.getTypeFromExtension(leafName);
+            let binContent = utils.uint8ArrayToBase64(bytes);
+            src = "data:" + type + ";filename=" + leafName + ";base64," + binContent;
+            break;
+          }
+        }
       } catch (e) {
         console.error(e);
       }
     }
-    return rv;
+    if (src) {
+      return (type == "tag")
+        ? "<img src='" + src + "'>"
+        : src;
+    }
+    return "";
   }
+
   async get_image(aVariables) {
-    let details = await this.getDetails();
-    if (!details.isPlainText) {
+    let { isPlainText } = await this.getStaticDetails();
+    if (!isPlainText) {
       // image tag may only be added in html mode
       return this.process_image_content(aVariables);
     } else {
@@ -507,9 +580,9 @@
   }
 
   async process_selection(aVariables) {
-    let details = await this.getDetails();
+    let { isPlainText } = await this.getStaticDetails();
 
-    if (details.isPlainText) {
+    if (isPlainText) {
       return messenger.tabs.sendMessage(this.mTabId, {
         getSelection: "TEXT",
       });
@@ -519,12 +592,12 @@
       });
     }
   }
-  async get_selection(aVariables, aType) {
-    return this.process_selection(aVariables, aType);
+  async get_selection(aVariables) {
+    return this.process_selection(aVariables);
   }
 
   async process_text(aVariables) {
-    if (aVariables.length != 2)
+    if (aVariables.length < 2)
       return "";
     // Looks after the group and text-name and returns
     // the text from it
@@ -538,15 +611,21 @@
             // This will affect also the "parent" template, if the current
             // template is a nested template, because the entire parsed string
             // will be inserted in one go. 
-            if (text.type == "text/plain") {
-              this.mForceAsText = true;
-            }
-            if (aVariables.length > 1 && aVariables[1].includes("force_as_text")) {
+            let { isPlainText } = await this.getStaticDetails();
+            if (
+              (text.type == "text/plain" || (aVariables.length > 2 && aVariables[2].includes("force_as_text"))) &&
+              isPlainText == false
+            ) {
               this.mForceAsText = true;
             }
-            if (aVariables.length > 1 && aVariables[1].includes("strip_html_comments")) {
+
+            // The template insertion type (text/html or text/plain).
+            this.mInsertType = text.type;
+
+            if (aVariables.length > 2 && aVariables[2].includes("strip_html_comments")) {
               content = content.replace(/<!--[\s\S]*?(?:-->)/g, '');
             }
+
             return content;
           }
         }
@@ -560,48 +639,43 @@
   }
 
   async process_input(aVariables) {
-    if (typeof this.mData['INPUT'] == 'undefined')
-      this.mData['INPUT'] = {};
-    if (typeof this.mData['INPUT'].data == 'undefined')
-      this.mData['INPUT'].data = {};
-
-    if (typeof this.mData['INPUT'].data[aVariables[0]] != 'undefined')
-      return this.mData['INPUT'].data;
-
-    let rv;
-    let label = browser.i18n.getMessage("inputText", [aVariables[0]]);
-    let value = typeof aVariables[2] != 'undefined'
-      ? aVariables[2]
-      : "";
-
-    // There are two types of input: select and text.
-    if (aVariables[1] == 'select') {
-      let values = value.split(";");
-      rv = await utils.openPopup(this.mTabId, {
-        selectLabel: label,
-        selectValues: values,
-      });
-    } else {
-      rv = await utils.openPopup(this.mTabId, {
-        promptLabel: label,
-        promptValue: value,
-      });
-    }
-    if (rv) {
-      this.mData['INPUT'].data[aVariables[0]] = rv
-    } else {
-      this.mData['INPUT'].data[aVariables[0]] = "";
+    const inputState = `INPUT_${aVariables[0]}`;
+    let states = await this.loadStates({
+      [inputState]: { checked: false, data: "" }
+    });
+
+    if (!states[inputState].checked) {
+      let rv;
+      let label = browser.i18n.getMessage("inputText", [aVariables[0]]);
+      let value = aVariables[2] ?? "";
+
+      // There are two types of input: select and text.
+      if (aVariables[1] == 'select') {
+        let values = value.split(";");
+        rv = await utils.openPopup(this.mTabId, {
+          selectLabel: label,
+          selectValues: values,
+        });
+      } else {
+        rv = await utils.openPopup(this.mTabId, {
+          promptLabel: label,
+          promptValue: value,
+        });
+      }
+
+      // Note: Empty is cancel.
+      if (rv) {
+        states[inputState].data = rv;
+        states[inputState].checked = true;
+        await this.saveStates(states);
+      }
+
     }
 
-    return this.mData['INPUT'].data;
+    return states[inputState].data;
   }
   async get_input(aVariables) {
-    let data = await this.process_input(aVariables);
-
-    if (typeof data[aVariables[0]] != "undefined")
-      return data[aVariables[0]];
-
-    return "";
+    return this.process_input(aVariables);
   }
 
   async process_alert(aVariables) {
@@ -617,41 +691,42 @@
   }
 
   async preprocess_org() {
-    this.mData['ORGHEADER'] = {};
-    this.mData['ORGHEADER'].checked = true;
-    this.mData['ORGHEADER'].data = {};
-
-    this.mData['ORGATT'] = {};
-    this.mData['ORGATT'].checked = true;
-    this.mData['ORGATT'].data = [];
-
-    let details = await this.getDetails();
-    if (!details.relatedMessageId) {
-      return
-    }
-
+    let states = await this.loadStates({
+      "ORGHEADER": { checked: false, data: {} },
+      "ORGATT": { checked: false, data: [] },
+    });
 
-    // Store all headers in the mData-variable
-    let data = await browser.messages.getFull(details.relatedMessageId);
-    for (let [name, value] of Object.entries(data.headers)) {
-      if (typeof this.mData['ORGHEADER'].data[name] == 'undefined') {
-        this.mData['ORGHEADER'].data[name] = [];
+    if (!states["ORGHEADER"].checked || !states["ORGATT"].checked) {
+      states["ORGHEADER"].checked = true;
+      states["ORGATT"].checked = true;
+
+      let { relatedMessageId } = await this.getStaticDetails();
+      if (relatedMessageId) {
+        // Store all headers in states["ORGHEADER"].
+        let data = await browser.messages.getFull(relatedMessageId);
+        for (let [name, value] of Object.entries(data.headers)) {
+          if (!Object.hasOwn(states["ORGHEADER"].data, name)) {
+            states["ORGHEADER"].data[name] = [];
+          }
+          states["ORGHEADER"].data[name].push(...value);
+        }
+        // Store all attachments in states["ORGATT"].
+        let attachments = await browser.messages.listAttachments(relatedMessageId);
+        for (let attachment of attachments) {
+          states["ORGATT"].data.push(attachment); // {contentType, name, size, partName}
+        }
       }
-      this.mData['ORGHEADER'].data[name].push(...value);
+      await this.saveStates(states)
     }
 
-    // Store all attachments in the mData-variable
-    let attachments = await browser.messages.listAttachments(details.relatedMessageId);
-    for (let attachment of attachments) {
-      this.mData['ORGATT'].data.push(attachment); // {contentType, name, size, partName}
+    return {
+      orgHeaderState: states["ORGHEADER"],
+      orgAttState: states["ORGATT"]
     }
   }
   async process_orgheader(aVariables) {
-    if (this.mData['ORGHEADER'] && this.mData['ORGHEADER'].checked)
-      return this.mData['ORGHEADER'].data;
-
-    await this.preprocess_org();
-    return this.mData['ORGHEADER'].data;
+    const { orgHeaderState } = await this.preprocess_org();
+    return orgHeaderState.data;
   }
   async get_orgheader(aVariables) {
     if (aVariables.length == 0) {
@@ -672,11 +747,8 @@
     return "";
   }
   async process_orgatt(aVariables) {
-    if (this.mData['ORGATT'] && this.mData['ORGATT'].checked)
-      return this.mData['ORGATT'].data;
-
-    await this.preprocess_org();
-    return this.mData['ORGATT'].data;
+    const { orgAttState } = await this.preprocess_org();
+    return orgAttState.data;
   }
   async get_orgatt(aVariables) {
     let data = await this.process_orgatt(aVariables);
@@ -690,18 +762,19 @@
   }
 
   async process_version(aVariables) {
-    if (this.mData['VERSION'] && this.mData['VERSION'].checked) {
-      return this.mData['VERSION'].data;
-    }
+    let states = await this.loadStates({
+      "VERSION": { checked: false, data: {} }
+    });
 
-    let info = await browser.runtime.getBrowserInfo();
-    this.mData['VERSION'] = {};
-    this.mData['VERSION'].checked = true;
-    this.mData['VERSION'].data = {};
-    this.mData['VERSION'].data['number'] = info.version;
-    this.mData['VERSION'].data['full'] = `${info.name} ${info.version}`;
+    if (!states["VERSION"].checked) {
+      let info = await browser.runtime.getBrowserInfo();
+      states["VERSION"].checked = true;
+      states["VERSION"].data['number'] = info.version;
+      states["VERSION"].data['full'] = `${info.name} ${info.version}`;
+      await this.saveStates(states);
+    }
 
-    return this.mData['VERSION'].data;
+    return states["VERSION"].data;
   }
   async get_version(aVariables = []) {
     let data = await this.process_version(aVariables);
@@ -710,7 +783,7 @@
       aVariables.push("full");
     }
 
-    if (typeof data[aVariables[0]] != 'undefined') {
+    if (Object.hasOwn(data, aVariables[0])) {
       return data[aVariables[0]];
     }
 
@@ -718,20 +791,28 @@
   }
 
   async process_att(aVariables) {
-    if (this.mData['ATT'] && this.mData['ATT'].checked)
-      return this.mData['ATT'].data;
-
-    this.mData['ATT'] = {};
-    this.mData['ATT'].checked = true;
-    this.mData['ATT'].data = [];
+    // We cache known attachments, but not the return value itself, since
+    // attachments can be removed/added by scripts.
+    // Note: We do have onAttachmentAdded/onAttachmentRemoved.
+    let att = [];
+    let updated = false;
+    let states = await this.loadStates({
+      "ATT": { data: {} }
+    });
 
     let attachments = await browser.compose.listAttachments(this.mTabId);
     for (let attachment of attachments) {
-      let file = await browser.compose.getAttachmentFile(attachment.id);
-      this.mData['ATT'].data.push([file.name, file.size, file.lastModified]);
+      if (!Object.hasOwn(states["ATT"], attachment.id)) {
+        let file = await browser.compose.getAttachmentFile(attachment.id);
+        states['ATT'][attachment.id] = [file.name, file.size, file.lastModified];
+        updated = true;
+      }
+      att.push(states["ATT"][attachment.id]);
     }
-
-    return this.mData['ATT'].data;
+    if (updated) {
+      await this.saveStates(states);
+    }
+    return att;
   }
   async get_att(aVariables) {
     let data = await this.process_att(aVariables);
@@ -756,60 +837,89 @@
     return "";
   }
 
-  async process_subject(aVariables) {
-    if (this.mData['SUBJECT'] && this.mData['SUBJECT'].checked)
-      return this.mData['SUBJECT'].data;
 
-    this.mData['SUBJECT'] = {};
-    this.mData['SUBJECT'].checked = true;
-    this.mData['SUBJECT'].data = "";
+  async process_attachment(aVariables) {
+    let [mode, source, name] = aVariables;
+    let mode_lc = mode.toLowerCase();
+
+    // The first parameter is optional, defaults to FILE.
+    if (!["url", "file"].includes(mode_lc)) {
+      name = source;
+      source = mode;
+      mode_lc = "file";
+    }
 
-    let details = await this.getDetails();
-    this.mData['SUBJECT'].data = details.subject;
+    switch (mode_lc) {
+      case "url": {
+        let file = await utils.fetchFileAsFile(source, name);
+        await this.addAttachment(file);
+        break;
+      }
+      case "file": {
+        let bytes = await browser.Quicktext.readBinaryFile(source);
+        let leafName = name ?? utils.getLeafName(source);
+        let type = utils.getTypeFromExtension(leafName);
+        let file = new File([bytes], leafName, { type });
+        await this.addAttachment(file);
+        break;
+      }
+    }
+    return "";
+  }
+  async get_attachment(aVariables) {
+    return this.process_attachment(aVariables);
+  }
 
-    return this.mData['SUBJECT'].data;
+  async process_subject(aVariables) {
+    // For now we do not cache the subject. Since scripts can change it, we
+    // need a global onChange event in order to cache and update it correctly.
+    let { subject } = await this.getDetails();
+    return subject;
   }
   async get_subject(aVariables) {
     return this.process_subject(aVariables);
   }
 
-  preprocess_datetime() {
-    this.mData['DATE'] = {};
-    this.mData['DATE'].checked = true;
-    this.mData['DATE'].data = {};
-    this.mData['TIME'] = {};
-    this.mData['TIME'].checked = true;
-    this.mData['TIME'].data = {};
-
-    let timeStamp = new Date();
-    let fields = ["DATE-long", "DATE-short", "DATE-monthname", "TIME-seconds", "TIME-noseconds"];
-    for (let i = 0; i < fields.length; i++) {
-      let field = fields[i];
-      let fieldinfo = field.split("-");
-      this.mData[fieldinfo[0]].data[fieldinfo[1]] = utils.trimString(utils.getDateTimeFormat(field, timeStamp));
+  async preprocess_datetime() {
+    let states = await this.loadStates({
+      "TIME": { checked: false, data: {} },
+      "DATE": { checked: false, data: {} },
+    });
+
+    if (!states["TIME"].checked || !states["DATE"].checked) {
+      states["DATE"].checked = true;
+      states["TIME"].checked = true;
+
+      let timeStamp = new Date();
+      for (let field of ["long", "short", "monthname"]) {
+        states["DATE"].data[field] = utils.trimString(utils.getDateTimeFormat(`date-${field}`, timeStamp));
+      }
+      for (let field of ["seconds", "noseconds"]) {
+        states["TIME"].data[field] = utils.trimString(utils.getDateTimeFormat(`time-${field}`, timeStamp));
+      }
+      await this.saveStates(states);
     }
+
+    return {
+      timeState: states["TIME"],
+      dateState: states["DATE"],
+    };
   }
   async process_date(aVariables) {
-    if (this.mData['DATE'] && this.mData['DATE'].checked)
-      return this.mData['DATE'].data;
-
-    this.preprocess_datetime();
-    return this.mData['DATE'].data;
+    const { dateState } = await this.preprocess_datetime();
+    return dateState.data;
   }
   async process_time(aVariables) {
-    if (this.mData['TIME'] && this.mData['TIME'].checked)
-      return this.mData['TIME'].data;
-
-    this.preprocess_datetime();
-    return this.mData['TIME'].data;
+    const { timeState } = await this.preprocess_datetime();
+    return timeState.data;
   }
   async get_date(aVariables) {
     let data = await this.process_date(aVariables);
-
     if (aVariables.length < 1)
       aVariables[0] = "short";
-    if (typeof data[aVariables[0]] != 'undefined')
+    if (Object.hasOwn(data, aVariables[0])) {
       return data[aVariables[0]];
+    }
 
     return "";
   }
@@ -817,68 +927,96 @@
     let data = await this.process_time(aVariables);
     if (aVariables.length < 1)
       aVariables[0] = "noseconds";
-    if (typeof data[aVariables[0]] != 'undefined')
+    if (Object.hasOwn(data, aVariables[0])) {
       return data[aVariables[0]];
+    }
 
     return "";
   }
 
-  async process_clipboard(aVariables) {
-    if (this.mData['CLIPBOARD'] && this.mData['CLIPBOARD'].checked)
-      return this.mData['CLIPBOARD'].data;
-
-    this.mData['CLIPBOARD'] = {};
-    this.mData['CLIPBOARD'].checked = true;
-    this.mData['CLIPBOARD'].data = "";
-
-    // I do not know how to access html variant, but if, we would call
-    // this.getDetails and check isPlainText to determine if we need it.
-    this.mData['CLIPBOARD'].data = await navigator.clipboard.readText();
+  async process_clipboard() {
+    let states = await this.loadStates({
+      "CLIPBOARD": { checked: false, data: {} }
+    });
+
+    if (!states["CLIPBOARD"].checked) {
+      states['CLIPBOARD'].data.plain = await navigator.clipboard.readText();
+      const html = await navigator.clipboard.read().then(items => items.find(
+        item => item.types.includes("text/html")
+      ));
+      if (html) {
+        states['CLIPBOARD'].data.html = await html.getType("text/html").then(
+          v => v.text()
+        );
+      }
+      await this.saveStates(states);
+    }
 
-    return this.mData['CLIPBOARD'].data;
+    return states['CLIPBOARD'].data;
   }
   async get_clipboard(aVariables) {
-    return utils.trimString(await this.process_clipboard(aVariables));
+    const { isPlainText } = await this.getStaticDetails();
+    const data = await this.process_clipboard();
+    const parameter = aVariables?.[0]?.toLowerCase?.();
+
+    const getFormat = (parameter) => {
+      switch (parameter) {
+        case "auto":
+          // Auto should never paste verbatim html code into the composer. The
+          // insert type must be text/html and the composer must support html.
+          return (!isPlainText && this.mInsertType == "text/html")
+            ? "html"
+            : "plain";
+        case "html":
+          return "html";
+        case "plain":
+        default:
+          return "plain"
+      }
+    }
+
+    return utils.trimString(data[getFormat(parameter)] || data.plain);
   }
 
   async process_counter(aVariables) {
-    if (this.mData['COUNTER'] && this.mData['COUNTER'].checked)
-      return this.mData['COUNTER'].data;
+    let states = await this.loadStates({
+      "COUNTER": { checked: false, data: null }
+    });
 
-    this.mData['COUNTER'] = {};
-    this.mData['COUNTER'].checked = true;
-    this.mData['COUNTER'].data = await storage.getPref("counter");
-    this.mData['COUNTER'].data++;
-    await storage.setPref("counter", this.mData['COUNTER'].data);
+    if (!states["COUNTER"].checked) {
+      states['COUNTER'].checked = true;
+      states['COUNTER'].data = (await storage.getPref("counter")) + 1;
+      await storage.setPref("counter", states['COUNTER'].data);
+      await this.saveStates(states);
+    }
 
-    return this.mData['COUNTER'].data;
+    return states['COUNTER'].data;
   }
   async get_counter(aVariables) {
     return this.process_counter(aVariables);
   }
 
   async process_from(aVariables) {
-    if (this.mData['FROM'] && this.mData['FROM'].checked) {
-      return this.mData['FROM'].data;
-    }
-
+    // For now we do not cache FROM, since it can be changed by scripts. We need
+    // a global on change event for the used identity in order to cache FROM.
+    // Note: We do have onIdentityChanged
     let details = await this.getDetails();
     let identity = await browser.identities.get(details.identityId);
 
-    this.mData['FROM'] = {};
-    this.mData['FROM'].checked = true;
-    this.mData['FROM'].data = {
+    let states = {};
+    states['FROM'] = {};
+    states['FROM'].data = {
       'email': identity.email,
       'displayname': identity.name,
       'firstname': '',
       'lastname': ''
     };
-    await this.getcarddata_from(identity);
+    await this.getcarddata_from(identity, states);
 
-    return this.mData['FROM'].data;
+    return states['FROM'].data;
   }
-  async getcarddata_from(identity) {
-    // 1. CardBook -> need cardbook api
+  async getcarddata_from(identity, states) {
+    // 1. TODO: CardBook -> need cardbook api
     // ...
 
     // 2. search identity email
@@ -888,7 +1026,7 @@
     })
     let card = cards.find(c => c.type == "contact");
 
-    // 3. vcard of identity -> todo: not yet supported
+    // 3. TODO: vcard of identity
     if (!card && identity.escapedVCard) {
       //card = manager.escapedVCardToAbCard(aIdentity.escapedVCard);
     }
@@ -900,26 +1038,25 @@
     // Get directly stored props first.
     for (let [name, value] of Object.entries(card.properties)) {
       // For backward compatibility, use lowercase props.
-      this.mData['FROM'].data[name.toLowerCase()] = value;
+      states['FROM'].data[name.toLowerCase()] = value;
     }
-    this.mData['FROM'].data['fullname'] = utils.trimString(this.mData['FROM'].data['firstname'] + " " + this.mData['FROM'].data['lastname']);
+    states['FROM'].data['fullname'] = utils.trimString(states['FROM'].data['firstname'] + " " + states['FROM'].data['lastname']);
   }
   async get_from(aVariables) {
     let data = await this.process_from(aVariables);
 
-    if (typeof data[aVariables[0]] != 'undefined') {
+    if (Object.hasOwn(data, aVariables[0])) {
       return utils.trimString(data[aVariables[0]]);
     }
     return "";
   }
 
   async process_to(aVariables) {
-    if (this.mData['TO'] && this.mData['TO'].checked)
-      return this.mData['TO'].data;
-
-    this.mData['TO'] = {};
-    this.mData['TO'].checked = true;
-    this.mData['TO'].data = {
+    // For now we do not cache TO, since it can be changed by scripts or by
+    // the HEADER tag.
+    let states = {};
+    states['TO'] = {};
+    states['TO'].data = {
       'email': [],
       'firstname': [],
       'lastname': [],
@@ -933,39 +1070,39 @@
       // TODO: Add code for getting info about all people in a mailing list
 
       let contactData = await utils.parseDisplayName(emailAddresses[i]);
-      let k = this.mData['TO'].data['email'].length;
-      this.mData['TO'].data['email'][k] = contactData.email.toLowerCase();
-      this.mData['TO'].data['fullname'][k] = utils.trimString(contactData.name);
-      this.mData['TO'].data['firstname'][k] = "";
-      this.mData['TO'].data['lastname'][k] = "";
+      let k = states['TO'].data['email'].length;
+      states['TO'].data['email'][k] = contactData.email.toLowerCase();
+      states['TO'].data['fullname'][k] = utils.trimString(contactData.name);
+      states['TO'].data['firstname'][k] = "";
+      states['TO'].data['lastname'][k] = "";
 
-      await this.getcarddata_to(k);
+      await this.getcarddata_to(k, states);
 
-      let validParts = [this.mData['TO'].data['firstname'][k], this.mData['TO'].data['lastname'][k]].filter(e => e.trim() != "");
+      let validParts = [states['TO'].data['firstname'][k], states['TO'].data['lastname'][k]].filter(e => e.trim() != "");
       if (validParts.length == 0) {
         // if no first and last name, generate them from fullname
-        let parts = this.mData['TO'].data['fullname'][k].replace(/,/g, ", ").split(" ").filter(e => e.trim() != "");
-        this.mData['TO'].data['firstname'][k] = parts.length > 1 ? utils.trimString(parts.splice(0, 1)) : "";
-        this.mData['TO'].data['lastname'][k] = utils.trimString(parts.join(" "));
+        let parts = states['TO'].data['fullname'][k].replace(/,/g, ", ").split(" ").filter(e => e.trim() != "");
+        states['TO'].data['firstname'][k] = parts.length > 1 ? utils.trimString(parts.splice(0, 1)) : "";
+        states['TO'].data['lastname'][k] = utils.trimString(parts.join(" "));
       } else {
         // if we have a first and/or last name (which can only happen if read from card), generate fullname from it
-        this.mData['TO'].data['fullname'][k] = validParts.join(" ");
+        states['TO'].data['fullname'][k] = validParts.join(" ");
       }
 
       // swap names if wrong
-      if (this.mData['TO'].data['firstname'][k].endsWith(",")) {
-        let temp_firstname = this.mData['TO'].data['firstname'][k].replace(/,/g, "");
-        let temp_lastname = this.mData['TO'].data['lastname'][k];
-        this.mData['TO'].data['firstname'][k] = temp_lastname;
-        this.mData['TO'].data['lastname'][k] = temp_firstname;
+      if (states['TO'].data['firstname'][k].endsWith(",")) {
+        let temp_firstname = states['TO'].data['firstname'][k].replace(/,/g, "");
+        let temp_lastname = states['TO'].data['lastname'][k];
+        states['TO'].data['firstname'][k] = temp_lastname;
+        states['TO'].data['lastname'][k] = temp_firstname;
         // rebuild fullname
-        this.mData['TO'].data['fullname'][k] = [this.mData['TO'].data['firstname'][k], this.mData['TO'].data['lastname'][k]].join(" ");
+        states['TO'].data['fullname'][k] = [states['TO'].data['firstname'][k], states['TO'].data['lastname'][k]].join(" ");
       }
     }
 
-    return this.mData['TO'].data;
+    return states['TO'].data;
   }
-  async getcarddata_to(aIndex) {
+  async getcarddata_to(aIndex, states) {
     // 1. CardBook -> need cardbook api
     // ...
 
@@ -973,7 +1110,7 @@
     // 2. search identity email
     let cards = await browser.contacts.quickSearch({
       includeRemote: false,
-      searchString: this.mData['TO'].data['email'][aIndex].toLowerCase()
+      searchString: states['TO'].data['email'][aIndex].toLowerCase()
     })
     let card = cards.find(c => c.type == "contact");
 
@@ -982,20 +1119,20 @@
       for (let [name, value] of Object.entries(card.properties)) {
         let lowerCaseName = name.toLowerCase();
 
-        if (typeof this.mData['TO'].data[lowerCaseName] == 'undefined') {
-          this.mData['TO'].data[lowerCaseName] = []
+        if (!Object.hasOwn(states['TO'].data, lowerCaseName)) {
+          states['TO'].data[lowerCaseName] = []
         }
-        if (value != "" || typeof this.mData['TO'].data[lowerCaseName][aIndex] == 'undefined' || this.mData['TO'].data[lowerCaseName][aIndex] == "") {
-          this.mData['TO'].data[lowerCaseName][aIndex] = utils.trimString(value);
+        if (value != "" || !Object.hasOwn(states['TO'].data[lowerCaseName], aIndex) || states['TO'].data[lowerCaseName][aIndex] == "") {
+          states['TO'].data[lowerCaseName][aIndex] = utils.trimString(value);
         }
       }
     }
-    return this.mData;
+    return states;
   }
   async get_to(aVariables) {
     let data = await this.process_to(aVariables);
 
-    if (typeof data[aVariables[0]] != 'undefined') {
+    if (Object.hasOwn(data, aVariables[0])) {
       // use ", " as default seperator
       let mainSep = (aVariables.length > 1) ? aVariables[1].replace(/\\n/g, "\n").replace(/\\t/g, "\t") : ", ";
       let lastSep = (aVariables.length > 2) ? aVariables[2].replace(/\\n/g, "\n").replace(/\\t/g, "\t") : mainSep;
@@ -1035,13 +1172,6 @@
     }
   }
   async parseText(aStr) {
-    // If a template is inserted, keepStates is set to true and all non-persistent
-    // states are kept until the entire template has been processed. The persistent
-    // states are kept for the entire lifetime of the tab.
-    if (!this.keepStates) {
-      await this.clearNonPersistentData();
-    }
-
     let tags = getTags(aStr);
 
     // If we don't find any tags there will be no changes to the string so return.
@@ -1050,9 +1180,6 @@
 
     // Replace all tags with there right contents
     for (let i = 0; i < tags.length; i++) {
-      // Save state.
-      await this.saveState();
-
       let value = "";
       let variable_limit = -1;
       switch (tags[i].tagName.toLowerCase()) {
@@ -1077,6 +1204,7 @@
         case 'cscript':
         case 'to':
         case 'url':
+        case 'attachment':
           variable_limit = 1;
           break;
         case 'text':
@@ -1090,11 +1218,7 @@
       if (typeof this["get_" + tags[i].tagName.toLowerCase()] == "function" && variable_limit >= 0 && tags[i].variables.length >= variable_limit) {
         value = await this["get_" + tags[i].tagName.toLowerCase()](tags[i].variables);
       }
-
-      // Save state.
-      await this.saveState();
-
-      aStr = utils.replaceText(tags[i].tag, value, aStr);
+      aStr = utils.replaceText(tags[i].tag, value, aStr, { collapseLineBreaks: collapsingTags.includes(tags[i].tagName) });
     }
 
     return aStr;
@@ -1104,17 +1228,18 @@
 function getTags(aStr) {
   // We only get the beginning of the tag.
   // This is because we want to handle recursive use of tags.
-  let rexp = new RegExp("\\[\\[((" + allowedTags.join("|") + ")(\\_[a-z]+)?)", "ig");
+  // Sorting to test for longer tags first (ATTACHMENT vs ATT).
+  let rexp = new RegExp("\\[\\[((" + allowedTags.sort((a, b) => b.length - a.length).join("|") + ")(\\_[a-z]+)?)", "ig");
   let results = [];
   let result = null;
   while ((result = rexp.exec(aStr)))
     results.push(result);
 
-  // If we don't found any tags we return
+  // If we did't find any tags we return.
   if (results.length == 0)
     return [];
 
-  // Take care of the tags starting with the last one
+  // Take care of the tags starting with the last one.
   let hits = [];
   results.reverse();
   let strLen = aStr.length;
@@ -1134,15 +1259,15 @@
     else
       tmpHit.tagName = results[i][1];
 
-    // Get the end of the starttag
+    // Get the end of the starttag.
     pos = results[i].index + results[i][1].length + 2;
 
-    // If the tag ended here we're done
+    // If the tag ended here we're done.
     if (aStr.substr(pos, 2) == "]]") {
       tmpHit.tag += "]]";
       hits = addTag(hits, tmpHit);
     }
-    // If there is arguments we get them
+    // If there are arguments we get them.
     else if (aStr[pos] == "=") {
       // We go through until we find ]] but we must have went
       // through the same amount of [ and ] before. So if there
@@ -1165,7 +1290,7 @@
         pos++;
       }
 
-      // If we found the end we parses the arguments
+      // If we found the end we parse the arguments.
       if (ready) {
         tmpHit.tag += "=" + vars + "]]";
         vars = vars.split("|");
diff -Nru quicktext-6.3.2/modules/storage.mjs quicktext-6.4.6/modules/storage.mjs
--- quicktext-6.3.2/modules/storage.mjs	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/modules/storage.mjs	2025-08-26 01:05:47.000000000 +0200
@@ -4,65 +4,256 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  */
 
-export async function getPref(aName, aFallback = null) {
-  const defaultPref = await browser.storage.local
-    .get({ [`${aName}.default`]: aFallback })
-    .then(o => o[`${aName}.default`]);
+import * as quicktext from "./quicktext.mjs";
+
+const defaultPrefs = {
+  "counter": 0,
+  "templateFolder": "",
+  "defaultImport": JSON.stringify([]),
+  "menuCollapse": true,
+  "toolbar": true,
+  "popup": true,
+  "keywordKey": "Tab",
+  "shortcutModifier": "alt",
+  "shortcutTypeAdv": false,
+  "collapseState": "",
+  "storageLocations": JSON.stringify([{
+    source: "INTERNAL",
+    data: "local",
+  }]),
+  "activeStorageLocationIdx": 0
+};
+
+const managedPrefs = [
+  "storageLocations",
+  "activeStorageLocationIdx",
+  "defaultImport",
+  "menuCollapse",
+  "popup",
+  "keywordKey",
+  "shortcutModifier",
+  "shortcutTypeAdv",
+];
+
+let ACTIVE_STORAGE;
+
+async function getActiveStorage() {
+  if (!ACTIVE_STORAGE) {
+    let storageLocations = JSON.parse(await getPref("storageLocations"));
+    let activeStorageLocationIdx = await getPref("activeStorageLocationIdx");
+    let { source, data } = storageLocations[activeStorageLocationIdx];
+    ACTIVE_STORAGE = {
+      source: source.toLowerCase(),
+      data,
+    }
+  }
+  return ACTIVE_STORAGE;
+}
+
+function migratePrefOnTheFly(data, name) {
+  switch (name) {
+    case "defaultImport": {
+      try {
+        JSON.parse(data[name]);
+        return data[name];
+      } catch {
+        // Is not a JSON and needs to be migrated.
+      }
+
+      // Assume legacy string, separated by ";"
+      let defaultImports = [];
+      for (let path of data[name].split(";").map(e => e.trim()).filter(Boolean)) {
+        if (!path.match(/^(http|https):\/\//)) {
+          defaultImports.push({
+            source: "FILE",
+            data: path
+          })
+        } else {
+          defaultImports.push({
+            source: "URL",
+            data: path
+          })
+        }
+      }
+      return JSON.stringify(defaultImports);
+    }
+    default:
+      return data[name];
+  }
+}
+
+async function getManagedPref(aName) {
+  if (!managedPrefs.includes(aName)) {
+    return undefined;
+  }
+  try {
+    let override = await browser.storage.managed.get({ [aName]: undefined });
+    return migratePrefOnTheFly(override, aName);
+  } catch {
+    // No managed storage available.
+  }
+  return undefined;
+}
+async function getLocalPref(aName, aFallback = undefined) {
+  const defaultPref = Object.hasOwn(defaultPrefs, aName)
+    ? defaultPrefs[aName]
+    : aFallback
 
   return browser.storage.local
-    .get({ [`${aName}.value`]: defaultPref })
-    .then(o => o[`${aName}.value`]);
+    .get({ [aName]: defaultPref })
+    .then(o => migratePrefOnTheFly(o, aName));
+}
+
+export async function getPrefWithManagedInfo(aName, aFallback = undefined) {
+  let managedPref = await getManagedPref(aName);
+  if (managedPref !== undefined) {
+    return { value: managedPref, isManaged: true }
+  }
+  let localPref = await getLocalPref(aName, aFallback);
+  return { value: localPref, isManaged: false }
+}
+
+export async function getPref(aName, aFallback = undefined) {
+  const managedPref = await getManagedPref(aName);
+  if (managedPref !== undefined) {
+    return managedPref;
+  }
+  return getLocalPref(aName, aFallback);
 }
+
 export async function setPref(aName, aValue) {
-  await browser.storage.local.set({ [`${aName}.value`]: aValue });
+  await browser.storage.local.set({ [aName]: aValue });
 }
 export async function clearPref(aName) {
-  await browser.storage.local.remove(`${aName}.value`);
+  await browser.storage.local.remove(aName);
+}
+
+/**
+ * Read data fronm the active storage.
+ * 
+ * @param {scripts|templates} type 
+ */
+async function readDataFromStorage(type) {
+  const { source, data: path } = await getActiveStorage();
+  const DEFAULT_RV = {
+    scripts: [],
+    templates: { groups: [], texts: [] },
+  }
+  try {
+    switch (source) {
+      case "internal":
+        return browser.storage.local.get({ [type]: null })
+          .then(e => e[type] ? JSON.parse(e[type]) : null);
+      case "file": {
+        // Try reading the cache first.
+        const cache = await browser.storage.session.get({ [type]: null })
+          .then(e => e[type] ? JSON.parse(e[type]) : null);
+        if (cache) {
+          return cache;
+        }
+
+        // Read the actual storage.
+        const content = await browser.Quicktext.readTextFile(`${type}.json`, path);
+        const parsed = await quicktext.parseConfigFileData(content);
+        if (parsed[type]) {
+          // Update cache.
+          await browser.storage.session.set({ [type]: JSON.stringify(parsed[type]) });
+          return parsed[type];
+        }
+        break;
+      }
+      default:
+        throw new Error(`Unkown storage source "${source}".`);
+    }
+  } catch (ex) {
+    // Failed, use default.
+    console.log(ex)
+  }
+  return DEFAULT_RV[type];
+}
+
+/**
+ * Write data to the active storage.
+ * 
+ * @param {scripts|templates} type
+ * @param {object} content 
+ */
+async function writeDataToStorage(type, content) {
+  const { source, data: path } = await getActiveStorage();
+  try {
+    switch (source) {
+      case "internal":
+        await browser.storage.local.set({ [type]: JSON.stringify(content) });
+        break;
+      case "file": {
+        // Check for changes.
+        const stringContent = JSON.stringify(content);
+        const cache = await browser.storage.session.get({ [type]: null })
+          .then(e => e[type] ? e[type] : null);
+        if (cache && cache == stringContent) {
+          return;
+        }
+
+        // Update cache and write to the actual storage..
+        await browser.storage.session.set({ [type]: stringContent });
+        await browser.Quicktext.writeTextFile(`${type}.json`, JSON.stringify({ [type]: content }), path);
+        break;
+      }
+      default:
+        throw new Error(`Unkown storage source "${source}".`);
+    }
+  } catch (ex) {
+    // Failed.
+    console.log(ex)
+  }
 }
 
 export async function setTemplates(templates) {
-  await browser.storage.local.set({ templates: JSON.stringify(templates) });
+  return writeDataToStorage("templates", templates);
 }
 export async function getTemplates() {
-  return browser.storage.local.get({ templates: null }).then(
-    e => e.templates ? JSON.parse(e.templates) : null);
+  return readDataFromStorage("templates")
 }
-
 export async function setScripts(scripts) {
-  await browser.storage.local.set({ scripts: JSON.stringify(scripts) });
+  return writeDataToStorage("scripts", scripts);
 }
 export async function getScripts() {
-  return browser.storage.local.get({ scripts: null }).then(
-    e => e.scripts ? JSON.parse(e.scripts) : null);
+  return readDataFromStorage("scripts")
 }
 
-export async function init(defaults = null) {
+export async function migrate() {
   // Migrate options from sync to local storage, as sync storage can only hold
   // 100 KB which will not be enough for templates.
-  const { userPrefs: syncUserPrefs } = await browser.storage.sync.get({ userPrefs: null });
+  const { userPrefs: syncUserPrefs } = await browser.storage.sync.get({ userPrefs: undefined });
   if (syncUserPrefs) {
     await browser.storage.local.set({ userPrefs: syncUserPrefs });
-    await browser.storage.sync.set({ userPrefs: null });
+    await browser.storage.sync.remove("userPrefs");
   }
 
-  // Migrate from userPrefs/defaultPrefs objects to *.value and *.default
-  const { userPrefs } = await browser.storage.local.get({ userPrefs: null });
-  if (userPrefs) {
-    for (let [key, value] of Object.entries(userPrefs)) {
+  // Migrate from userPrefs/defaultPrefs objects to *.value and *.default.
+  const { userPrefs: v1UserPrefs } = await browser.storage.local.get({ userPrefs: undefined });
+  if (v1UserPrefs) {
+    for (let [key, value] of Object.entries(v1UserPrefs)) {
       await browser.storage.local.set({ [`${key}.value`]: value });
     }
     await browser.storage.local.remove("userPrefs");
   }
-  const { defaultPrefs } = await browser.storage.local.get({ defaultPrefs: null });
-  if (defaultPrefs) {
+  const { defaultPrefs: v1DefaultPrefs } = await browser.storage.local.get({ defaultPrefs: undefined });
+  if (v1DefaultPrefs) {
     await browser.storage.local.remove("defaultPrefs");
   }
 
-  // If defaults are given, push them into storage.local
-  if (defaults) {
-    for (let [key, value] of Object.entries(defaults)) {
-      await browser.storage.local.set({ [`${key}.default`]: value });
+  // Migrate from *.value and *.default to simple values.
+  for (let aName of Object.keys(defaultPrefs)) {
+    const aValue = await browser.storage.local
+      .get({ [`${aName}.value`]: undefined })
+      .then(o => o[`${aName}.value`]);
+    if (aValue !== undefined) {
+      await browser.storage.local.remove(`${aName}.value`);
+      await browser.storage.local.set({ [aName]: aValue });
     }
+    await browser.storage.local.remove(`${aName}.default`);
+    await browser.storage.local.remove(`${aName}.managed.value`);
   }
 }
 
@@ -77,10 +268,16 @@
     this.#changedWatchedPrefs = {}
   }
 
-  #eventCollapse = (changes, area) => {
+  #eventCollapse = async (changes, area) => {
     if (area == "local") {
       for (let [key, value] of Object.entries(changes)) {
-        const watchedPref = this.#watchedPrefs.find(p => key == `${p}.value` || key == p);
+        const watchedPref = this.#watchedPrefs.find(p => key == p);
+
+        // Do not monitor managed prefs.
+        let managedPref = await getManagedPref(key);
+        if (managedPref !== undefined) {
+          continue;
+        }
 
         if (watchedPref && value.oldValue != value.newValue) {
           this.#changedWatchedPrefs[watchedPref] = value;
diff -Nru quicktext-6.3.2/modules/utils.mjs quicktext-6.4.6/modules/utils.mjs
--- quicktext-6.3.2/modules/utils.mjs	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/modules/utils.mjs	2025-08-26 01:05:47.000000000 +0200
@@ -4,6 +4,19 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  */
 
+export async function registerExternalScriptAddon(id, name, scripts) {
+    let externalScripts = await browser.storage.session
+        .get({ externalScripts: [] })
+        .then(rv => rv.externalScripts.filter(e => e.id != id));
+    externalScripts.push({
+        id,
+        name,
+        scripts,
+    });
+    await browser.storage.session.set({ externalScripts });
+    console.log("registered external scripts", externalScripts)
+}
+
 export function getDateTimeFormat(format, timeStamp) {
     let options = {};
     options["date-short"] = { dateStyle: "short" };
@@ -27,17 +40,36 @@
     }
 }
 
-export function replaceText(tag, value, text) {
-    var replaceRegExp;
-    if (value != "")
-        replaceRegExp = new RegExp(escapeRegExp(tag), 'g');
-    else
-        replaceRegExp = new RegExp("( )?" + escapeRegExp(tag), 'g');
-    return text.replace(replaceRegExp, value);
+export function replaceText(tag, value, text, { collapseLineBreaks }) {
+    const escapedTag = escapeRegExp(tag);
+
+    if (value != "") {
+        return text.replace(new RegExp(escapedTag, 'g'), value);
+    }
+
+    // If value is "", we collapse a leading spaces and optionally linebreaks. Do not use global mode
+    // here, but force this function to be called on each tag (even if used multiple times), so the
+    // fallback regexp can cleanup a line until it is matching the single tag regexp and correctly
+    // removes the entire line.
+    if (collapseLineBreaks) {
+        // Match lines with a single empty tag and optional whitespaces.
+        const singleTagRegExp = new RegExp(`(^|\\r?\\n)( )*${escapedTag}( )*(\\r?\\n|$)`, 'm');
+        const collapsed = text.replace(singleTagRegExp, (match, leadingLB, leadingWSP, trailingWSP, trailingLB, offset, fullText) => {
+            // leadingLB and trailingLB are either "" or a line break. If we're matching two
+            // line breaks (one before, one after), preserve one.
+            return leadingLB && trailingLB ? leadingLB : "";
+        });
+        if (collapsed !== text) {
+            return collapsed;
+        }
+    }
+
+    // Match empty tags anywhere with optional single space before the tag.
+    return text.replace(new RegExp(`( )?${escapedTag}`, ''), value);
 }
 
 function escapeRegExp(aStr) {
-    return aStr.replace(/([\^\$\_\.\\\[\]\(\)\|\+\?])/g, "\\$1");
+    return aStr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
 }
 
 export function removeBadHTML(aStr) {
@@ -96,14 +128,28 @@
     const url = URL.createObjectURL(blob);
 
     try {
-        await browser.downloads.download({
+        let id = null;
+        const { promise, resolve } = Promise.withResolvers();
+
+        const listener = delta => {
+            if (id == delta.id && delta.state?.current === 'complete') {
+                browser.downloads.onChanged.removeListener(listener);
+                resolve();
+            }
+        }
+
+        browser.downloads.onChanged.addListener(listener);
+        id = await browser.downloads.download({
             url,
             filename,
             saveAs: true,
         });
+
+        await promise;
     } catch (error) {
         console.error("Error downloading the file:", error);
     }
+
     URL.revokeObjectURL(url);
 }
 
@@ -120,9 +166,11 @@
     for (let aType of aTypes) {
         switch (aType) {
             case 0: // TXT files
+            case "text/plain":
                 acceptedFileTypes.push("text/plain");
                 break;
             case 1: // HTML files
+            case "text/html":
                 acceptedFileTypes.push("text/html");
                 break;
             case 2: // arbitrary files
@@ -163,7 +211,21 @@
     return content;
 }
 
-export async function fetchFileFromServer(url) {
+export async function fetchFileAsFile(url, name) {
+    const response = await fetch(url);
+
+    if (!response.ok) {
+        throw new Error(`Failed to fetch file: ${response.status} ${response.statusText}`);
+    }
+
+    const blob = await response.blob();
+    const filename = name ?? getLeafName(url);
+    const contentType = blob.type || getTypeFromExtension(filename)
+
+    return new File([blob], filename, { type: contentType });
+}
+
+export async function fetchFileAsText(url) {
     try {
         const response = await fetch(url);
         if (response?.ok) {
@@ -175,6 +237,22 @@
     }
 }
 
+export async function fetchFileAsDataUrl(url) {
+    const response = await fetch(url);
+    if (!response.ok) {
+        throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
+    }
+
+    const blob = await response.blob();
+
+    return new Promise((resolve, reject) => {
+        const reader = new FileReader();
+        reader.onloadend = () => resolve(reader.result);
+        reader.onerror = reject;
+        reader.readAsDataURL(blob);
+    });
+}
+
 export async function openPopup(tabId, config) {
     let status = "none";
     let popup = Promise.withResolvers();
@@ -213,7 +291,7 @@
                 focused: true
             });
         }
-        
+
         // GOAL: We want to allow switching away from the popup to a different
         // window, but if the parent is focused, bring us back in front.
         if (windowId == parentId) {
@@ -222,7 +300,7 @@
                 ...dimension(await browser.windows.get(parentId))
             });
         }
-        
+
     };
     const onMessageListener = (info, sender, sendResponse) => {
         if (sender.tab.windowId != popupId) {
@@ -271,4 +349,164 @@
     });
 
     return rv;
+}
+
+// Keep this function async, so it can be used in then-chaining.
+export async function removeProtectedTemplates(templates) {
+    if (!templates) {
+        return null;
+    }
+    const protectedTemplatesIndices = new Set(
+        templates.groups.reduce((indices, value, index) => {
+            if (value.protected) {
+                indices.push(index);
+            }
+            return indices;
+        }, [])
+    );
+    if (protectedTemplatesIndices.size > 0) {
+        templates = {
+            groups: templates.groups.filter((_, index) => !protectedTemplatesIndices.has(index)),
+            texts: templates.texts.filter((_, index) => !protectedTemplatesIndices.has(index))
+        }
+    }
+    return templates;
+}
+
+// Keep this function async, so it can be used in then-chaining.
+export async function removeProtectedScripts(scripts) {
+    if (!scripts) {
+        return null;
+    }
+    const protectedScriptsIndices = new Set(
+        scripts.reduce((indices, value, index) => {
+            if (value.protected) {
+                indices.push(index);
+            }
+            return indices;
+        }, [])
+    );
+    if (protectedScriptsIndices.size > 0) {
+        scripts = scripts.filter((_, index) => !protectedScriptsIndices.has(index));
+    }
+    return scripts;
+}
+
+export async function checkBadNameEntries(templates, scripts) {
+    const badSubstrings = ["|", "[[", "]]"];
+    let badEntries = 0;
+
+    if (templates?.groups) {
+        badEntries += templates.groups.filter(e => badSubstrings.some(sub => e.name.includes(sub))).length;
+    }
+    if (templates?.texts) {
+        badEntries += templates.texts.flat().filter(e => badSubstrings.some(sub => e.name.includes(sub))).length;
+    }
+    if (scripts) {
+        badEntries += scripts.filter(e => badSubstrings.some(sub => e.name.includes(sub))).length
+    }
+    if (badEntries > 0) {
+        browser.notifications.create("qt-bad-entries", {
+            type: "basic",
+            title: "Quicktext v6",
+            message: `Some of your template, group or script names include one or more forbidden chars ("|", "[[" or "]]"). These entries will not work.`,
+        });
+    }
+}
+
+const createNotification = async message => {
+    await browser.notifications.create(
+        "qt-duplicated-entries", {
+        type: "basic",
+        title: "Quicktext v6",
+        message
+    });
+    console.warn(`[Quicktext v6] ${message}`)
+}
+
+export async function checkDuplicatedEntries(templates, scripts) {
+    const findDuplicates = array => {
+        const seen = new Set();
+        const duplicates = new Set();
+        for (const item of array) {
+            if (seen.has(item)) {
+                duplicates.add(item);
+            } else {
+                seen.add(item);
+            }
+        }
+        return [...duplicates];
+    }
+
+
+    const scriptNames = Array.isArray(scripts)
+        ? scripts.map(e => e.name.trim())
+        : []
+    const duplicatedScriptNames = findDuplicates(scriptNames);
+    if (duplicatedScriptNames.length) {
+        await createNotification(
+            `Invalid script data, multiple scripts with the same name: ${duplicatedScriptNames.join(", ")}`
+        );
+    }
+
+    const groupNames = Array.isArray(templates?.groups)
+        ? templates.groups.map(e => e.name.trim())
+        : []
+    const duplicatedGroupNames = findDuplicates(groupNames);
+    if (duplicatedGroupNames.length) {
+        await createNotification(
+            `Invalid template data, multiple groups with the same name: ${duplicatedGroupNames.join(", ")}`
+        );
+    }
+
+    if (Array.isArray(templates?.texts)) {
+        if (templates.texts.length != groupNames.length) {
+            await createNotification(
+                `Invalid template data, number of groups does not match number of template groups.`
+            );
+        }
+        for (let i = 0; i < templates.texts.length; i++) {
+            const textNames = templates.texts[i].map(e => e.name.trim());
+            const duplicatedNames = findDuplicates(textNames);
+            if (duplicatedNames.length) {
+                await createNotification(
+                    `Invalid template data, multiple templates in group "${groupNames[i]}" with the same name: ${duplicatedNames.join(", ")}`
+                )
+            }
+        }
+    }
+}
+
+export async function checkForIncompatibleScripts(scripts) {
+    const targets = ["this.mWindow", "this.mVariables", "this.mQuicktext"];
+    const incompatibleScripts = scripts.filter(s =>
+        targets.some(target => s.script.includes(target))
+    );
+    if (incompatibleScripts.length > 0) {
+        browser.notifications.create("qt-incompatible-scripts", {
+            type: "basic",
+            title: "Quicktext v6 - Incompatible Scripts!",
+            message: `Some of your scripts (for example ${incompatibleScripts.map(s => `'${s.name}'`).slice(0, 2).join(" and ")}) are incompatible with Quicktext v6. Click for more details.`,
+        });
+
+    }
+}
+
+export async function checkForDeprecatedAttachmentUsage(templates) {
+    const groupNames = Array.isArray(templates?.groups)
+        ? templates.groups.map(e => e.name.trim())
+        : []
+
+    if (templates?.texts) {
+        for (let i = 0; i < templates.texts.length; i++) {
+            const badEntries = templates.texts[i].filter(e => e.attachments).map(
+                e => e.name.trim()
+            );
+            if (badEntries.length) {
+                await createNotification(
+                    `Some of your templates in group "${groupNames[i]}" use the deprecated attachments field instead of the ATTACHMENT tag: ${badEntries.join(", ")}`
+                );
+            }
+        }
+    }
 }
\ Kein Zeilenumbruch am Dateiende.
diff -Nru quicktext-6.3.2/scripts/background.js quicktext-6.4.6/scripts/background.js
--- quicktext-6.3.2/scripts/background.js	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/scripts/background.js	2025-08-26 01:05:47.000000000 +0200
@@ -4,50 +4,64 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  */
 
+import * as quicktext from "../modules/quicktext.mjs";
+import * as storage from "../modules/storage.mjs";
+import * as menus from "../modules/menus.mjs";
+import * as utils from "../modules/utils.mjs";
+
 browser.runtime.onInstalled.addListener(details => {
+  let manifest = browser.runtime.getManifest();
+  if (!manifest.browser_specific_settings.gecko.update_url) {
+    return
+  };
+
   if (details.reason == "update") {
-    browser.notifications.create("qtupdate", {
+    browser.notifications.create("qt-update", {
       type: "basic",
-      title: "Quicktext",
-      message: `Quicktext pre-release was updated to v${browser.runtime.getManifest().version}\n(new script engine, click for details)`,
+      title: "Quicktext v6",
+      message: `Quicktext GitHub Edition was updated to v${manifest.version}. Click for details.`,
     });
   }
 });
 
+browser.runtime.onMessageExternal.addListener(({ register_script_addon, available_scripts }, { id }) => {
+  if (register_script_addon && available_scripts && available_scripts.length > 0) {
+    return utils.registerExternalScriptAddon(id, register_script_addon, available_scripts);
+  }
+  return false;
+});
+
 browser.notifications.onClicked.addListener(notificationId => {
-  if (notificationId != "qtupdate") {
-    return;
+  switch (notificationId) {
+    case "qt-deprecate-default-file-import":
+      browser.tabs.create({
+        url: `https://github.com/jobisoft/quicktext/wiki/Centrally-manage-configurations-and-templates`,
+      });
+      break;
+    case "qt-update":
+      browser.tabs.create({
+        url: `https://github.com/jobisoft/quicktext/releases/tag/v${browser.runtime.getManifest().version}`,
+      });
+      break;
+    case "qt-bad-entries":
+      browser.Quicktext.openTemplateManager();
+      break;
+    case "qt-incompatible-scripts":
+      browser.tabs.create({
+        url: `https://github.com/jobisoft/quicktext/issues/451`,
+      });
+      break;
   }
-  browser.tabs.create({
-    url: `https://github.com/jobisoft/quicktext/releases/tag/v${browser.runtime.getManifest().version}`,
-  })
 })
 
-import * as quicktext from "../modules/quicktext.mjs";
-import * as storage from "../modules/storage.mjs";
-import * as menus from "../modules/menus.mjs";
-import * as utils from "../modules/utils.mjs";
-
 // Legacy: Register global urls.
 await browser.LegacyHelper.registerGlobalUrls([
   ["content", "quicktext", "xul_settings_dialog/"],
   ["resource", "quicktext", "."],
 ]);
 
-// Define default prefs.
-let defaultPrefs = {
-  "counter": 0,
-  "templateFolder": "",
-  "defaultImport": "",
-  "menuCollapse": true,
-  "toolbar": true,
-  "popup": true,
-  "keywordKey": "Tab",
-  "shortcutModifier": "alt",
-  "shortcutTypeAdv": false,
-  "collapseState": ""
-};
-await storage.init(defaultPrefs);
+// Over the years, the storage concept has changed.
+await storage.migrate();
 
 // Fix invalid options:
 // - reset the value of shortcutModifier to "alt", if it has not a valid value - see issue #177
@@ -56,27 +70,6 @@
   await storage.setPref("shortcutModifier", "alt");
 }
 
-// Define prefs, which can be overridden by system admins. Admins have to migrate
-// these manually from legacy prefs to managed storage.
-const managedPrefs = [
-  "defaultImport",
-];
-
-let managedStorageAvailable = true;
-for (let managedPref of managedPrefs) {
-  try {
-    let override = await browser.storage.managed.get({ [managedPref]: null });
-    if (override[managedPref] !== null) {
-      await storage.setPref(managedPref, override[managedPref]);
-    }
-    await storage.setPref(`${managedPref}.managed`, true);
-  } catch {
-    // No managed storage available.
-    await storage.setPref(`${managedPref}.managed`, false);
-    managedStorageAvailable = false;
-  }
-}
-
 // Legacy: The XML files will be kept for backup, but are read only if they have
 //         not already been migrated to local storage. Uninstalling Quicktext (which
 //         clears the storage) and installing it again, will re-import the XML files.
@@ -107,27 +100,55 @@
   await storage.setScripts(scripts);
 }
 
+// Remove managed templates.
+let cleanedTemplates = await utils.removeProtectedTemplates(templates);
+if (templates != cleanedTemplates) {
+  templates = cleanedTemplates;
+  await storage.setTemplates(templates);
+}
+
+// Remove managed scripts.
+let cleanedScripts = await utils.removeProtectedScripts(scripts);
+if (scripts != cleanedScripts) {
+  scripts = cleanedScripts;
+  await storage.setScripts(scripts);
+}
+
 // Startup import.
-const defaultImport = await storage.getPref("defaultImport");
-if (defaultImport) {
-  const defaultImports = defaultImport.split(";").map(e => e.trim()).reverse();
-  for (let path of defaultImports) {
-    try {
-      // Import XML or JSON config data from remote server or local file system.
-      // Support for importing from the local file system will be removed for the
-      // pure WebExtension version. Use managed storage instead.
-      const data = path.match(/^(http|https):\/\//)
-        ? await utils.fetchFileFromServer(path)
-        : await browser.Quicktext.readTextFile(path);
-      const imports = quicktext.parseConfigFileData(data);
-      if (imports.templates) {
-        quicktext.mergeTemplates(templates, imports.templates, true);
-      }
-      if (imports.scripts) {
-        quicktext.mergeScripts(scripts, imports.scripts, true);
+let defaultImports = JSON.parse(await storage.getPref("defaultImport"));;
+if (Array.isArray(defaultImports) && defaultImports.length > 0) {
+  for (let defaultImportEntry of defaultImports) {
+    let data;
+    switch (defaultImportEntry.source.toLowerCase()) {
+      case "file":
+        try {
+          // Import XML or JSON config data from the local file system.
+          data = await browser.Quicktext.readTextFile(defaultImportEntry.data);
+        } catch (ex) {
+          console.error("Failed to read file", ex);
+        }
+        break;
+      case "url":
+        try {
+          // Import XML or JSON config data from remote server.
+          data = await utils.fetchFileAsText(defaultImportEntry.data);
+        } catch (ex) {
+          console.error("Failed to read url", ex);
+        }
+        break;
+    }
+    if (data) {
+      try {
+        const imports = await quicktext.parseConfigFileData(data);
+        if (imports.templates) {
+          quicktext.mergeTemplates(templates, imports.templates, true);
+        }
+        if (imports.scripts) {
+          quicktext.mergeScripts(scripts, imports.scripts, true);
+        }
+      } catch (ex) {
+        console.error("Failed to parse data", ex);
       }
-    } catch (e) {
-      console.error(e);
     }
   }
   await storage.setTemplates(templates);
@@ -135,7 +156,7 @@
 }
 
 // Startup import via managed storage.
-if (managedStorageAvailable) {
+try {
   let { templates: managedTemplates } = await browser.storage.managed.get({ templates: null });
   if (managedTemplates) {
     quicktext.mergeTemplates(templates, managedTemplates, true);
@@ -144,16 +165,23 @@
   if (managedScripts) {
     quicktext.mergeScripts(scripts, managedScripts, true);
   }
+  await storage.setTemplates(templates);
+  await storage.setScripts(scripts);
+} catch {
+  // No managed storage.
 }
 
 // NotifyTools needed by Experiment code to access WebExtension code.
 messenger.NotifyTools.onNotifyBackground.addListener(async (info) => {
   switch (info.command) {
+    case "reload":
+      browser.runtime.reload();
     case "setPref":
       return storage.setPref(info.pref, info.value);
     case "getPref":
       return storage.getPref(info.pref);
-
+    case "getPrefWithManagedInfo":
+      return storage.getPrefWithManagedInfo(info.pref);
     case "setScripts":
       return storage.setScripts(info.data);
     case "getScripts":
@@ -163,12 +191,23 @@
       return storage.setTemplates(info.data);
     case "getTemplates":
       return storage.getTemplates();
-
+    case "checkBadEntries":
+      await utils.checkBadNameEntries(info.data.templates, info.data.scripts);
+      return utils.checkDuplicatedEntries(info.data.templates, info.data.scripts);
     case "openWebPage":
       return browser.windows.openDefaultBrowser(info.url);
 
-    case "parseConfigFile":
-      return browser.Quicktext.readTextFile(info.path).then(quicktext.parseConfigFileData);
+    case "getDateTimeFormat":
+      return utils.getDateTimeFormat(info.data.format, info.data.timeStamp);
+
+    case "parseTemplateFileForImport":
+      return browser.Quicktext.readTextFile(info.path)
+        .then(quicktext.parseConfigFileData)
+        .then(parsedData => utils.removeProtectedTemplates(parsedData?.templates))
+    case "parseScriptFileForImport":
+      return browser.Quicktext.readTextFile(info.path)
+        .then(quicktext.parseConfigFileData)
+        .then(parsedData => utils.removeProtectedScripts(parsedData?.scripts))
     case "pickAndParseConfigFile":
       // Currently not used. Instead the settings window keeps using the legacy
       // file picker.
@@ -300,4 +339,10 @@
       windows.forEach(window => browser.QuicktextToolbar.updateLegacyToolbar(window.id));
     }
   }
-)
\ Kein Zeilenumbruch am Dateiende.
+)
+
+// Check if templates or scripts are invalid.
+await utils.checkBadNameEntries(templates, scripts);
+await utils.checkDuplicatedEntries(templates, scripts);
+await utils.checkForIncompatibleScripts(scripts);
+await utils.checkForDeprecatedAttachmentUsage(templates);
diff -Nru quicktext-6.3.2/scripts/compose.js quicktext-6.4.6/scripts/compose.js
--- quicktext-6.3.2/scripts/compose.js	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/scripts/compose.js	2025-08-26 01:05:47.000000000 +0200
@@ -91,8 +91,10 @@
                     // spaces which selection.deleteFromDocument() does. All the
                     // text altering functions from selection and range seem to
                     // collapse spaces.
+
                     document.execCommand('delete');
                     //selection.deleteFromDocument();
+                    //selection.getRangeAt(0).deleteContents();
                 }
                 startPos = foundElement.nodeValue.indexOf(CURSOR);
             } while (startPos != -1)
@@ -121,8 +123,8 @@
     )
 }
 
-function isRealKey(e) {
-    return e.key.length == 1;
+function isRealNumberKey(e) {
+    return e.key.length == 1 &&  /^[0-9]$/i.test(e.key);
 }
 
 function keywordListener(e) {
@@ -132,49 +134,41 @@
             return;
         }
 
-        // This gives us a range object of the currently selected text
-        // and as the user usually does not have any text selected when
-        // triggering keywords, it is a collapsed range at the current
-        // cursor position.
+        // This gives us a range object of the currently selected text.
         let initialSelectionRange = selection.getRangeAt(0).cloneRange();
 
-        // Get a temp selection, which we can modify to search for the beginning
-        // of the last word.
-        let tmpRange = initialSelectionRange.cloneRange();
-        tmpRange.collapse(false);
-        selection.removeAllRanges();
-        selection.addRange(tmpRange);
-
-        // Extend selection to the beginning of the current word.
-        selection.modify("extend", "backward", "word");
+        // Get the text from the beginning of the current node to the end of the
+        // selection/cursor. We assume the keyword is not split between two nodes.
+        let range = initialSelectionRange.cloneRange();
+        range.setStart(range.startContainer, 0);
+        let lastWord = range.toString().split(" ").pop();
 
-        // We should only have one word selected, but make sure to only get the
-        // last one by chopping up its content.
-        let lastWord = selection.toString().split(" ").pop().toLowerCase();
-        if (!lastWord) {
-            // Restore to the initialSelectionRange and abort.
-            selection.removeAllRanges();
-            selection.addRange(initialSelectionRange);
+        if (!lastWord || !keywords.hasOwnProperty(lastWord)) {
             return;
         }
 
-        let lastWordIsKeyword = keywords.hasOwnProperty(lastWord);
-        if (!lastWordIsKeyword) {
-            // Restore to the initialSelectionRange and abort.
-            selection.removeAllRanges();
-            selection.addRange(initialSelectionRange);
-            return;
-        }
-
-        // So this is it. Eat the keypress, remove the keyword from the document
-        // and insert the template.
+        // We found a valid keyword, eat the keypress.
         e.stopPropagation();
         e.preventDefault();
 
+        // Extend selection from the end of the current selection/cursor to the
+        // beginning of the current word.
+        selection.collapseToEnd();
+        selection.modify("extend", "backward", "word");
+        // Verify that the entire lastWord is selected, and extend the selection
+        // if needed. This is needed since #hi is not fully extended as # is not
+        // considered to be part of the word.
+        while (selection.toString().length < lastWord.length) {
+            selection.modify("extend", "backward", "character");
+        }
+
         // The following line will remove the keyword before we replace it. If we
         // do not do that, we see the keyword being selected and then replaced.
         // It does look interesting, but I keep it as it was before.
-        selection.deleteFromDocument()
+
+        document.execCommand('delete');
+        //selection.deleteFromDocument();
+        //selection.getRangeAt(0).deleteContents();
         requestInsertTemplate(keywords[lastWord])
     }
 }
@@ -186,13 +180,13 @@
 
     if (shortcutTypeAdv) {
         advShortcutModifierIsDown = true;
-        if (isRealKey(e)) {
+        if (isRealNumberKey(e)) {
             advShortcutString += e.key;
+            // Eat keys if we acted upon them.
+            e.stopPropagation();
+            e.preventDefault();
         }
-        // Eat keys if we acted upon them.
-        e.stopPropagation();
-        e.preventDefault();
-    } else if (isRealKey(e) && shortcuts[e.key] && !e.repeat) {
+    } else if (isRealNumberKey(e) && shortcuts[e.key] && !e.repeat) {
         requestInsertTemplate(shortcuts[e.key]);
         // Eat keys if we acted upon them.
         e.stopPropagation();
diff -Nru quicktext-6.3.2/updates.json quicktext-6.4.6/updates.json
--- quicktext-6.3.2/updates.json	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/updates.json	2025-08-26 01:05:47.000000000 +0200
@@ -3,9 +3,9 @@
     "{8845E3B3-E8FB-40E2-95E9-EC40294818C4}": {
       "updates": [
         {
-          "version": "6.3.1",
-          "update_info_url": "https://github.com/jobisoft/quicktext/issues/439",
-          "update_link": "https://github.com/jobisoft/quicktext/releases/download/v6.3.1/quicktext_6_3_1.xpi",
+          "version": "6.4.5",
+          "update_info_url": "https://github.com/jobisoft/quicktext/releases",
+          "update_link": "https://github.com/jobisoft/quicktext/releases/download/v6.4.5/quicktext_6_4_5.xpi",
           "applications": {
             "gecko": {
               "strict_min_version": "128.0"
diff -Nru quicktext-6.3.2/xul_settings_dialog/settings.css quicktext-6.4.6/xul_settings_dialog/settings.css
--- quicktext-6.3.2/xul_settings_dialog/settings.css	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/xul_settings_dialog/settings.css	2025-08-26 01:05:47.000000000 +0200
@@ -5,12 +5,12 @@
 dialog[OS=WINNT] legend.insideTab {
   padding-right: 5px;
   padding-left: 5px;
-  margin-bottom:5px;
+  margin-bottom: 5px;
   background-color: -moz-Field !important;
 }
 
 .textarea-container {
-  margin:4px;
+  margin: 4px;
   overflow: hidden;
 }
 
@@ -19,6 +19,31 @@
   resize: none;
 }
 
-#text-defaultImport:disabled {
-  background-color: #ccc;
+#script-list {
+  overflow-y: auto;
+  height: 100%;
+  width: 100%
+}
+
+#scripthelpbutton {
+  display: none;
+}
+
+#scripthelpbutton[data-selected-tab-index="2"][data-is-incompatible="true"] {
+  display: block;
+}
+
+#box-defaultImport {
+  overflow-y: auto;
+  height: 120px;
+  width: 100%
+}
+
+#box-storageLocations {
+  overflow-y: auto;
+  height: 120px;
+}
+
+#box-storageLocations richlistitem[data-active] {
+  background-color: skyblue
 }
\ Kein Zeilenumbruch am Dateiende.
diff -Nru quicktext-6.3.2/xul_settings_dialog/settings.js quicktext-6.4.6/xul_settings_dialog/settings.js
--- quicktext-6.3.2/xul_settings_dialog/settings.js	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/xul_settings_dialog/settings.js	2025-08-26 01:05:47.000000000 +0200
@@ -19,6 +19,12 @@
 
 Services.scriptloader.loadSubScript("resource://quicktext/api/NotifyTools/notifyTools.js", window, "UTF-8");
 
+
+function isIncompatibleScript(script) {
+  const targets = ["this.mWindow", "this.mVariables", "this.mQuicktext"];
+  return script && targets.some(target => script.script.includes(target));
+}
+
 // This exists for historic reasons, but all of it is related to the settings
 // dialog as well.
 var gQuicktext = {
@@ -43,8 +49,6 @@
   get viewToolbar() { return this.mViewToolbar; },
   set viewToolbar(aViewToolbar) {
     this.mViewToolbar = aViewToolbar;
-
-    notifyTools.notifyBackground({ command: "setPref", pref: "toolbar", value: aViewToolbar });
     this.notifyObservers("updatetoolbar", "");
 
     return this.mViewToolbar;
@@ -52,15 +56,11 @@
   get viewPopup() { return this.mViewPopup; },
   set viewPopup(aViewPopup) {
     this.mViewPopup = aViewPopup;
-    notifyTools.notifyBackground({ command: "setPref", pref: "popup", value: aViewPopup });
-
     return this.mViewPopup;
   },
   get collapseGroup() { return this.mCollapseGroup; },
   set collapseGroup(aCollapseGroup) {
     this.mCollapseGroup = aCollapseGroup;
-    notifyTools.notifyBackground({ command: "setPref", pref: "menuCollapse", value: aCollapseGroup });
-
     this.notifyObservers("updatesettings", "");
 
     return this.mCollapseGroup;
@@ -68,29 +68,26 @@
   get defaultImport() { return this.mDefaultImport; },
   set defaultImport(aDefaultImport) {
     this.mDefaultImport = aDefaultImport;
-    notifyTools.notifyBackground({ command: "setPref", pref: "defaultImport", value: aDefaultImport });
-
     return this.mDefaultImport;
   },
+  get storageLocations() { return this.mStorageLocations; },
+  set storageLocations(aStorageLocations) {
+    this.mStorageLocations = aStorageLocations;
+    return this.mStorageLocations;
+  },
   get keywordKey() { return this.mKeywordKey; },
   set keywordKey(aKeywordKey) {
     this.mKeywordKey = aKeywordKey;
-    notifyTools.notifyBackground({ command: "setPref", pref: "keywordKey", value: aKeywordKey });
-
     return this.mKeywordKey;
   },
   get shortcutModifier() { return this.mShortcutModifier; },
   set shortcutModifier(aShortcutModifier) {
     this.mShortcutModifier = aShortcutModifier;
-    notifyTools.notifyBackground({ command: "setPref", pref: "shortcutModifier", value: aShortcutModifier });
-
     return this.mShortcutModifier;
   },
   get collapseState() { return this.mCollapseState; },
   set collapseState(aCollapseState) {
     this.mCollapseState = aCollapseState;
-    notifyTools.notifyBackground({ command: "setPref", pref: "collapseState", value: aCollapseState });
-
     return this.mCollapseState;
   },
   get shortcutTypeAdv() {
@@ -101,8 +98,6 @@
   },
   set shortcutTypeAdv(aShortcutTypeAdv) {
     this.mShortcutTypeAdv = aShortcutTypeAdv;
-    notifyTools.notifyBackground({ command: "setPref", pref: "shortcutTypeAdv", value: aShortcutTypeAdv });
-
     return this.mShortcutTypeAdv;
   },
   loadSettings: async function () {
@@ -119,20 +114,33 @@
 
     // Get prefs
     this.mViewToolbar = await notifyTools.notifyBackground({ command: "getPref", pref: "toolbar" });
+    this.mCollapseState = await notifyTools.notifyBackground({ command: "getPref", pref: "collapseState" });
+
     this.mCollapseGroup = await notifyTools.notifyBackground({ command: "getPref", pref: "menuCollapse" });
-    this.mKeywordKey = await notifyTools.notifyBackground({ command: "getPref", pref: "keywordKey" });
     this.mViewPopup = await notifyTools.notifyBackground({ command: "getPref", pref: "popup" });
+    this.mKeywordKey = await notifyTools.notifyBackground({ command: "getPref", pref: "keywordKey" });
     this.mShortcutTypeAdv = await notifyTools.notifyBackground({ command: "getPref", pref: "shortcutTypeAdv" });
     this.mShortcutModifier = await notifyTools.notifyBackground({ command: "getPref", pref: "shortcutModifier" });
-    this.mCollapseState = await notifyTools.notifyBackground({ command: "getPref", pref: "collapseState" });
     this.mDefaultImport = await notifyTools.notifyBackground({ command: "getPref", pref: "defaultImport" });
 
-    const defaultImportManaged = await notifyTools.notifyBackground({
-      command: "getPref",
-      pref: "defaultImport.managed"
-    });
-    if (defaultImportManaged) {
-      document.getElementById("text-defaultImport").setAttribute("disabled", "true");
+    // Managed prefs cannot be changed by the user.
+    for (let { pref, elemId, m } of [
+      { pref: "menuCollapse", elemId: "checkbox-collapseGroup", m: this.mCollapseGroup },
+      { pref: "popup", elemId: "checkbox-viewPopup", m: this.mViewPopup },
+      { pref: "keywordKey", elemId: "select-keywordKey", m: this.mKeywordKey },
+      { pref: "shortcutTypeAdv", elemId: "checkbox-shortcutTypeAdv", m: this.mShortcutTypeAdv },
+      { pref: "shortcutModifier", elemId: "select-shortcutModifier", m: this.mShortcutModifier },
+    ]) {
+      const { value, isManaged } = await notifyTools.notifyBackground({
+        command: "getPrefWithManagedInfo",
+        pref
+      });
+      m = value;
+      if (isManaged) {
+        const element = document.getElementById(elemId);
+        element.setAttribute("disabled", "true");
+        element.setAttribute('tooltiptext', extension.localeData.localizeMessage("controlled-via-managed-storage"));
+      }
     }
 
     this.startEditing();
@@ -177,11 +185,15 @@
     await notifyTools.notifyBackground({ command: "setPref", pref: "shortcutModifier", value: this.mShortcutModifier });
     await notifyTools.notifyBackground({ command: "setPref", pref: "collapseState", value: this.mCollapseState });
     await notifyTools.notifyBackground({ command: "setPref", pref: "defaultImport", value: this.mDefaultImport });
+    await notifyTools.notifyBackground({ command: "setPref", pref: "storageLocations", value: this.mStorageLocations });
 
     // Save templates and scripts.
     this.endEditing();
-    await notifyTools.notifyBackground({ command: "setScripts", data: this.prettify(this.mScripts) });
-    await notifyTools.notifyBackground({ command: "setTemplates", data: { texts: this.prettify(this.mTexts), groups: this.prettify(this.mGroup) } });
+    const templates = { texts: this.prettify(this.mTexts), groups: this.prettify(this.mGroup) }
+    const scripts = this.prettify(this.mScripts);
+    await notifyTools.notifyBackground({ command: "setScripts", data: scripts });
+    await notifyTools.notifyBackground({ command: "setTemplates", data: templates });
+    await notifyTools.notifyBackground({ command: "checkBadEntries", data: { scripts, templates } });
     this.startEditing();
 
     this.notifyObservers("updatesettings", "");
@@ -382,11 +394,14 @@
   },
 
   /*
-   * FILE FUNCTIONS
+   * FILE FUNCTIONS (will be replaced by picker from the planned vfs API)
    */
   async pickFile(aTypes, aMode, aTitle) {
     let filePicker = Components.classes["@mozilla.org/filepicker;1"].createInstance(Components.interfaces.nsIFilePicker);
     switch (aMode) {
+      case 2: // modeGetFolder
+        filePicker.init(window.browsingContext, aTitle, filePicker.modeGetFolder);
+        break;
       case 1: // save
         filePicker.init(window.browsingContext, aTitle, filePicker.modeSave);
         break;
@@ -475,6 +490,232 @@
   }
 }
 
+class SourceListBox {
+  #listBox
+  #loadCallback
+  #updateCallback
+  #selectCallback
+  #canRemoveCallback
+  #canSelectCallback
+  #buttonDefinitions
+
+  constructor({ listBoxId, loadCallback, updateCallback, selectCallback, canRemoveCallback, canSelectCallback, buttonDefinitions }) {
+    this.#listBox = document.getElementById(listBoxId);
+    this.#loadCallback = loadCallback;
+    this.#updateCallback = updateCallback;
+    this.#selectCallback = selectCallback;
+    this.#canRemoveCallback = canRemoveCallback;
+    this.#canSelectCallback = canSelectCallback;
+    this.#buttonDefinitions = buttonDefinitions;
+  }
+
+  get defaultUrlValue() {
+    return "https://";
+  }
+
+  get listBox() {
+    return this.#listBox;
+  }
+
+  async load() {
+    const {
+      entries,
+      activeIdx,
+    } = await this.#loadCallback();
+
+
+    for (let [elementId, definition] of Object.entries(this.#buttonDefinitions)) {
+      if (!definition.isManaged) {
+        document.getElementById(elementId).removeAttribute("disabled");
+      }
+      document.getElementById(elementId).addEventListener("command", () => this[definition.callback](), false);
+    }
+
+    for (let i = 0; i < entries.length; i++) {
+      const item = await this.addItem(entries[i]);
+      if (i == activeIdx) {
+        item.dataset.active = "true";
+        this.listBox.selectedIndex = i;
+      }
+    }
+    this.listBox.addEventListener("select", () => this.checkButtons());
+    this.checkButtons();
+  }
+
+  checkButtons() {
+    if (this.#canRemoveCallback) {
+      const canRemove = this.#canRemoveCallback(this.listBox.selectedIndex, this.activeIdx);
+      let btns = Object.entries(this.#buttonDefinitions).filter(e => e[1].callback == "removeItem");
+      for (let [btnId, definition] of btns) {
+        if (definition.isManaged) {
+          continue;
+        }
+
+        if (canRemove) {
+          document.getElementById(btnId).removeAttribute("disabled");
+        } else {
+          document.getElementById(btnId).setAttribute("disabled", "true");
+        }
+      }
+    }
+    if (this.#canSelectCallback) {
+      const canSelect = this.#canSelectCallback(this.listBox.selectedIndex, this.activeIdx);
+      let btns = Object.entries(this.#buttonDefinitions).filter(e => e[1].callback == "selectItem");
+      for (let [btnId, definition] of btns) {
+        if (definition.isManaged) {
+          continue;
+        }
+
+        if (canSelect) {
+          document.getElementById(btnId).removeAttribute("disabled");
+        } else {
+          document.getElementById(btnId).setAttribute("disabled", "true");
+        }
+      }
+    }
+  }
+
+  get activeIdx() {
+    const childrenArray = Array.from(this.listBox.children);
+    const activeIdx = childrenArray.findIndex(e => e.dataset.active === "true");
+    return activeIdx;
+  }
+
+  get value() {
+    let entries = [];
+    for (let child of this.listBox.children) {
+      entries.push({
+        source: child.dataset.source,
+        data: child.dataset.data,
+      })
+    }
+    return entries;
+  }
+
+  async update() {
+    if (this.#updateCallback) {
+      await this.#updateCallback(this.value, this.activeIdx);
+    }
+    this.checkButtons();
+  }
+
+  getLabel(entry) {
+    return entry.source.toLowerCase() == "internal"
+      ? extension.localeData.localizeMessage(`quicktext.storage.internal.${entry.data.toLowerCase()}.label`)
+      : entry.data
+  }
+
+  async addItem(entry) {
+    let newItem = document.createXULElement("richlistitem");
+    newItem.dataset.source = entry.source;
+    newItem.dataset.data = entry.data;
+
+    const ICONS = {
+      "url": "?",
+      "file": "?", // ?
+      "internal": "?", // ?,?
+    }
+
+    let newItemType = document.createXULElement("label");
+    newItemType.value = ICONS[entry.source.toLowerCase()] ?? "??";
+    newItemType.style.width = "16px";
+    newItemType.style.display = "block";
+    newItemType.style.textAlign = "center";
+    newItem.appendChild(newItemType);
+
+    let newItemLabel = document.createXULElement("label");
+    newItemLabel.value = this.getLabel(entry);
+    if (entry.source.toLowerCase() == "url") {
+      newItem.addEventListener("dblclick", () => {
+        let input = document.createElement("input");
+        input.value = newItemLabel.value;
+        input.dataset.originalValue = newItemLabel.value;
+        newItemLabel.parentNode.replaceChild(input, newItemLabel);
+        input.focus();
+
+        // commit value on Enter
+        const commit = (data) => {
+          newItemLabel.value = data;
+          input.parentNode.replaceChild(newItemLabel, input);
+          newItem.dataset.data = data;
+        };
+
+        input.addEventListener("blur", (e) => {
+          commit(input.value);
+          this.update();
+        });
+        input.addEventListener("keydown", (e) => {
+          if (e.key === "Enter") {
+            commit(input.value);
+            this.update();
+          }
+          if (e.key === "Escape") {
+            if (input.dataset.originalValue == this.defaultUrlValue) {
+              this.listBox.removeChild(newItem);
+            } else {
+              commit(input.dataset.originalValue);
+            }
+            this.update();
+          }
+        }, true);
+
+      });
+    }
+    newItem.appendChild(newItemLabel);
+
+    this.listBox.appendChild(newItem);
+    return newItem;
+  }
+
+  async addUrlItem() {
+    const item = await this.addItem({
+      source: "URL",
+      data: this.defaultUrlValue,
+    });
+    await this.update();
+    item.dispatchEvent(new MouseEvent("dblclick", {
+      bubbles: true,
+      cancelable: true,
+      view: window,
+      detail: 2
+    }));
+  }
+
+  async addFileItem() {
+    const file = await gQuicktext.pickFile([5, 3], 0, extension.localeData.localizeMessage("importFile"));
+    if (!file) return;
+    await this.addItem({
+      source: "FILE",
+      data: file.path,
+    });
+    await this.update();
+  }
+
+  async addFolderItem() {
+    const folder = await gQuicktext.pickFile([], 2, "select folder");
+    if (!folder) return;
+    await this.addItem({
+      source: "FILE",
+      data: folder.path,
+    });
+    await this.update();
+  }
+
+  async selectItem() {
+    if (this.#selectCallback) {
+      await this.#selectCallback(this.listBox.selectedIndex);
+    }
+  }
+
+  async removeItem() {
+    let item = this.listBox.getItemAtIndex(this.listBox.selectedIndex);
+    if (item) {
+      this.listBox.removeChild(item);
+    }
+    await this.update();
+  }
+}
+
 var settingsDialog = {
   mChangesMade: false,
   mTextChangesMade: [],
@@ -507,6 +748,9 @@
     }
 
     document.getElementById('tabbox-main').selectedIndex = 1;
+    document.getElementById('tabpanels-main').addEventListener("select", function (e) {
+      document.getElementById('scripthelpbutton').dataset.selectedTabIndex = document.getElementById('tabbox-main').selectedIndex;
+    }, false);
 
     document.getElementById('text-keyword').addEventListener("keypress", function (e) { settingsDialog.noSpaceForKeyword(e); }, false);
 
@@ -514,6 +758,116 @@
     document.getElementById("savebutton").addEventListener("command", function (e) { settingsDialog.save(); }, false);
     document.getElementById("closebutton").addEventListener("command", function (e) { settingsDialog.close(true); }, false);
     document.getElementById("helpbutton").addEventListener("command", function (e) { settingsDialog.openHomepage(); }, false);
+    document.getElementById("scripthelpbutton").addEventListener("command", function (e) { settingsDialog.openScriptHelp(); }, false);
+
+    // Update boxHeightOffset
+    let scriptListElem = document.getElementById('script-list');
+    let elementHeight = scriptListElem.getBoundingClientRect().height;
+    boxHeightOffset = window.innerHeight - elementHeight;
+
+    // Load defaultImport
+    {
+      const {
+        value: defaultImportEntries,
+        isManaged: defaultImportEntriesManaged,
+      } = await notifyTools.notifyBackground({
+        command: "getPrefWithManagedInfo",
+        pref: "defaultImport"
+      });
+
+      const defaultImportUI = new SourceListBox({
+        listBoxId: "box-defaultImport",
+        loadCallback: async () => {
+          document.getElementById("box-defaultImport").dataset.value = defaultImportEntries;
+          return {
+            entries: JSON.parse(defaultImportEntries),
+          }
+        },
+        updateCallback: (updatedEntries, activeIdx) => {
+          document.getElementById("box-defaultImport").dataset.value = JSON.stringify(updatedEntries);
+          settingsDialog.checkForGeneralChanges(5);
+        },
+        buttonDefinitions: {
+          "defaultImport_remove": { callback: "removeItem", isManaged: defaultImportEntriesManaged },
+          "defaultImport_addUrlItem": { callback: "addUrlItem", isManaged: defaultImportEntriesManaged },
+          "defaultImport_addFileItem": { callback: "addFileItem", isManaged: defaultImportEntriesManaged },
+        }
+      })
+      await defaultImportUI.load();
+    }
+
+    // Storage location
+    {
+      const {
+        value: storageLocationEntries,
+        isManaged: storageLocationsManaged,
+      } = await notifyTools.notifyBackground({
+        command: "getPrefWithManagedInfo",
+        pref: "storageLocations"
+      });
+      const {
+        value: activeStorageLocationIdx,
+        isManaged: activeStorageLocationIdxManaged,
+      } = await notifyTools.notifyBackground({
+        command: "getPrefWithManagedInfo",
+        pref: "activeStorageLocationIdx"
+      });
+
+      const storageLocationsUI = new SourceListBox({
+        listBoxId: "box-storageLocations",
+        loadCallback: async () => {
+          const entries = JSON.parse(storageLocationEntries);
+          if (!(
+            entries.some(e => e.source.toLowerCase() == "internal") &&
+            entries.some(e => e.data.toLowerCase() == "local")
+          )) {
+            entries.unshift({
+              source: "INTERNAL",
+              data: "local",
+            })
+          }
+          document.getElementById("box-storageLocations").dataset.value = storageLocationEntries;
+          document.getElementById("box-storageLocations").dataset.activeIdx = activeStorageLocationIdx;
+          return {
+            entries,
+            activeIdx: activeStorageLocationIdx,
+          }
+        },
+        updateCallback: async (updatedEntries, activeIdx) => {
+          document.getElementById("box-storageLocations").dataset.value = JSON.stringify(updatedEntries);
+          document.getElementById("box-storageLocations").dataset.activeIdx = activeIdx;
+          settingsDialog.checkForGeneralChanges(6);
+        },
+        selectCallback: async (idx) => {
+          // Save the current list of storage entries.
+          await notifyTools.notifyBackground({
+            command: "setPref",
+            pref: "storageLocations",
+            value: document.getElementById("box-storageLocations").dataset.value
+          });
+          await notifyTools.notifyBackground({
+            command: "setPref",
+            pref: "activeStorageLocationIdx",
+            value: idx
+          });
+          notifyTools.notifyBackground({
+            command: "reload",
+          });
+        },
+        canRemoveCallback: (idx, activeIdx) => {
+          return idx > 0 && idx != activeIdx
+        },
+        canSelectCallback: (idx, activeIdx) => {
+          return idx > -1 && idx != activeIdx
+        },
+        buttonDefinitions: {
+          "storageLocations_remove": { callback: "removeItem", isManaged: storageLocationsManaged || activeStorageLocationIdxManaged},
+          "storageLocations_addFolderItem": { callback: "addFolderItem", isManaged: storageLocationsManaged || activeStorageLocationIdxManaged},
+          "storageLocations_select": { callback: "selectItem", isManaged: activeStorageLocationIdxManaged },
+        }
+      })
+      await storageLocationsUI.load();
+    }
   },
   unload: function () {
     gQuicktext.removeObserver(this);
@@ -529,7 +883,7 @@
     this.saveText();
     this.saveScript();
 
-    if (this.mChangesMade) {
+    if (this.anyChangesMade()) {
       promptService = Services.prompt;
       if (promptService) {
         result = promptService.confirmEx(window,
@@ -566,8 +920,10 @@
 
     if (document.getElementById("checkbox-viewPopup"))
       gQuicktext.viewPopup = document.getElementById("checkbox-viewPopup").checked;
-    if (document.getElementById("text-defaultImport"))
-      gQuicktext.defaultImport = document.getElementById("text-defaultImport").value;
+    if (document.getElementById("box-defaultImport"))
+      gQuicktext.defaultImport = document.getElementById("box-defaultImport").dataset.value;
+    if (document.getElementById("box-storageLocations"))
+      gQuicktext.storageLocations = document.getElementById("box-storageLocations").dataset.value;
     if (document.getElementById("select-shortcutModifier"))
       gQuicktext.shortcutModifier = document.getElementById("select-shortcutModifier").value;
     if (document.getElementById("checkbox-shortcutTypeAdv"))
@@ -584,16 +940,16 @@
     this.mScriptChangesMade = [];
     this.mGeneralChangesMade = [];
     this.disableSave();
-    this.updateGUI();
+    await this.updateGUI();
   },
   saveText: function () {
     if (this.mPickedIndex != null) {
       if (this.mPickedIndex[1] > -1) {
-        var title = document.getElementById('text-title').value;
-        if (title.replace(/[\s]/g, '') == "")
-          title = extension.localeData.localizeMessage("newTemplate");
-
-        this.saveTextCell(this.mPickedIndex[0], this.mPickedIndex[1], 'name', title);
+        // The title is updated in the onchange event.
+        //var title = document.getElementById('text-title').value;
+        //if (title.replace(/[\s]/g, '') == "")
+        //  title = extension.localeData.localizeMessage("newTemplate");
+        //this.saveTextCell(this.mPickedIndex[0], this.mPickedIndex[1], 'name', title);
         this.saveTextCell(this.mPickedIndex[0], this.mPickedIndex[1], 'text', document.getElementById('text').value);
 
         if (gQuicktext.shortcutTypeAdv)
@@ -607,11 +963,11 @@
         this.saveTextCell(this.mPickedIndex[0], this.mPickedIndex[1], 'attachments', document.getElementById('text-attachments').value);
       }
       else {
-        var title = document.getElementById('text-title').value;
-        if (title.replace(/[\s]/g, '') == "")
-          title = extension.localeData.localizeMessage("newGroup");
-
-        this.saveGroupCell(this.mPickedIndex[0], 'name', title);
+        // The title is updated in the onchange event.
+        //var title = document.getElementById('text-title').value;
+        //if (title.replace(/[\s]/g, '') == "")
+        //  title = extension.localeData.localizeMessage("newGroup");
+        //this.saveGroupCell(this.mPickedIndex[0], 'name', title);
       }
     }
   },
@@ -637,11 +993,11 @@
   },
   saveScript: function () {
     if (this.mScriptIndex != null) {
-      var title = document.getElementById('script-title').value;
-      if (title.replace(/[\s]/g, '') == "")
-        title = extension.localeData.localizeMessage("newScript");
-
-      this.saveScriptCell(this.mScriptIndex, 'name', title);
+      // The title is updated in the onchange event.
+      //var title = document.getElementById('script-title').value;
+      //if (title.replace(/[\s]/g, '') == "")
+      //  title = extension.localeData.localizeMessage("newScript");
+      //this.saveScriptCell(this.mScriptIndex, 'name', title);
       this.saveScriptCell(this.mScriptIndex, 'script', document.getElementById('script').value);
     }
   },
@@ -663,14 +1019,16 @@
     }
   },
   checkForGeneralChanges: function (aIndex) {
-    var ids = ['checkbox-viewPopup', 'checkbox-collapseGroup', 'select-shortcutModifier', 'checkbox-shortcutTypeAdv', 'select-keywordKey', 'text-defaultImport'];
-    var type = ['checked', 'checked', 'value', 'checked', 'value', 'value'];
-    var keys = ['viewPopup', 'collapseGroup', 'shortcutModifier', 'shortcutTypeAdv', 'keywordKey', 'defaultImport'];
+    const ids = ['checkbox-viewPopup', 'checkbox-collapseGroup', 'select-shortcutModifier', 'checkbox-shortcutTypeAdv', 'select-keywordKey', 'box-defaultImport', 'box-storageLocations'];
+    const type = ['checked', 'checked', 'value', 'checked', 'value', 'dataset', 'dataset'];
+    const keys = ['viewPopup', 'collapseGroup', 'shortcutModifier', 'shortcutTypeAdv', 'keywordKey', 'defaultImport', 'storageLocations'];
 
     if (typeof ids[aIndex] == 'undefined')
       return;
 
-    var value = document.getElementById(ids[aIndex])[type[aIndex]];
+    const value = (type[aIndex] === "dataset")
+      ? document.getElementById(ids[aIndex]).dataset.value
+      : document.getElementById(ids[aIndex])[type[aIndex]];
 
     if (gQuicktext[keys[aIndex]] != value)
       this.generalChangeMade(aIndex);
@@ -687,37 +1045,60 @@
     if (gQuicktext.shortcutTypeAdv)
       ids[2] = 'text-shortcutAdv';
 
-    var value = document.getElementById(ids[aIndex]).value;
+    let element = document.getElementById(ids[aIndex])
+    var value = element.value;
     switch (aIndex) {
       case 0:
-        if (value.replace(/[\s]/g, '') == "")
-          if (this.mPickedIndex[1] > -1)
+        if (this.mPickedIndex[1] > -1) {
+          if (value.replace(/[\s]/g, '') == "") {
             value = extension.localeData.localizeMessage("newTemplate");
-          else
+          }
+          // Prevent duplicated names.
+          value = this.makeUnique(value, gQuicktext.mEditingTexts[this.mPickedIndex[0]].map(t => t.mName));
+        } else {
+          if (value.replace(/[\s]/g, '') == "") {
             value = extension.localeData.localizeMessage("newGroup");
+          }
+          // Prevent duplicated names.
+          value = this.makeUnique(value, gQuicktext.mEditingGroup.map(g => g.mName))
+        }
         break;
       case 2:
         if (gQuicktext.shortcutTypeAdv) {
           value = value.replace(/[^\d]/g, '');
-          document.getElementById(ids[aIndex]).value = value;
+          element.value = value;
         }
       case 4:
         value = value.replace(/[\s]/g, '');
-        document.getElementById(ids[aIndex]).value = value;
+        element.value = value;
+        break;
+      case 6:
+        document.getElementById("deprecated_attachment").style.display = document.getElementById('text-attachments').value ? "" : "none"
         break;
     }
 
     if (this.mPickedIndex[1] > -1) {
-      if (gQuicktext.getText(this.mPickedIndex[0], this.mPickedIndex[1], true)[keys[aIndex]] != value)
+      if (gQuicktext.getText(this.mPickedIndex[0], this.mPickedIndex[1], true)[keys[aIndex]] != value) {
         this.textChangeMade(aIndex);
-      else
+      } else {
         this.noTextChangeMade(aIndex);
+      }
     }
     else {
-      if (gQuicktext.getGroup(this.mPickedIndex[0], true)[keys[aIndex]] != value)
+      if (gQuicktext.getGroup(this.mPickedIndex[0], true)[keys[aIndex]] != value) {
         this.textChangeMade(aIndex);
-      else
+      } else {
         this.noTextChangeMade(aIndex);
+      }
+    }
+
+    // Auto-save names.
+    if (aIndex == 0) {
+      if (this.mPickedIndex[1] > -1) {
+        gQuicktext.mEditingTexts[this.mPickedIndex[0]][this.mPickedIndex[1]].mName = value;
+      } else {
+        gQuicktext.mEditingGroup[this.mPickedIndex[0]].mName = value;
+      }
     }
 
     if (aIndex == 0 || aIndex == 2) {
@@ -742,8 +1123,11 @@
     var value = document.getElementById(ids[aIndex]).value;
     switch (aIndex) {
       case 0:
-        if (value.replace(/[\s]/g, '') == "")
+        if (value.replace(/[\s]/g, '') == "") {
           value = extension.localeData.localizeMessage("newScript");
+        }
+        // Prevent duplicated names.
+        value = this.makeUnique(value, gQuicktext.mEditingScripts.map(t => t.mName));
         break;
     }
 
@@ -753,6 +1137,9 @@
       this.noScriptChangeMade(aIndex);
 
     if (aIndex == 0) {
+      // Auto-save names.
+      gQuicktext.mEditingScripts[this.mScriptIndex].mName = value;
+
       this.updateVariableGUI();
       var listItem = document.getElementById('script-list').getItemAtIndex(this.mScriptIndex);
       listItem.firstChild.value = value;
@@ -833,15 +1220,9 @@
   /*
    * GUI CHANGES
    */
-  updateGUI: function () {
-    const dateTimeFormat = (format, timeStamp) => {
-      let options = {};
-      options["date-short"] = { dateStyle: "short" };
-      options["date-long"] = { dateStyle: "long" };
-      options["date-monthname"] = { month: "long" };
-      options["time-noseconds"] = { timeStyle: "short" };
-      options["time-seconds"] = { timeStyle: "long" };
-      return new Services.intl.DateTimeFormat(undefined, options[format.toLowerCase()]).format(timeStamp)
+  updateGUI: async function () {
+    const dateTimeFormat = async (format, timeStamp) => {
+      return notifyTools.notifyBackground({ command: "getDateTimeFormat", data: { format, timeStamp } });
     }
 
     // Set the date/time in the variablemenu
@@ -849,11 +1230,11 @@
     let fields = ["date-short", "date-long", "date-monthname", "time-noseconds", "time-seconds"];
     for (let i = 0; i < fields.length; i++) {
       let field = fields[i];
-      let fieldtype = field.split("-")[0];
+      let fieldType = field.split("-")[0];
       if (document.getElementById(field)) {
         document.getElementById(field).setAttribute(
           "label",
-          extension.localeData.localizeMessage(fieldtype, [dateTimeFormat(field, timeStamp)])
+          extension.localeData.localizeMessage(fieldType, [await dateTimeFormat(field, timeStamp)])
         );
       }
     }
@@ -870,8 +1251,6 @@
       elem.checked = gQuicktext.shortcutTypeAdv;
       this.shortcutModifierChange();
     }
-    if (document.getElementById("text-defaultImport"))
-      document.getElementById("text-defaultImport").value = gQuicktext.defaultImport;
     if (document.getElementById("select-keywordKey"))
       document.getElementById("select-keywordKey").value = gQuicktext.keywordKey;
 
@@ -958,14 +1337,33 @@
           var listItem = listElem.getItemAtIndex(i);
           listItem.firstChild.value = script.name;
           listItem.value = i;
-        }
-        else {
+
+          let isIncompatible = isIncompatibleScript(script);
+          if (listItem.children.length > 1 && !isIncompatible) {
+            listItem.children[1].remove();
+          } else if (listItem.children.length == 1 && isIncompatible) {
+            let newItemWarning = document.createXULElement("label");
+            newItemWarning.value = "??";
+            listItem.appendChild(newItemWarning);
+          };
+        } else {
+          // Keep the height of the script list fixed, prevent it growing.
+          let elementHeight = listElem.getBoundingClientRect().height;
           let newItem = document.createXULElement("richlistitem");
           newItem.value = i;
+
           let newItemLabel = document.createXULElement("label");
           newItemLabel.value = script.name;
           newItem.appendChild(newItemLabel);
+
+          if (isIncompatibleScript(script)) {
+            let newItemWarning = document.createXULElement("label");
+            newItemWarning.value = "??";
+            newItem.appendChild(newItemWarning);
+          }
+
           listElem.appendChild(newItem);
+          listElem.style.height = `${elementHeight}px`;
         }
       }
     }
@@ -1272,6 +1670,13 @@
     this.enableSave();
   },
   // TODO: Hardcoding files is no longer possible in pure WebExt, either Exp only or gallery.
+  insertAttachmentVariable: async function () {
+    if ((file = await gQuicktext.pickFile([2], 0, extension.localeData.localizeMessage("attachmentFile"))) != null) {
+      this.insertVariable('ATTACHMENT=FILE|' + file.path);
+    }
+    this.enableSave();
+  },
+  // TODO: Hardcoding files is no longer possible in pure WebExt, either Exp only or gallery.
   insertFileVariable: async function () {
     if ((file = await gQuicktext.pickFile([2], 0, extension.localeData.localizeMessage("insertFile"))) != null) {
       this.insertVariable('FILE=' + file.path);
@@ -1281,7 +1686,7 @@
   // TODO: Hardcoding files is no longer possible in pure WebExt, either Exp only or gallery.
   insertImageVariable: async function () {
     if ((file = await gQuicktext.pickFile([4], 0, extension.localeData.localizeMessage("insertImage"))) != null) {
-      this.insertVariable('IMAGE=' + file.path);
+      this.insertVariable('IMAGE=FILE|' + file.path);
     }
     this.enableSave();
   },
@@ -1299,15 +1704,15 @@
     // const parsedData = await notifyTools.notifyBackground({ command: "pickAndParseConfigFile" });
     const file = await gQuicktext.pickFile([5, 3], 0, extension.localeData.localizeMessage("importFile"));
     if (!file) return;
-    const parsedData = await notifyTools.notifyBackground({ command: "parseConfigFile", path: file.path });
-    
-    if (!parsedData || !parsedData.templates) return;
+    const templates = await notifyTools.notifyBackground({ command: "parseTemplateFileForImport", path: file.path });
+
+    if (!templates) return;
 
     this.saveText();
     this.saveScript();
     var length = this.mTreeArray.length;
 
-    gQuicktext.importTemplates(parsedData.templates)
+    gQuicktext.importTemplates(templates)
 
     this.changesMade();
     this.makeTreeArray();
@@ -1324,14 +1729,14 @@
     // const parsedData = await notifyTools.notifyBackground({ command: "pickAndParseConfigFile" });
     const file = await gQuicktext.pickFile([5, 3], 0, extension.localeData.localizeMessage("importFile"));
     if (!file) return;
-    const parsedData = await notifyTools.notifyBackground({ command: "parseConfigFile", path: file.path });
-    
-    if (!parsedData || !parsedData.scripts) return;
+    const scripts = await notifyTools.notifyBackground({ command: "parseScriptFileForImport", path: file.path });
+
+    if (!scripts) return;
 
     this.saveText();
     this.saveScript();
 
-    gQuicktext.importScripts(parsedData.scripts)
+    gQuicktext.importScripts(scripts)
 
     this.changesMade();
     this.updateScriptGUI();
@@ -1385,6 +1790,15 @@
       document.getElementById('script-button-remove').setAttribute("disabled", true);
     else
       document.getElementById('script-button-remove').removeAttribute("disabled");
+
+    if (isIncompatibleScript(script)) {
+      document.getElementById('scripthelpbutton').dataset.isIncompatible = "true";
+      document.getElementById('scriptwarning').style.display = "block";
+    } else {
+      document.getElementById('scripthelpbutton').dataset.isIncompatible = "false";
+      document.getElementById('scriptwarning').style.display = "none";
+    }
+
   },
   pickText: function () {
     var index = document.getElementById('group-tree').view.selection.currentIndex;
@@ -1417,6 +1831,7 @@
       document.getElementById('text-keyword').value = text.keyword;
       document.getElementById('text-subject').value = text.subject;
       document.getElementById('text-attachments').value = text.attachments;
+      document.getElementById("deprecated_attachment").style.display = text.attachments ? "" : "none"
 
       document.getElementById('label-shortcutModifier').value = extension.localeData.localizeMessage(document.getElementById('select-shortcutModifier').value + "Key") + "+";
 
@@ -1453,6 +1868,7 @@
       document.getElementById("text-keyword").value = "";
       document.getElementById("text-subject").value = "";
       document.getElementById("text-attachments").value = "";
+      document.getElementById("deprecated_attachment").style.display = "none";
     }
 
     var disabled = false;
@@ -1496,13 +1912,30 @@
     }
   },
 
+  makeUnique: function (name, arr) {
+    let sanitizedName = name
+      .replaceAll("|", "/")
+      .replaceAll("[[", "{[")
+      .replaceAll("]]", "]}");
+    let suffix = 1;
+    let unique = sanitizedName;
+    while (arr.includes(unique)) {
+      suffix++;
+      unique = `${sanitizedName} #${suffix}`
+    }
+    return unique;
+  },
+
   /*
    * Add/Remove groups/templates
    */
   addGroup: function () {
-    var title = extension.localeData.localizeMessage("newGroup");
     this.saveText();
 
+    let title = this.makeUnique(
+      extension.localeData.localizeMessage("newGroup"),
+      gQuicktext.mEditingGroup.map(g => g.mName)
+    );
     gQuicktext.addGroup(title, true);
     this.mCollapseState.push(true);
 
@@ -1522,7 +1955,6 @@
     titleElem.setSelectionRange(0, title.length);
   },
   addText: function () {
-    var title = extension.localeData.localizeMessage("newTemplate");
     this.saveText();
 
     var groupIndex = -1;
@@ -1537,6 +1969,10 @@
         groupIndex = 0;
     }
 
+    let title = this.makeUnique(
+      extension.localeData.localizeMessage("newTemplate"),
+      gQuicktext.mEditingTexts[groupIndex].map(t => t.mName)
+    );
     gQuicktext.addText(groupIndex, title, true);
 
     this.makeTreeArray();
@@ -1624,7 +2060,10 @@
   addScript: function () {
     this.saveScript();
 
-    var title = extension.localeData.localizeMessage("newScript");
+    let title = this.makeUnique(
+      extension.localeData.localizeMessage("newScript"),
+      gQuicktext.mEditingScripts.map(s => s.mName)
+    );
     gQuicktext.addScript(title, true);
 
     this.updateScriptGUI();
@@ -1659,6 +2098,7 @@
         else {
           this.mScriptIndex = null;
           selectedIndex = -1;
+          document.getElementById('scriptwarning').style.display = "none";
         }
 
         document.getElementById('script-list').selectedIndex = selectedIndex;
@@ -1679,6 +2119,9 @@
   openHomepage: function () {
     notifyTools.notifyBackground({ command: "openWebPage", url: "https://github.com/jobisoft/quicktext/wiki/" });
   },
+  openScriptHelp: function () {
+    notifyTools.notifyBackground({ command: "openWebPage", url: "https://github.com/jobisoft/quicktext/issues/451" });
+  },
   resetCounter: function () {
     notifyTools.notifyBackground({ command: "setPref", pref: "counter", value: 0 });
   },
@@ -1695,3 +2138,11 @@
 
 window.addEventListener("DOMContentLoaded", () => settingsDialog.init());
 window.addEventListener("unload", () => settingsDialog.unload());
+window.addEventListener('resize', () => {
+  // Keep the known offset between window size and script list size, to make it
+  // correctly adjust to the changed window size.
+  let listElem = document.getElementById('script-list');
+  listElem.style.height = `${Math.max(150, window.innerHeight - boxHeightOffset)}px`;
+});
+
+
diff -Nru quicktext-6.3.2/xul_settings_dialog/settings.xhtml quicktext-6.4.6/xul_settings_dialog/settings.xhtml
--- quicktext-6.3.2/xul_settings_dialog/settings.xhtml	2025-04-04 09:46:33.000000000 +0200
+++ quicktext-6.4.6/xul_settings_dialog/settings.xhtml	2025-08-26 01:05:47.000000000 +0200
@@ -6,10 +6,9 @@
   xmlns="http://www.w3.org/1999/xhtml"
   xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
   xmlns:html="http://www.w3.org/1999/xhtml"
-  persist="width height screenX screenY sizemode"
-  sstyle="padding: 5px;"
-  width="600"
-  height="400">
+  persist="screenX screenY"
+  width="750"
+  height="600">
 
   <head>
     <title>__MSG_quicktext.settings.title__</title>
@@ -39,8 +38,9 @@
             <tab label="__MSG_quicktext.general.label__" accesskey="__MSG_quicktext.accesskey.general__"/>
             <tab label="__MSG_quicktext.templates.label__" accesskey="__MSG_quicktext.accesskey.templates__"/>
             <tab label="__MSG_quicktext.scripts.label__" accesskey="__MSG_quicktext.accesskey.scripts__"/>
+            <tab label="__MSG_quicktext.advanced.label__" accesskey="__MSG_quicktext.accesskey.advanced__"/>
           </tabs>
-          <tabpanels flex="1" style="margin: 0px; padding: 0px">
+          <tabpanels id="tabpanels-main" flex="1" style="margin: 0px; padding: 0px">
             <tabpanel id="tab-general">
               <hbox flex="1">
                 <vbox flex="1">
@@ -86,7 +86,14 @@
                   <fieldset>
                     <vbox flex="1">
                       <hbox><legend class="insideTab">__MSG_quicktext.defaultImport.label__</legend></hbox>
-                      <hbox class="input-container"><html:input id="text-defaultImport" oninput="settingsDialog.checkForGeneralChanges(5);" /></hbox>
+                      <hbox flex="1">
+                        <richlistbox id="box-defaultImport"/>
+                      </hbox>
+                      <hbox>
+                        <button id="defaultImport_addFileItem" label="__MSG_quicktext.buttons.addFile.label__" disabled="true" />
+                        <button id="defaultImport_addUrlItem" label="__MSG_quicktext.buttons.addUrl.label__" disabled="true" />
+                        <button id="defaultImport_remove" label="-" disabled="true" />
+                      </hbox>
                     </vbox>
                   </fieldset>
                 </vbox>
@@ -207,6 +214,8 @@
                               <menupopup>
                                 <menuitem label="__MSG_quicktext.filename.label__" oncommand="settingsDialog.insertVariable('ATT=name');" />
                                 <menuitem label="__MSG_quicktext.filenameAndSize.label__" oncommand="settingsDialog.insertVariable('ATT=full');" />
+                                <menuseparator/>
+                                <menuitem label="__MSG_attachmentFile__" oncommand="settingsDialog.insertAttachmentVariable();" />
                               </menupopup>
                             </menu>
                             <menu label="__MSG_quicktext.dateTime.label__">
@@ -305,7 +314,7 @@
                       </html:tr>
 
                       <html:tr showfor="text">
-                        <html:td width="80">
+                        <html:td width="80" valign="top">
                           <label align="center" value="__MSG_quicktext.attachments.label__:" control="text-attachments" style="margin-top: 6px" />
                         </html:td>
                         <html:td>
@@ -313,8 +322,11 @@
                             <html:input id="text-attachments" disabled="true" flex="1" candisable="true" oninput="settingsDialog.checkForTextChanges(6);"/>
                             <button id="button-attachments" label="__MSG_quicktext.browse.label__" oncommand="settingsDialog.browseAttachment();" disabled="true" candisable="true"/>
                           </hbox>
+                          <hbox id="deprecated_attachment" style="display:none">
+                            <description style="margin-top: 6px; color:red;">__MSG_deprecated_attachment_field__</description>
+                          </hbox>
                         </html:td>
-                      </html:tr>                
+                      </html:tr>
                     </html:table>
                   </vbox>
                 </fieldset>
@@ -327,8 +339,12 @@
                     <hbox><legend class="insideTab">__MSG_quicktext.title.label__</legend></hbox>
                     <hbox><button id="script-button-add" label="__MSG_quicktext.addScript.label__" flex="1" oncommand="settingsDialog.addScript();" accesskey="__MSG_quicktext.accesskey.addScript__"/></hbox>
                     <hbox><button label="__MSG_quicktext.getScript.label__" flex="1" oncommand="settingsDialog.getCommunityScripts();" accesskey="__MSG_quicktext.accesskey.communityScripts__"/></hbox>
-                    <richlistbox id="script-list" flex="1" onselect="settingsDialog.pickScript();"/>
-                    <hbox><button id="script-button-remove" label="__MSG_quicktext.remove.label__" flex="1" oncommand="settingsDialog.removeScript();" accesskey="__MSG_quicktext.accesskey.remove__"/></hbox>
+                    <hbox flex="1">
+                      <richlistbox id="script-list" onselect="settingsDialog.pickScript();"/>
+                    </hbox>
+                    <hbox>
+                      <button id="script-button-remove" label="__MSG_quicktext.remove.label__" flex="1" oncommand="settingsDialog.removeScript();" accesskey="__MSG_quicktext.accesskey.remove__"/>
+                    </hbox>
                   </vbox>
                 </fieldset>
 
@@ -355,11 +371,43 @@
 
                       <vbox class="textarea-container" style="display:flex; flex-direction: column;" flex="1">
                         <html:textarea id="script" style="font-family:Consolas,Courier New,monospace; flex:1" disabled="true" wrap="off" oninput="settingsDialog.checkForScriptChanges(1);"/>
+                        <html:div id="scriptwarning" style="
+                            display:none;
+                            position: absolute;
+                            bottom: 2em;
+                            right: 15px;
+                            font-size: 1em;
+                            color: red;
+                            pointer-events: none; ">
+                            ?? __MSG_quicktext.scripthelp.label__ (Quicktext v6)
+                          </html:div>
+                      </vbox>
+                      <vbox>
+
                       </vbox>
                   </vbox>
                 </fieldset>
               </vbox>
             </tabpanel>
+            <tabpanel id="tab-advanced">
+              <hbox flex="1">
+                <vbox flex="1">
+                  <fieldset>
+                    <vbox flex="1">
+                      <hbox><legend class="insideTab">__MSG_quicktext.storageLocations.label__</legend></hbox>
+                      <description style="margin-top: 6px;">__MSG_quicktext.storageLocations.description__</description>
+                      <richlistbox id="box-storageLocations"/>
+                      <hbox>
+                        <button id="storageLocations_addFolderItem" label="__MSG_quicktext.buttons.addFolder.label__" disabled="true" />
+                        <button id="storageLocations_remove" label="-" disabled="true" />
+                        <spacer flex="1"/>
+                        <button id="storageLocations_select" label="__MSG_quicktext.buttons.selectStorage.label__" disabled="true" />
+                      </hbox>
+                    </vbox>
+                  </fieldset>
+                </vbox>
+              </hbox>
+            </tabpanel>
           </tabpanels>
         </tabbox>
       </vbox>
@@ -367,6 +415,7 @@
 
     <hbox style="margin: 5px">
       <div flex="1"></div>
+      <button id="scripthelpbutton" label="__MSG_quicktext.scripthelp.label__?" />
       <button id="helpbutton" label="__MSG_quicktext.help.label__"/>
       <button id="savebutton" label="__MSG_quicktext.save.label__"/>
       <button id="closebutton" label="__MSG_quicktext.close.label__"/>


More information about the Pkg-mozext-maintainers mailing list