From 40db42379016683b8f6fba000dc149be3b51878c Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Fri, 23 Jan 2026 12:55:11 +0700 Subject: [PATCH 01/10] Update for modern Python versions (Anki has 3.13) Python 3.6 switched how package-local imports work, so the top-level __init__.py file of a package is no longer found in sys.path by default. One solution would be to change all the imports throughout the package to be relative imports using the `from . import pkg` syntax, but that would involve a lot of changes. Instead, we simply re-add the top-level directory to sys.path, and all the existing imports can be unchanged. Python 3.10 deprecated the distutils package, and Python 3.12 removed it from the standard library. There was only one function that FlashGrab was actually using, the "newer" function, so we just reimplement it. Also, instead of DistutilsFileError which is no longer available, all errors in copying files will now throw OSError. Finally, the log function from distutils has been replaced with simple print() calls, which will not be visible to the user anyway since Anki runs without a terminal console by default. --- __init__.py | 4 ++++ syncxml/file_util.py | 57 +++++++++++++++++++++----------------------- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/__init__.py b/__init__.py index 89cc784..4f8fc3a 100644 --- a/__init__.py +++ b/__init__.py @@ -3,5 +3,9 @@ from the source XML file(s) as specified in the config file. """ +# Make package-local imports work in Python 3.6 and later +import os, sys +sys.path.append(os.path.dirname(os.path.realpath(__file__))) + from syncxml import SyncFromXML diff --git a/syncxml/file_util.py b/syncxml/file_util.py index 7b73656..f767379 100644 --- a/syncxml/file_util.py +++ b/syncxml/file_util.py @@ -12,9 +12,7 @@ __revision__ = "$Id$" -import os -from distutils.errors import DistutilsFileError -from distutils import log +import os, sys # for generating verbose output in 'copy_file()' _copy_action = {None: 'copying', @@ -22,11 +20,21 @@ 'sym': 'symbolically linking'} +# from distutils.dep_util import newer +# distutils deprecated in Python 3.12 and later, so reimplement newer() function +def newer(source, target): + if not os.path.exists(source): + raise OSError("file %s does not exist" % (os.path.abspath(source))) + return not os.path.exists(target) or ( + os.path.getmtime(source) > os.path.getmtime(target) + ) + + def _copy_file_contents(src, dst, buffer_size=16*1024): """Copy the file 'src' to 'dst'. Both must be filenames. Any error opening either file, reading from - 'src', or writing to 'dst', raises DistutilsFileError. Data is + 'src', or writing to 'dst', raises OSError. Data is read/written in chunks of 'buffer_size' bytes (default 16k). No attempt is made to handle anything apart from regular files. """ @@ -39,30 +47,27 @@ def _copy_file_contents(src, dst, buffer_size=16*1024): fsrc = open(src, 'rb') except os.error as xxx_todo_changeme3: (errno, errstr) = xxx_todo_changeme3.args - raise DistutilsFileError("could not open '%s': %s" % (src, errstr)) + raise OSError("could not open '%s': %s" % (src, errstr)) if os.path.exists(dst): try: os.unlink(dst) except os.error as xxx_todo_changeme: (errno, errstr) = xxx_todo_changeme.args - raise DistutilsFileError( - "could not delete '%s': %s" % (dst, errstr)) + raise OSError("could not delete '%s': %s" % (dst, errstr)) try: fdst = open(dst, 'wb') except os.error as xxx_todo_changeme4: (errno, errstr) = xxx_todo_changeme4.args - raise DistutilsFileError( - "could not create '%s': %s" % (dst, errstr)) + raise OSError("could not create '%s': %s" % (dst, errstr)) while 1: try: buf = fsrc.read(buffer_size) except os.error as xxx_todo_changeme1: (errno, errstr) = xxx_todo_changeme1.args - raise DistutilsFileError( - "could not read from '%s': %s" % (src, errstr)) + raise OSError("could not read from '%s': %s" % (src, errstr)) if not buf: break @@ -71,8 +76,7 @@ def _copy_file_contents(src, dst, buffer_size=16*1024): fdst.write(buf) except os.error as xxx_todo_changeme2: (errno, errstr) = xxx_todo_changeme2.args - raise DistutilsFileError( - "could not write to '%s': %s" % (dst, errstr)) + raise OSError("could not write to '%s': %s" % (dst, errstr)) finally: if fdst: @@ -113,12 +117,10 @@ def copy_file(src, dst, preserve_mode=1, preserve_times=1, update=0, # changing it (ie. it's not already a hard/soft link to src OR # (not update) and (src newer than dst). - from distutils.dep_util import newer from stat import ST_ATIME, ST_MTIME, ST_MODE, S_IMODE if not os.path.isfile(src): - raise DistutilsFileError( - "can't copy '%s': doesn't exist or not a regular file" % src) + raise OSError("can't copy '%s': doesn't exist or not a regular file" % src) if os.path.isdir(dst): dir = dst @@ -128,7 +130,7 @@ def copy_file(src, dst, preserve_mode=1, preserve_times=1, update=0, if update and not newer(src, dst): if verbose >= 1: - log.debug("not copying %s (output up-to-date)", src) + print("not copying %s (output up-to-date)".format(src), file=sys.stderr) return dst, 0 try: @@ -138,9 +140,9 @@ def copy_file(src, dst, preserve_mode=1, preserve_times=1, update=0, if verbose >= 1: if os.path.basename(dst) == os.path.basename(src): - log.info("%s %s -> %s", action, src, dir) + print("%s %s -> %s".format(action, src, dir)) else: - log.info("%s %s -> %s", action, src, dst) + print("%s %s -> %s".format(action, src, dst)) if dry_run: return (dst, 1) @@ -185,25 +187,21 @@ def move_file (src, dst, verbose=1, dry_run=0): import errno if verbose >= 1: - log.info("moving %s -> %s", src, dst) + print("moving %s -> %s".format(src, dst)) if dry_run: return dst if not isfile(src): - raise DistutilsFileError("can't move '%s': not a regular file" % src) + raise OSError("can't move '%s': not a regular file" % src) if isdir(dst): dst = os.path.join(dst, basename(src)) elif exists(dst): - raise DistutilsFileError( - "can't move '%s': destination '%s' already exists" % - (src, dst)) + raise OSError("can't move '%s': destination '%s' already exists" % (src, dst)) if not isdir(dirname(dst)): - raise DistutilsFileError( - "can't move '%s': destination '%s' not a valid path" % \ - (src, dst)) + raise OSError("can't move '%s': destination '%s' not a valid path" % (src, dst)) copy_it = 0 try: @@ -213,8 +211,7 @@ def move_file (src, dst, verbose=1, dry_run=0): if num == errno.EXDEV: copy_it = 1 else: - raise DistutilsFileError( - "couldn't move '%s' to '%s': %s" % (src, dst, msg)) + raise OSError("couldn't move '%s' to '%s': %s" % (src, dst, msg)) if copy_it: copy_file(src, dst, verbose=verbose) @@ -226,7 +223,7 @@ def move_file (src, dst, verbose=1, dry_run=0): os.unlink(dst) except os.error: pass - raise DistutilsFileError( + raise OSError( ("couldn't move '%s' to '%s' by copy/delete: " + "delete '%s' failed: %s") % (src, dst, src, msg)) From dce64e532a1db38ff0e13fc512fa1b83671ffacf Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Fri, 23 Jan 2026 13:01:24 +0700 Subject: [PATCH 02/10] Update for Qt6 changes in PyQt Older versions of Anki used Qt5, but more recent versions use Qt6. Most things are unchanged between 5 and 6, but a few constants or methods changed namespace. We try both locations, old and new, so that the plugin will work on older and newer versions of Anki. --- syncxml/SyncFromXML.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/syncxml/SyncFromXML.py b/syncxml/SyncFromXML.py index 110941a..605006c 100644 --- a/syncxml/SyncFromXML.py +++ b/syncxml/SyncFromXML.py @@ -31,8 +31,14 @@ from anki.notes import Note # from anki.utils import isMac #obsolete now, I think # from PyQt4.QtGui import QMessageBox #done for us already? - QUESTION = QMessageBox.Question - CRITICAL = QMessageBox.Critical + try: + QUESTION = QMessageBox.Icon.Question + except AttributeError: + QUESTION = QMessageBox.Question + try: + CRITICAL = QMessageBox.Icon.Critical + except AttributeError: + CRITICAL = QMessageBox.Critical else: # crash-prevention dummies QUESTION = 0 @@ -59,7 +65,11 @@ def dialogbox(text, buttons, icon=4, log=True): def hourglass(): if A.IN_ANKI: - mw.app.setOverrideCursor(QCursor(Qt.WaitCursor)) # display an hourglass cursor + try: + mw.app.setOverrideCursor(QCursor(Qt.WaitCursor)) # display an hourglass cursor + except AttributeError: + # Qt6 has moved this to CursorShape + mw.app.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor)) # display an hourglass cursor def no_hourglass(): if A.IN_ANKI: mw.app.restoreOverrideCursor() From d4cd8866c1e0bf7c553d6bface70d78737de2466 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Fri, 23 Jan 2026 13:03:05 +0700 Subject: [PATCH 03/10] Anki now wants a manifest.json file The manifest.json file requires at least two keys, the name of the addon package (determines the folder name it will be stored under) and the user-visible name of the addon. Since the name is now FlashGrab instead of SyncXml, we will use FlashGrab as the name of the folder where Anki should store the addon. --- manifest.json | 4 ++++ syncxml/anki_util.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 manifest.json diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..963933b --- /dev/null +++ b/manifest.json @@ -0,0 +1,4 @@ +{ + "package": "FlashGrab", + "name": "FlashGrab: import LIFT XML from FieldWorks or WeSay" +} diff --git a/syncxml/anki_util.py b/syncxml/anki_util.py index d84e8ec..73d5245 100644 --- a/syncxml/anki_util.py +++ b/syncxml/anki_util.py @@ -18,9 +18,9 @@ ANKI_MEDIA_FOLDER = 'collection.media' -ADDON_FOLDER = 'syncxml' +ADDON_FOLDER = 'FlashGrab' APKG_PATH = os.path.join("samples", "lift-dictionary.apkg") -NO_MODEL_INSTR = "\nSteps: Restart Anki and make sure the target deck exists. Use Tools, Manage Note Types, or go to File Import, and import Anki/addons/syncxml/{}. Or, reconfigure.)".format(APKG_PATH) +NO_MODEL_INSTR = "\nSteps: Restart Anki and make sure the target deck exists. Use Tools, Manage Note Types, or go to File Import, and import Anki/addons/{}/{}. Or, reconfigure.)".format(ADDON_FOLDER, APKG_PATH) NO_MODEL_MSG = "The target model with the fields we needed was missing; have attempted to re-import the default APKG but an error occurred. \nPlease try again, or create it manually. \n{}".format(NO_MODEL_INSTR) TARGET_DECK = "lift-dictionary" From b9cef7e7bf8d49f9b9d3ae81cabf72b32fde44dc Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Fri, 23 Jan 2026 13:04:45 +0700 Subject: [PATCH 04/10] Update CopyFilesToAddons batch file with new files The manifest.json file needs to be copied into the addons folder as well, plus the syncx.py file has now been replaced by __init__.py. --- CopyFilesToAddons.bat | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CopyFilesToAddons.bat b/CopyFilesToAddons.bat index dd68b5b..06a27b0 100644 --- a/CopyFilesToAddons.bat +++ b/CopyFilesToAddons.bat @@ -11,6 +11,10 @@ xcopy /D /E /Y /exclude:ExcludedFilesWindows.txt xml\*.* %USERPROFILE%\documents xcopy /D /E /Y /exclude:ExcludedFilesWindows.txt xpath\*.* %USERPROFILE%\documents\Anki\addons\xpath\ xcopy /D /E /Y /exclude:ExcludedFilesWindows.txt distutils\*.* %USERPROFILE%\documents\Anki\addons\distutils\ xcopy /D /E /Y /exclude:ExcludedFilesWindows.txt syncxml\*.* %USERPROFILE%\documents\Anki\addons\syncxml\ -xcopy /D /Y syncx.py %USERPROFILE%\documents\Anki\addons\ +xcopy /D /E /Y /exclude:ExcludedFilesWindows.txt syncxml\samples\*.* %USERPROFILE%\documents\Anki\addons\samples\ +xcopy /D /Y __init__.py %USERPROFILE%\documents\Anki\addons\ +REM WAS: xcopy /D /Y syncx.py %USERPROFILE%\documents\Anki\addons\ +xcopy /D /Y manifest.json %USERPROFILE%\documents\Anki\addons\ +xcopy /D /Y syncxml\SyncFromXML_config_default.txt %USERPROFILE%\documents\Anki\addons\ PAUSE From 61950e7fa86a5e7afebe946314888081b6dd0742 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Thu, 22 Jan 2026 17:24:29 +0700 Subject: [PATCH 05/10] Use encoding=utf8 when reading and writing --- syncxml/xml_util.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/syncxml/xml_util.py b/syncxml/xml_util.py index 8523e5b..9fe7da9 100644 --- a/syncxml/xml_util.py +++ b/syncxml/xml_util.py @@ -693,12 +693,11 @@ def replace_all(fname, to_replace, target=None): # while os.path.exists(fname2): # fname2 += ".tmp" - # using python 2.x decode()/encode() syntax below, since its open() function doesn't support this v3 parameter: encoding='utf-8' - with open(fname, 'r') as infile: + with open(fname, 'r', encoding='utf-8') as infile: data = infile.read() for pair in to_replace: data = re.sub(pair[0], pair[1], data) - with open (target, 'w') as outfile: + with open (target, 'w', encoding='utf-8') as outfile: outfile.write(data) def _workaround_px(fname): From cbb9ad0b49c130fc71f49677e6a0c0f867a93bc5 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Fri, 23 Jan 2026 12:30:45 +0700 Subject: [PATCH 06/10] Add zip-building scripts for Windows and Linux --- BuildZip.bat | 35 +++++++++++++++++++++++++++++++++++ BuildZip.sh | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 BuildZip.bat create mode 100755 BuildZip.sh diff --git a/BuildZip.bat b/BuildZip.bat new file mode 100644 index 0000000..e5e056a --- /dev/null +++ b/BuildZip.bat @@ -0,0 +1,35 @@ +@ECHO OFF +REM A Windows 'build' script. + +IF NOT EXIST "C:\Program Files/7-Zip/7z.exe" ( + ECHO This script uses 7-Zip to build the .zip file, but 7-Zip does not seem to be installed, so the script will fail. + ECHO The script also assumes that 7-Zip is installed in its default location, C:\Program Files/7-Zip/7z.exe + ECHO So please do not change the default install location of 7-zip when you install it. + EXIT /B 1 +) + +REM Delete and recreate temporary folder so we don't end up copying old files +RMDIR /S /Q %TEMP%\FlashGrabBuild +MKDIR %TEMP%\FlashGrabBuild + +REM Copy files into folder +xcopy /D /E /Y /exclude:ExcludedFilesWindows.txt xml\*.* %TEMP%\FlashGrabBuild\xml\ +xcopy /D /E /Y /exclude:ExcludedFilesWindows.txt xpath\*.* %TEMP%\FlashGrabBuild\xpath\ +REM No /E for syncxml since we don't want to include docsrc or samples directories +xcopy /D /Y /exclude:ExcludedFilesWindows.txt syncxml\*.* %TEMP%\FlashGrabBuild\syncxml\ +REM We do want one file from samples, but it should be in a top-level folder in the zip file +xcopy /D /E /Y /exclude:ExcludedFilesWindows.txt syncxml\samples\lift-dictionary.apkg %TEMP%\FlashGrabBuild\samples\ +REM The default config also needs to live in the top-level folder +move %TEMP%\FlashGrabBuild\syncxml\SyncFromXML_config_default.txt %TEMP%\FlashGrabBuild\ +xcopy /D /Y /exclude:ExcludedFilesWindows.txt __init__.py %TEMP%\FlashGrabBuild\ +xcopy /D /Y /exclude:ExcludedFilesWindows.txt manifest.json %TEMP%\FlashGrabBuild\ + +PUSHD %TEMP%\FlashGrabBuild +"C:\Program Files/7-Zip/7z.exe" a FlashGrab.zip syncxml samples xml xpath __init__.py manifest.json SyncFromXML_config_default.txt +POPD + +MOVE %TEMP%\FlashGrabBuild\FlashGrab.zip . + +ECHO FlashGrab.zip file created! +DIR FlashGrab.zip +PAUSE diff --git a/BuildZip.sh b/BuildZip.sh new file mode 100755 index 0000000..9a438cd --- /dev/null +++ b/BuildZip.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# A Linux build script + +which 7z || ( + echo 7zip not found. Please install the 7zip package, e.g. sudo apt install 7zip or sudo pacman -S 7zip + exit 1 +) + +# Delete and recreate temporary folder so we don't end up copying old files +rm -rf /tmp/FlashGrabBuild +mkdir /tmp/FlashGrabBuild + +# Copy files into folder +rsync -r -u -v --exclude-from ExcludedFilesLinux.txt xml /tmp/FlashGrabBuild +rsync -r -u -v --exclude-from ExcludedFilesLinux.txt xpath /tmp/FlashGrabBuild +rsync -r -u -v --exclude-from ExcludedFilesLinux.txt syncxml /tmp/FlashGrabBuild + +cp -u -v __init__.py /tmp/FlashGrabBuild +cp -u -v manifest.json /tmp/FlashGrabBuild + +# Some files under syncxml need to live in top-level directory +mv /tmp/FlashGrabBuild/syncxml/SyncFromXML_config_default.txt /tmp/FlashGrabBuild +mkdir -p /tmp/FlashGrabBuild/samples +mv /tmp/FlashGrabBuild/syncxml/samples/lift-dictionary.apkg /tmp/FlashGrabBuild/samples + +# Rest of samples and docsrc from syncxml dir doesn't need to be in the addon, to save file size +rm -rf /tmp/FlashGrabBuild/syncxml/docsrc +rm -rf /tmp/FlashGrabBuild/syncxml/samples + +pushd /tmp/FlashGrabBuild +7z a FlashGrab.zip syncxml samples xml xpath __init__.py manifest.json SyncFromXML_config_default.txt +popd + +mv /tmp/FlashGrabBuild/FlashGrab.zip . +ls -l FlashGrab.zip From 2c22ced94d288f75c75f7f61a162ff2a10487c50 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Fri, 23 Jan 2026 12:31:37 +0700 Subject: [PATCH 07/10] Fix sample cards not being removed on first import --- syncxml/anki_util.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/syncxml/anki_util.py b/syncxml/anki_util.py index 73d5245..da0d914 100644 --- a/syncxml/anki_util.py +++ b/syncxml/anki_util.py @@ -90,7 +90,11 @@ def import_apkg_model(path, delete=False): # assert deck.cardCount() > 0 # assert deck.noteCount() > 0 ids = mw.col.findCards("deck:{}".format(TARGET_DECK)) - mw.col.remCards(ids, True) + try: + mw.col.remCards(ids, True) + except TypeError: + # Newer versions of Anki renamed remCards and removed the second argument + mw.col.remove_cards_and_orphaned_notes(ids) # assert deck.cardCount() == 0 # assert deck.noteCount() == 0 except: From de841f6a2c8a7c0f2d73b666315504a86abaa701 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Fri, 23 Jan 2026 12:33:46 +0700 Subject: [PATCH 08/10] Fix Python warning about invalid syntax Docstrings were warning about \C not being a valid string escape. --- syncxml/flex_util.py | 4 ++-- syncxml/xml_util.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/syncxml/flex_util.py b/syncxml/flex_util.py index de8c97b..fb320c0 100644 --- a/syncxml/flex_util.py +++ b/syncxml/flex_util.py @@ -22,9 +22,9 @@ def flex_dir(): return d def flex_media(f, dir=""): - """Given an absolute filepath such as c:\files\Catalan.lift, and the FLEx project folder, + """Given an absolute filepath such as c:\\files\\Catalan.lift, and the FLEx project folder, check whether audio and image folders are likely to be available under, say, - C:\ProgramData\SIL\FieldWorks\Projects\Catalan\LinkedFiles + C:\\ProgramData\\SIL\\FieldWorks\\Projects\\Catalan\\LinkedFiles Or, given just an absolute path, assume it's an fwdata file and deduce accordingly. """ media_dir = "" diff --git a/syncxml/xml_util.py b/syncxml/xml_util.py index 9fe7da9..14b43ae 100644 --- a/syncxml/xml_util.py +++ b/syncxml/xml_util.py @@ -200,8 +200,8 @@ def __init__(self, file_path, source_file='', source_audio=None, source_image=No It only really stores the minidom, but it (and XmlSource) provide more convenient read access via get methods that return string dictionaries. If the optional parameters are provided, they will overwrite whatever was in those - attributes for *every* source element. If just source_file is provided, e.g. C:\mylift\Catalan.LIFT, - then audio and image will be deduced, e.g. C:\mylift\audio and C:\mylift\pictures . + attributes for *every* source element. If just source_file is provided, e.g. C:\\mylift\\Catalan.LIFT, + then audio and image will be deduced, e.g. C:\\mylift\\audio and C:\\mylift\\pictures . NOTE: if source_file is not provided, the other source_ parameters will be ignored. """ From 2ffe2d274a7d0f3fab46411c91dfbe41213ddf06 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Fri, 23 Jan 2026 12:36:06 +0700 Subject: [PATCH 09/10] Exclude __pycache__ folder from addon package Anki documentation specifically asks for __pycache__ folders not to be included in plugins, and says that the plugin .zip will be rejected if it contains any __pycache__ folders. I suspect they'll reject the .zip even if the __pycache__ folders are empty, so let's make sure we don't include any __pycache__ folders, even empty, in the install. --- ExcludedFilesLinux.txt | 3 ++- ExcludedFilesWindows.txt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ExcludedFilesLinux.txt b/ExcludedFilesLinux.txt index 3bd6463..98dd645 100644 --- a/ExcludedFilesLinux.txt +++ b/ExcludedFilesLinux.txt @@ -1,6 +1,7 @@ - *.pyc +- __pycache__/ - local_launch.py - syncx_local.py - SyncFromXML_config.txt - SyncFromXML_log.txt -- SyncFromXML_configB* \ No newline at end of file +- SyncFromXML_configB* diff --git a/ExcludedFilesWindows.txt b/ExcludedFilesWindows.txt index 7477bc4..5d7a767 100644 --- a/ExcludedFilesWindows.txt +++ b/ExcludedFilesWindows.txt @@ -1,6 +1,7 @@ .pyc\ +\__pycache__\ \local_launch.py\ \syncx_local.py\ \SyncFromXML_config.txt\ \SyncFromXML_log.txt\ -\SyncFromXML_configB \ No newline at end of file +\SyncFromXML_configB From e8ed756422ae62f75ba22898b120e135c2348eb1 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Fri, 23 Jan 2026 14:24:41 +0700 Subject: [PATCH 10/10] Update CopyFilesToAddons scripts for Anki 2.1 Anki 2.1 changed the location where plugins are stored (because it introduced breaking changes to the plugin interface, so that old-style plugins would no longer run). Since this plugin is now compatible with Anki 2.1 and later, the CopyFilesToAddons scripts for developers should copy it to the plugin's new location. --- CopyFilesToAddons.bat | 23 ++++++++++++++--------- CopyFilesToAddons.sh | 30 +++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/CopyFilesToAddons.bat b/CopyFilesToAddons.bat index 06a27b0..e70fcf6 100644 --- a/CopyFilesToAddons.bat +++ b/CopyFilesToAddons.bat @@ -2,19 +2,24 @@ ECHO OFF REM A Windows 'build' script. Run this after editing the addon's files. Then restart Anki. REM Don't make edits over there in addons directly, as VCS won't see them, and an Anki or addon upgrade REM might destroy them. + +REM IMPORTANT: If you change the plugin package in manifest.json, make the same change to PLUGIN_NAME below ECHO ON +SET PLUGIN_NAME=FlashGrab +SET ROOTFOLDER=%APPDATA%\Anki2\addons21\%PLUGIN_NAME% + REM copy all modified (D) files and folders (even E, empty ones), and overwrite (Y) without prompting REM (Like Anki's own deployment, this does NOT remove any files. The simplest way to do so manually here is to delete the addons folder, then re-run.) -xcopy /D /E /Y /exclude:ExcludedFilesWindows.txt xml\*.* %USERPROFILE%\documents\Anki\addons\xml\ -xcopy /D /E /Y /exclude:ExcludedFilesWindows.txt xpath\*.* %USERPROFILE%\documents\Anki\addons\xpath\ -xcopy /D /E /Y /exclude:ExcludedFilesWindows.txt distutils\*.* %USERPROFILE%\documents\Anki\addons\distutils\ -xcopy /D /E /Y /exclude:ExcludedFilesWindows.txt syncxml\*.* %USERPROFILE%\documents\Anki\addons\syncxml\ -xcopy /D /E /Y /exclude:ExcludedFilesWindows.txt syncxml\samples\*.* %USERPROFILE%\documents\Anki\addons\samples\ -xcopy /D /Y __init__.py %USERPROFILE%\documents\Anki\addons\ -REM WAS: xcopy /D /Y syncx.py %USERPROFILE%\documents\Anki\addons\ -xcopy /D /Y manifest.json %USERPROFILE%\documents\Anki\addons\ -xcopy /D /Y syncxml\SyncFromXML_config_default.txt %USERPROFILE%\documents\Anki\addons\ +xcopy /D /E /Y /exclude:ExcludedFilesWindows.txt xml\*.* "%ROOTFOLDER%\xml\" +xcopy /D /E /Y /exclude:ExcludedFilesWindows.txt xpath\*.* "%ROOTFOLDER%\xpath\" +REM No /E for syncxml since we don't want to include docsrc or samples directories +xcopy /D /Y /exclude:ExcludedFilesWindows.txt syncxml\*.* "%ROOTFOLDER%\syncxml\" +REM We do want one file from samples, but it should be in a top-level "samples" folder in the addons directory +xcopy /D /E /Y /exclude:ExcludedFilesWindows.txt syncxml\samples\lift-dictionary.apkg "%ROOTFOLDER%\samples\" +xcopy /D /Y __init__.py "%ROOTFOLDER%\" +xcopy /D /Y manifest.json "%ROOTFOLDER%\" +xcopy /D /Y syncxml\SyncFromXML_config_default.txt "%ROOTFOLDER%\" PAUSE diff --git a/CopyFilesToAddons.sh b/CopyFilesToAddons.sh index 8fcacc0..2272583 100644 --- a/CopyFilesToAddons.sh +++ b/CopyFilesToAddons.sh @@ -1,15 +1,31 @@ #!/usr/bin/env bash # To run this script from the terminal command line: ./CopyFilesToAddons.sh -# A Linux 'build' script. Run this after editing the addon's files. Then restart Anki. +# A Linux 'build' script. Run this after editing the addon's files. Then restart Anki. # Don't make edits over there in addons directly, as VCS won't see them, and an Anki or addon upgrade might destroy them. +# IMPORTANT: If you change the plugin package in manifest.json, make the same change to PLUGIN_NAME below +PLUGIN_NAME=FlashGrab + # Copy recursively (-r) all modified files and folders, except the excluded ones # (Like Anki's own deployment, this does NOT remove any files. The simplest way to do so manually here is to delete the addons folder, then re-run.) -mkdir -p ~/Anki/addons -rsync -r -u -v --exclude-from ExcludedFilesLinux.txt xml ~/Anki/addons/ -rsync -r -u -v --exclude-from ExcludedFilesLinux.txt xpath ~/Anki/addons/ -rsync -r -u -v --exclude-from ExcludedFilesLinux.txt distutils ~/Anki/addons/ -rsync -r -u -v --exclude-from ExcludedFilesLinux.txt syncxml ~/Anki/addons/ -cp -r -u -v syncx.py ~/Anki/addons/ +# Use XDG_DATA_HOME if present, otherwise default to ~/.local/share +DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}" +PLUGIN_HOME="${DATA_HOME}/Anki2/addons21/${PLUGIN_NAME}" + +mkdir -p "${PLUGIN_HOME}" +rsync -r -u -v --exclude-from ExcludedFilesLinux.txt xml "${PLUGIN_HOME}/" +rsync -r -u -v --exclude-from ExcludedFilesLinux.txt xpath "${PLUGIN_HOME}/" +rsync -r -u -v --exclude-from ExcludedFilesLinux.txt syncxml "${PLUGIN_HOME}/" +# No -r for syncxml since we don't want to include docsrc or samples directories +# syncxml samples dir should be at the top level of the plugin directory and only contain one file +mkdir -p "${PLUGIN_HOME}/samples" +mv "${PLUGIN_HOME}/syncxml/samples/lift-dictionary.apkg" "${PLUGIN_HOME}/samples/" +# Default config should also be in plugin root +mv "${PLUGIN_HOME}/syncxml/SyncFromXML_config_default.txt" "${PLUGIN_HOME}/" +# Rest of samples, as well as docsrc folder, not needed +rm -rf "${PLUGIN_HOME}/syncxml/samples" +rm -rf "${PLUGIN_HOME}/syncxml/docsrc" +cp -u -v __init__.py "${PLUGIN_HOME}/" +cp -u -v manifest.json "${PLUGIN_HOME}/"