├── README.md ├── SyncRedirector-legacy ├── config.json ├── SyncRedirector.ankiaddon ├── Utils.py ├── README.md ├── __init__.py └── SyncRedirector.py └── SyncRedirector ├── config.json └── __init__.py /README.md: -------------------------------------------------------------------------------- 1 | # anki-desktop-addons 2 | A common repository to store addons for all ankicommunity projects for Anki. 3 | -------------------------------------------------------------------------------- /SyncRedirector-legacy/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "syncUrl": "http://127.0.0.1:27701/sync", 3 | "msyncUrl": "http://127.0.0.1:27701/msync" 4 | } 5 | -------------------------------------------------------------------------------- /SyncRedirector-legacy/SyncRedirector.ankiaddon: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankicommunity/anki-desktop-addons/main/SyncRedirector-legacy/SyncRedirector.ankiaddon -------------------------------------------------------------------------------- /SyncRedirector-legacy/Utils.py: -------------------------------------------------------------------------------- 1 | def url_ending_with_slash(url): 2 | if url[-1] == "/": 3 | return url 4 | else: 5 | return url + "/" 6 | -------------------------------------------------------------------------------- /SyncRedirector/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "enabled": false, 3 | "addr": "http://localhost:27701/", 4 | "onlyselected": false, 5 | "selectedprofiles": [] 6 | } -------------------------------------------------------------------------------- /SyncRedirector-legacy/README.md: -------------------------------------------------------------------------------- 1 | # SyncRedirector 2 | 3 | Supports Anki Desktop version 2.1.19. 4 | 5 | ### Usage 6 | 7 | Please refer to [the guide on the wiki](https://ankicommunity.github.io/Supported%20Projects/SyncRedirector/). 8 | -------------------------------------------------------------------------------- /SyncRedirector-legacy/__init__.py: -------------------------------------------------------------------------------- 1 | from aqt import mw 2 | from .SyncRedirector import SyncRedirector 3 | 4 | config = mw.addonManager.getConfig(__name__) 5 | 6 | sync_redirector = SyncRedirector(config["syncUrl"], config["msyncUrl"]) 7 | 8 | SyncRedirector.print_introduction() 9 | sync_redirector.print_config() 10 | SyncRedirector.print_restart_warning() 11 | 12 | sync_redirector.reconfigure_anki_syncing() 13 | -------------------------------------------------------------------------------- /SyncRedirector-legacy/SyncRedirector.py: -------------------------------------------------------------------------------- 1 | # Based on the work of tsudoko and their contributors 2 | # https://github.com/tsudoko/anki-sync-server 3 | 4 | from .Utils import url_ending_with_slash 5 | import anki.sync 6 | import anki.hooks 7 | import aqt 8 | 9 | 10 | class SyncRedirector: 11 | def __init__(self, configured_sync_url, configured_msync_url): 12 | """ 13 | Expects urls ending with /sync and /msync or /sync/ and /msync/. 14 | """ 15 | self.configured_sync_url = configured_sync_url 16 | self.configured_msync_url = configured_msync_url 17 | 18 | def print_introduction(): 19 | print("== SyncRedirector Addon ==") 20 | 21 | def print_config(self): 22 | print("/sync URL: " + self.configured_sync_url) 23 | print("/msync URL: " + self.configured_msync_url) 24 | 25 | def print_restart_warning(): 26 | print( 27 | "WARNING: please restart Anki after configuring this addon in order to the changes take effect!" 28 | ) 29 | 30 | def reconfigure_anki_syncing(self): 31 | """ 32 | Expects urls ending with /sync and /msync or /sync/ and /msync/. 33 | """ 34 | print("Configuring Anki to sync against custom servers...") 35 | 36 | anki.hooks.addHook("profileLoaded", resetHostNum) 37 | anki.sync.SYNC_BASE = self._parse_sync_url_for_anki_desktop() 38 | anki.sync.SYNC_MEDIA_BASE = self._parse_msync_url_for_anki_desktop() 39 | 40 | def _parse_sync_url_for_anki_desktop(self): 41 | """ 42 | Expects url ending with /sync or /sync/ 43 | """ 44 | return url_ending_with_slash(self.configured_sync_url.strip("/sync")) + "%s" 45 | 46 | def _parse_msync_url_for_anki_desktop(self): 47 | """ 48 | Expects url ending with /msync or /msync/ 49 | """ 50 | return url_ending_with_slash(self.configured_msync_url) + "%s" 51 | 52 | 53 | def resetHostNum(): 54 | aqt.mw.pm.profile["hostNum"] = None 55 | -------------------------------------------------------------------------------- /SyncRedirector/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import aqt 4 | from anki.hooks import addHook, wrap 5 | from PyQt5.Qt import QAbstractItemView, QCheckBox, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, Qt 6 | 7 | DEFAULT_ADDR = "http://localhost:27701/" 8 | config = aqt.mw.addonManager.getConfig(__name__) 9 | 10 | # TODO: force the user to log out before changing any of the settings 11 | 12 | 13 | def addui(self, _): 14 | self = self.form 15 | parent_w = self.tab_2 16 | parent_l = self.vboxlayout 17 | self.useCustomServer = QCheckBox(parent_w) 18 | self.useCustomServer.setText("Use custom sync server") 19 | parent_l.addWidget(self.useCustomServer) 20 | cshl = QHBoxLayout() 21 | parent_l.addLayout(cshl) 22 | 23 | self.serverAddrLabel = QLabel(parent_w) 24 | self.serverAddrLabel.setText("Server address") 25 | cshl.addWidget(self.serverAddrLabel) 26 | self.customServerAddr = QLineEdit(parent_w) 27 | self.customServerAddr.setPlaceholderText(DEFAULT_ADDR) 28 | cshl.addWidget(self.customServerAddr) 29 | 30 | sphl = QHBoxLayout() 31 | parent_l.addLayout(sphl) 32 | 33 | self.onlySelected = QCheckBox(parent_w) 34 | self.onlySelected.setText("Only use custom sync server\nfor selected profiles") 35 | sphl.addWidget(self.onlySelected) 36 | 37 | self.selectedProfilesLabel = QLabel(parent_w) 38 | self.selectedProfilesLabel.setText("Selected profiles") 39 | sphl.addWidget(self.selectedProfilesLabel) 40 | self.selectedProfilesList = QListWidget(parent_w) 41 | for p in aqt.mw.pm.profiles(): 42 | item = QListWidgetItem(p) 43 | self.selectedProfilesList.addItem(item) 44 | if p in config["selectedprofiles"]: 45 | item.setSelected(True) 46 | self.selectedProfilesList.setFixedHeight( 47 | self.selectedProfilesList.sizeHintForRow(0) * self.selectedProfilesList.count() 48 | + 2 * self.selectedProfilesList.frameWidth() 49 | ) 50 | 51 | self.selectedProfilesList.setSelectionMode(QAbstractItemView.MultiSelection) 52 | sphl.addWidget(self.selectedProfilesList) 53 | 54 | if config["enabled"]: 55 | self.useCustomServer.setCheckState(Qt.Checked) 56 | if config["addr"]: 57 | self.customServerAddr.setText(config["addr"]) 58 | 59 | if config["onlyselected"]: 60 | self.onlySelected.setCheckState(Qt.Checked) 61 | self.customServerAddr.textChanged.connect(lambda text: updateserver(self, text)) 62 | 63 | def onchecked(state): 64 | config["enabled"] = state == Qt.Checked 65 | updateui(self, state) 66 | updateserver(self, self.customServerAddr.text()) 67 | 68 | self.useCustomServer.stateChanged.connect(onchecked) 69 | 70 | self.selectedProfilesList.itemClicked.connect(lambda _: updateprofiles(self)) 71 | 72 | def ononlyselectedchecked(state): 73 | config["onlyselected"] = state == Qt.Checked 74 | updateuiprofiles(self, state) 75 | updateprofiles(self) 76 | 77 | self.onlySelected.stateChanged.connect(ononlyselectedchecked) 78 | 79 | updateui(self, self.useCustomServer.checkState()) 80 | updateuiprofiles(self, self.onlySelected.checkState()) 81 | 82 | 83 | def updateserver(self, text): 84 | if config["enabled"]: 85 | addr = text or self.customServerAddr.placeholderText() 86 | config["addr"] = addr 87 | setserver() 88 | aqt.mw.addonManager.writeConfig(__name__, config) 89 | 90 | 91 | def updateprofiles(self): 92 | # For simplicity just flush the whole list rather than toggle the item 93 | config["selectedprofiles"] = [x.text() for x in self.selectedProfilesList.selectedItems()] 94 | aqt.mw.addonManager.writeConfig(__name__, config) 95 | 96 | 97 | def updateuiprofiles(self, state): 98 | self.selectedProfilesLabel.setEnabled(state == Qt.Checked and config["enabled"]) 99 | self.selectedProfilesList.setEnabled(state == Qt.Checked and config["enabled"]) 100 | 101 | 102 | def updateui(self, state): 103 | self.serverAddrLabel.setEnabled(state == Qt.Checked) 104 | self.customServerAddr.setEnabled(state == Qt.Checked) 105 | self.onlySelected.setEnabled(state == Qt.Checked) 106 | self.selectedProfilesLabel.setEnabled(state == Qt.Checked and config["onlyselected"]) 107 | self.selectedProfilesList.setEnabled(state == Qt.Checked and config["onlyselected"]) 108 | 109 | 110 | def setserver(): 111 | if config["enabled"] and (not config["onlyselected"] or aqt.mw.pm.name in config["selectedprofiles"]): 112 | os.environ["SYNC_ENDPOINT"] = config["addr"] + ("" if config["addr"][-1] == "/" else "/") + "sync/" 113 | os.environ["SYNC_ENDPOINT_MEDIA"] = config["addr"] + ("" if config["addr"][-1] == "/" else "/") + "msync/" 114 | 115 | 116 | addHook("profileLoaded", setserver) 117 | aqt.preferences.Preferences.__init__ = wrap(aqt.preferences.Preferences.__init__, addui, "after") 118 | --------------------------------------------------------------------------------