New upstream version 2.1.0+dfsg~rc2
Julian Gilbey
5 years ago
20 | 20 | |
21 | 21 | $ pip3 install -r requirements.txt |
22 | 22 | |
23 | You will also need PyQt development tools (specifically pyrcc5 and pyuic5). | |
24 | These are often contained in a separate package on Linux, such as | |
25 | 'pyqt5-dev-tools' on Debian/Ubuntu. | |
23 | If you're on a Linux distribution that packages PyQt 5.9 then you can use the | |
24 | distro's packages. Make sure you install the development tools (eg | |
25 | pyqt5-dev-tools) as well. | |
26 | ||
27 | If you're on another platform or your distro has the wrong Qt version, you | |
28 | can install PyQt with pip: | |
29 | ||
30 | $ pip3 install sip pyqt5==5.9 | |
26 | 31 | |
27 | 32 | To use the development version: |
28 | 33 | |
51 | 56 | |
52 | 57 | If you get any errors, please make sure you don't have an older version of |
53 | 58 | Anki installed in a system location. |
59 | ||
60 | To run the unit tests, you will need to install nose from your distro, or | |
61 | with pip: | |
62 | ||
63 | $ pip3 install nose | |
54 | 64 | |
55 | 65 | Before contributing code, please read README.contributing. |
56 | 66 |
9 | 9 | if sys.getfilesystemencoding().lower() in ("ascii", "ansi_x3.4-1968"): |
10 | 10 | raise Exception("Anki requires a UTF-8 locale.") |
11 | 11 | |
12 | version="2.1.0beta43" # build scripts grep this line, so preserve formatting | |
12 | version="2.1.0rc2" # build scripts grep this line, so preserve formatting | |
13 | 13 | from anki.storage import Collection |
14 | 14 | __all__ = ["Collection"] |
49 | 49 | SCHEMA_VERSION = 11 |
50 | 50 | SYNC_ZIP_SIZE = int(2.5*1024*1024) |
51 | 51 | SYNC_ZIP_COUNT = 25 |
52 | SYNC_BASE = "https://sync.ankiweb.net/" | |
53 | SYNC_MEDIA_BASE = "https://sync.ankiweb.net/msync/" | |
52 | SYNC_BASE = "https://sync%s.ankiweb.net/" | |
54 | 53 | SYNC_VER = 9 |
55 | 54 | |
56 | 55 | HELP_SITE="http://ankisrs.net/docs/manual.html" |
227 | 227 | parts = parts[:-1] |
228 | 228 | return "::".join(parts) |
229 | 229 | for deck in decks: |
230 | # if we've already seen the exact same deck name, remove the | |
230 | # if we've already seen the exact same deck name, rename the | |
231 | 231 | # invalid duplicate and reload |
232 | 232 | if deck['name'] in lims: |
233 | self.col.decks.rem(deck['id'], cardsToo=False, childrenToo=True) | |
233 | deck['name'] += "1" | |
234 | self.col.decks.save(deck) | |
234 | 235 | return self.deckDueList() |
235 | 236 | p = parent(deck['name']) |
236 | 237 | # new |
237 | 238 | nlim = self._deckNewLimitSingle(deck) |
238 | 239 | if p: |
239 | 240 | if p not in lims: |
240 | # if parent was missing, this deck is invalid, and we | |
241 | # need to reload the deck list | |
242 | self.col.decks.rem(deck['id'], cardsToo=False, childrenToo=True) | |
241 | # if parent was missing, this deck is invalid | |
242 | deck['name'] = "recovered" | |
243 | self.col.decks.save(deck) | |
243 | 244 | return self.deckDueList() |
244 | 245 | nlim = min(nlim, lims[p][0]) |
245 | 246 | new = self._newForDeck(deck['id'], nlim) |
217 | 217 | return "::".join(parts) |
218 | 218 | childMap = self.col.decks.childMap() |
219 | 219 | for deck in decks: |
220 | # if we've already seen the exact same deck name, remove the | |
220 | # if we've already seen the exact same deck name, rename the | |
221 | 221 | # invalid duplicate and reload |
222 | 222 | if deck['name'] in lims: |
223 | self.col.decks.rem(deck['id'], cardsToo=False, childrenToo=True) | |
223 | deck['name'] += "1" | |
224 | self.col.decks.save(deck) | |
224 | 225 | return self.deckDueList() |
225 | 226 | p = parent(deck['name']) |
226 | 227 | # new |
227 | 228 | nlim = self._deckNewLimitSingle(deck) |
228 | 229 | if p: |
229 | 230 | if p not in lims: |
230 | # if parent was missing, this deck is invalid, and we | |
231 | # need to reload the deck list | |
232 | self.col.decks.rem(deck['id'], cardsToo=False, childrenToo=True) | |
231 | # if parent was missing, this deck is invalid | |
232 | deck['name'] = "recovered" | |
233 | self.col.decks.save(deck) | |
233 | 234 | return self.deckDueList() |
234 | 235 | nlim = min(nlim, lims[p][0]) |
235 | 236 | new = self._newForDeck(deck['id'], nlim) |
583 | 584 | if delay is None: |
584 | 585 | delay = self._delayForGrade(conf, card.left) |
585 | 586 | |
586 | if card.due < time.time(): | |
587 | # not collapsed; add some randomness | |
588 | delay *= random.uniform(1, 1.25) | |
589 | 587 | card.due = int(time.time() + delay) |
590 | 588 | # due today? |
591 | 589 | if card.due < self.dayCutoff: |
590 | # add some randomness, up to 5 minutes or 25% | |
591 | maxExtra = min(300, int(delay*0.25)) | |
592 | fuzz = random.randrange(0, maxExtra) | |
593 | card.due = min(self.dayCutoff-1, card.due + fuzz) | |
592 | 594 | card.queue = 1 |
593 | 595 | if card.due < (intTime() + self.col.conf['collapseTime']): |
594 | 596 | self.lrnCount += 1 |
854 | 856 | return delay |
855 | 857 | |
856 | 858 | def _lapseIvl(self, card, conf): |
857 | due = card.odue or card.due | |
858 | elapsed = card.ivl - (due - self.today) | |
859 | ivl = min(elapsed, card.ivl) | |
860 | ivl = max(1, conf['minInt'], ivl*conf['mult']) | |
859 | ivl = max(1, conf['minInt'], card.ivl*conf['mult']) | |
861 | 860 | return ivl |
862 | 861 | |
863 | 862 | def _rescheduleRev(self, card, ease, early): |
1586 | 1585 | # remove review cards from relearning |
1587 | 1586 | self.col.db.execute(""" |
1588 | 1587 | update cards set |
1589 | due = odue, queue = 2, mod = %d, usn = %d, odue = 0 | |
1588 | due = odue, queue = 2, type = 2, mod = %d, usn = %d, odue = 0 | |
1590 | 1589 | where queue in (1,3) and type in (2, 3) |
1591 | 1590 | """ % (intTime(), self.col.usn())) |
1592 | 1591 | # remove new cards from learning |
101 | 101 | super().__init__(window_id=None, debug=False) |
102 | 102 | |
103 | 103 | def queueFile(self, file): |
104 | runHook("mpvWillPlay") | |
104 | runHook("mpvWillPlay", file) | |
105 | 105 | |
106 | 106 | path = os.path.join(os.getcwd(), file) |
107 | 107 | self.command("loadfile", path, "append-play") |
53 | 53 | rts = meta['ts'] |
54 | 54 | self.rmod = meta['mod'] |
55 | 55 | self.maxUsn = meta['usn'] |
56 | # this is a temporary measure to address the problem of users | |
57 | # forgetting which email address they've used - it will be removed | |
58 | # when enough time has passed | |
59 | 56 | self.uname = meta.get("uname", "") |
57 | self.hostNum = meta.get("hostNum") | |
60 | 58 | meta = self.meta() |
61 | 59 | self.col.log("lmeta", meta) |
62 | 60 | self.lmod = meta['mod'] |
77 | 75 | if not self.col.basicCheck(): |
78 | 76 | self.col.log("basic check") |
79 | 77 | return "basicCheckFailed" |
80 | # step 2: deletions | |
78 | # step 2: startup and deletions | |
81 | 79 | runHook("sync", "meta") |
82 | lrem = self.removed() | |
83 | rrem = self.server.start( | |
84 | minUsn=self.minUsn, lnewer=self.lnewer, graves=lrem) | |
80 | rrem = self.server.start(minUsn=self.minUsn, lnewer=self.lnewer) | |
81 | ||
82 | # apply deletions to server | |
83 | lgraves = self.removed() | |
84 | while lgraves: | |
85 | gchunk, lgraves = self._gravesChunk(lgraves) | |
86 | self.server.applyGraves(chunk=gchunk) | |
87 | ||
88 | # then apply server deletions here | |
85 | 89 | self.remove(rrem) |
90 | ||
86 | 91 | # ...and small objects |
87 | 92 | lchg = self.changes() |
88 | 93 | rchg = self.server.applyChanges(changes=lchg) |
121 | 126 | self.finish(mod) |
122 | 127 | return "success" |
123 | 128 | |
129 | def _gravesChunk(self, graves): | |
130 | lim = 250 | |
131 | chunk = dict(notes=[], cards=[], decks=[]) | |
132 | for cat in "notes", "cards", "decks": | |
133 | if lim and graves[cat]: | |
134 | chunk[cat] = graves[cat][:lim] | |
135 | graves[cat] = graves[cat][lim:] | |
136 | lim -= len(chunk[cat]) | |
137 | ||
138 | # anything remaining? | |
139 | if graves['notes'] or graves['cards'] or graves['decks']: | |
140 | return chunk, graves | |
141 | return chunk, None | |
142 | ||
124 | 143 | def meta(self): |
125 | 144 | return dict( |
126 | 145 | mod=self.col.mod, |
141 | 160 | d['conf'] = self.getConf() |
142 | 161 | d['crt'] = self.col.crt |
143 | 162 | return d |
144 | ||
145 | def applyChanges(self, changes): | |
146 | self.rchg = changes | |
147 | lchg = self.changes() | |
148 | # merge our side before returning | |
149 | self.mergeChanges(lchg, self.rchg) | |
150 | return lchg | |
151 | 163 | |
152 | 164 | def mergeChanges(self, lchg, rchg): |
153 | 165 | # then the other objects |
176 | 188 | return "tag had usn = -1" |
177 | 189 | found = False |
178 | 190 | for m in self.col.models.all(): |
179 | if self.col.server: | |
180 | # the web upgrade was mistakenly setting usn | |
181 | if m['usn'] < 0: | |
182 | m['usn'] = 0 | |
183 | found = True | |
184 | else: | |
185 | if m['usn'] == -1: | |
186 | return "model had usn = -1" | |
191 | if m['usn'] == -1: | |
192 | return "model had usn = -1" | |
187 | 193 | if found: |
188 | 194 | self.col.models.save() |
189 | 195 | self.col.sched.reset() |
201 | 207 | len(self.col.decks.allConf()), |
202 | 208 | ] |
203 | 209 | |
204 | def sanityCheck2(self, client): | |
205 | server = self.sanityCheck() | |
206 | if client != server: | |
207 | return dict(status="bad", c=client, s=server) | |
208 | return dict(status="ok") | |
209 | ||
210 | 210 | def usnLim(self): |
211 | if self.col.server: | |
212 | return "usn >= %d" % self.minUsn | |
213 | else: | |
214 | return "usn = -1" | |
211 | return "usn = -1" | |
215 | 212 | |
216 | 213 | def finish(self, mod=None): |
217 | if not mod: | |
218 | # server side; we decide new mod time | |
219 | mod = intTime(1000) | |
220 | 214 | self.col.ls = mod |
221 | 215 | self.col._usn = self.maxUsn + 1 |
222 | 216 | # ensure we save the mod time even if no changes made |
261 | 255 | # table is empty |
262 | 256 | self.tablesLeft.pop(0) |
263 | 257 | self.cursor = None |
264 | # if we're the client, mark the objects as having been sent | |
265 | if not self.col.server: | |
266 | self.col.db.execute( | |
267 | "update %s set usn=? where usn=-1"%curTable, | |
268 | self.maxUsn) | |
258 | # mark the objects as having been sent | |
259 | self.col.db.execute( | |
260 | "update %s set usn=? where usn=-1"%curTable, | |
261 | self.maxUsn) | |
269 | 262 | buf[curTable] = rows |
270 | 263 | lim -= fetched |
271 | 264 | if not self.tablesLeft: |
287 | 280 | cards = [] |
288 | 281 | notes = [] |
289 | 282 | decks = [] |
290 | if self.col.server: | |
291 | curs = self.col.db.execute( | |
292 | "select oid, type from graves where usn >= ?", self.minUsn) | |
293 | else: | |
294 | curs = self.col.db.execute( | |
295 | "select oid, type from graves where usn = -1") | |
283 | ||
284 | curs = self.col.db.execute( | |
285 | "select oid, type from graves where usn = -1") | |
286 | ||
296 | 287 | for oid, type in curs: |
297 | 288 | if type == REM_CARD: |
298 | 289 | cards.append(oid) |
300 | 291 | notes.append(oid) |
301 | 292 | else: |
302 | 293 | decks.append(oid) |
303 | if not self.col.server: | |
304 | self.col.db.execute("update graves set usn=? where usn=-1", | |
305 | self.maxUsn) | |
294 | ||
295 | self.col.db.execute("update graves set usn=? where usn=-1", | |
296 | self.maxUsn) | |
297 | ||
306 | 298 | return dict(cards=cards, notes=notes, decks=decks) |
307 | ||
308 | def start(self, minUsn, lnewer, graves): | |
309 | self.maxUsn = self.col._usn | |
310 | self.minUsn = minUsn | |
311 | self.lnewer = not lnewer | |
312 | lgraves = self.removed() | |
313 | self.remove(graves) | |
314 | return lgraves | |
315 | 299 | |
316 | 300 | def remove(self, graves): |
317 | 301 | # pretend to be the server so we don't set usn = -1 |
318 | wasServer = self.col.server | |
319 | 302 | self.col.server = True |
303 | ||
320 | 304 | # notes first, so we don't end up with duplicate graves |
321 | 305 | self.col._remNotes(graves['notes']) |
322 | 306 | # then cards |
324 | 308 | # and decks |
325 | 309 | for oid in graves['decks']: |
326 | 310 | self.col.decks.rem(oid, childrenToo=False) |
327 | self.col.server = wasServer | |
311 | ||
312 | self.col.server = False | |
328 | 313 | |
329 | 314 | # Models |
330 | 315 | ########################################################################## |
331 | 316 | |
332 | 317 | def getModels(self): |
333 | if self.col.server: | |
334 | return [m for m in self.col.models.all() if m['usn'] >= self.minUsn] | |
335 | else: | |
336 | mods = [m for m in self.col.models.all() if m['usn'] == -1] | |
337 | for m in mods: | |
338 | m['usn'] = self.maxUsn | |
339 | self.col.models.save() | |
340 | return mods | |
318 | mods = [m for m in self.col.models.all() if m['usn'] == -1] | |
319 | for m in mods: | |
320 | m['usn'] = self.maxUsn | |
321 | self.col.models.save() | |
322 | return mods | |
341 | 323 | |
342 | 324 | def mergeModels(self, rchg): |
343 | 325 | for r in rchg: |
350 | 332 | ########################################################################## |
351 | 333 | |
352 | 334 | def getDecks(self): |
353 | if self.col.server: | |
354 | return [ | |
355 | [g for g in self.col.decks.all() if g['usn'] >= self.minUsn], | |
356 | [g for g in self.col.decks.allConf() if g['usn'] >= self.minUsn] | |
357 | ] | |
358 | else: | |
359 | decks = [g for g in self.col.decks.all() if g['usn'] == -1] | |
360 | for g in decks: | |
361 | g['usn'] = self.maxUsn | |
362 | dconf = [g for g in self.col.decks.allConf() if g['usn'] == -1] | |
363 | for g in dconf: | |
364 | g['usn'] = self.maxUsn | |
365 | self.col.decks.save() | |
366 | return [decks, dconf] | |
335 | decks = [g for g in self.col.decks.all() if g['usn'] == -1] | |
336 | for g in decks: | |
337 | g['usn'] = self.maxUsn | |
338 | dconf = [g for g in self.col.decks.allConf() if g['usn'] == -1] | |
339 | for g in dconf: | |
340 | g['usn'] = self.maxUsn | |
341 | self.col.decks.save() | |
342 | return [decks, dconf] | |
367 | 343 | |
368 | 344 | def mergeDecks(self, rchg): |
369 | 345 | for r in rchg[0]: |
388 | 364 | ########################################################################## |
389 | 365 | |
390 | 366 | def getTags(self): |
391 | if self.col.server: | |
392 | return [t for t, usn in self.col.tags.allItems() | |
393 | if usn >= self.minUsn] | |
394 | else: | |
395 | tags = [] | |
396 | for t, usn in self.col.tags.allItems(): | |
397 | if usn == -1: | |
398 | self.col.tags.tags[t] = self.maxUsn | |
399 | tags.append(t) | |
400 | self.col.tags.save() | |
401 | return tags | |
367 | tags = [] | |
368 | for t, usn in self.col.tags.allItems(): | |
369 | if usn == -1: | |
370 | self.col.tags.tags[t] = self.maxUsn | |
371 | tags.append(t) | |
372 | self.col.tags.save() | |
373 | return tags | |
402 | 374 | |
403 | 375 | def mergeTags(self, tags): |
404 | 376 | self.col.tags.register(tags, usn=self.maxUsn) |
447 | 419 | def mergeConf(self, conf): |
448 | 420 | self.col.conf = conf |
449 | 421 | |
450 | # Local syncing for unit tests | |
451 | ########################################################################## | |
452 | ||
453 | class LocalServer(Syncer): | |
454 | ||
455 | # serialize/deserialize payload, so we don't end up sharing objects | |
456 | # between cols | |
457 | def applyChanges(self, changes): | |
458 | l = json.loads; d = json.dumps | |
459 | return l(d(Syncer.applyChanges(self, l(d(changes))))) | |
460 | ||
461 | 422 | # Wrapper for requests that tracks upload/download progress |
462 | 423 | ########################################################################## |
463 | 424 | |
511 | 472 | |
512 | 473 | class HttpSyncer: |
513 | 474 | |
514 | def __init__(self, hkey=None, client=None): | |
475 | def __init__(self, hkey=None, client=None, hostNum=None): | |
515 | 476 | self.hkey = hkey |
516 | 477 | self.skey = checksum(str(random.random()))[:8] |
517 | 478 | self.client = client or AnkiRequestsClient() |
518 | 479 | self.postVars = {} |
480 | self.hostNum = hostNum | |
481 | self.prefix = "sync/" | |
482 | ||
483 | def syncURL(self): | |
484 | if devMode: | |
485 | url = "https://l1sync.ankiweb.net/" | |
486 | else: | |
487 | url = SYNC_BASE % (self.hostNum or "") | |
488 | return url + self.prefix | |
519 | 489 | |
520 | 490 | def assertOk(self, resp): |
521 | 491 | # not using raise_for_status() as aqt expects this error msg |
591 | 561 | |
592 | 562 | class RemoteServer(HttpSyncer): |
593 | 563 | |
594 | def __init__(self, hkey): | |
595 | HttpSyncer.__init__(self, hkey) | |
596 | ||
597 | def syncURL(self): | |
598 | if devMode: | |
599 | return "https://l1sync.ankiweb.net/sync/" | |
600 | return SYNC_BASE + "sync/" | |
564 | def __init__(self, hkey, hostNum): | |
565 | HttpSyncer.__init__(self, hkey, hostNum=hostNum) | |
601 | 566 | |
602 | 567 | def hostKey(self, user, pw): |
603 | 568 | "Returns hkey or none if user/pw incorrect." |
625 | 590 | return |
626 | 591 | return json.loads(ret.decode("utf8")) |
627 | 592 | |
593 | def applyGraves(self, **kw): | |
594 | return self._run("applyGraves", kw) | |
595 | ||
628 | 596 | def applyChanges(self, **kw): |
629 | 597 | return self._run("applyChanges", kw) |
630 | 598 | |
655 | 623 | |
656 | 624 | class FullSyncer(HttpSyncer): |
657 | 625 | |
658 | def __init__(self, col, hkey, client): | |
659 | HttpSyncer.__init__(self, hkey, client) | |
626 | def __init__(self, col, hkey, client, hostNum): | |
627 | HttpSyncer.__init__(self, hkey, client, hostNum=hostNum) | |
660 | 628 | self.postVars = dict( |
661 | 629 | k=self.hkey, |
662 | 630 | v="ankidesktop,%s,%s"%(anki.version, platDesc()), |
663 | 631 | ) |
664 | 632 | self.col = col |
665 | ||
666 | def syncURL(self): | |
667 | if devMode: | |
668 | return "https://l1.ankiweb.net/sync/" | |
669 | return SYNC_BASE + "sync/" | |
670 | 633 | |
671 | 634 | def download(self): |
672 | 635 | runHook("sync", "download") |
846 | 809 | |
847 | 810 | class RemoteMediaServer(HttpSyncer): |
848 | 811 | |
849 | def __init__(self, col, hkey, client): | |
812 | def __init__(self, col, hkey, client, hostNum): | |
850 | 813 | self.col = col |
851 | HttpSyncer.__init__(self, hkey, client) | |
852 | ||
853 | def syncURL(self): | |
854 | if devMode: | |
855 | return "https://l1.ankiweb.net/msync/" | |
856 | return SYNC_MEDIA_BASE | |
814 | HttpSyncer.__init__(self, hkey, client, hostNum=hostNum) | |
815 | self.prefix = "msync/" | |
857 | 816 | |
858 | 817 | def begin(self): |
859 | 818 | self.postVars = dict( |
222 | 222 | parser.add_option("-b", "--base", help="path to base folder") |
223 | 223 | parser.add_option("-p", "--profile", help="profile name to load") |
224 | 224 | parser.add_option("-l", "--lang", help="interface language (en, de, etc)") |
225 | if not isMac: | |
226 | parser.add_option("--hwaccel", action="store_true", help="enable hardware acceleration") | |
225 | 227 | return parser.parse_args(argv[1:]) |
226 | 228 | |
227 | 229 | def run(): |
251 | 253 | opts, args = parseArgs(argv) |
252 | 254 | opts.base = opts.base or "" |
253 | 255 | opts.profile = opts.profile or "" |
256 | ||
257 | if not isMac and not opts.hwaccel: | |
258 | print("Hardware acceleration disabled.") | |
259 | if isWin: | |
260 | os.environ["QT_OPENGL"] = "software" | |
261 | else: | |
262 | os.environ["QT_XCB_FORCE_SOFTWARE_OPENGL"] = "1" | |
254 | 263 | |
255 | 264 | # work around pyqt loading wrong GL library |
256 | 265 | if isLin: |
292 | 301 | # remaining pm init |
293 | 302 | pm.ensureProfile() |
294 | 303 | |
295 | print("This is an BETA build - please do not package it up for Linux distributions") | |
296 | ||
297 | 304 | # load the main window |
298 | 305 | import aqt.main |
299 | 306 | mw = aqt.main.AnkiQt(app, pm, opts, args) |
203 | 203 | ###################################################################### |
204 | 204 | |
205 | 205 | _configButtonActions = {} |
206 | _configUpdatedActions = {} | |
206 | 207 | |
207 | 208 | def addonConfigDefaults(self, dir): |
208 | 209 | path = os.path.join(self.addonsFolder(dir), "config.json") |
225 | 226 | |
226 | 227 | def configAction(self, addon): |
227 | 228 | return self._configButtonActions.get(addon) |
229 | ||
230 | def configUpdatedAction(self, addon): | |
231 | return self._configUpdatedActions.get(addon) | |
228 | 232 | |
229 | 233 | # Add-on Config API |
230 | 234 | ###################################################################### |
244 | 248 | def setConfigAction(self, module, fn): |
245 | 249 | addon = self.addonFromModule(module) |
246 | 250 | self._configButtonActions[addon] = fn |
251 | ||
252 | def setConfigUpdatedAction(self, module, fn): | |
253 | addon = self.addonFromModule(module) | |
254 | self._configUpdatedActions[addon] = fn | |
247 | 255 | |
248 | 256 | def writeConfig(self, module, conf): |
249 | 257 | addon = self.addonFromModule(module) |
456 | 464 | restore = self.form.buttonBox.button(QDialogButtonBox.RestoreDefaults) |
457 | 465 | restore.clicked.connect(self.onRestoreDefaults) |
458 | 466 | self.updateHelp() |
459 | self.updateText() | |
467 | self.updateText(self.conf) | |
460 | 468 | self.show() |
461 | 469 | |
462 | 470 | def onRestoreDefaults(self): |
463 | self.conf = self.mgr.addonConfigDefaults(self.addon) | |
464 | self.updateText() | |
471 | default_conf = self.mgr.addonConfigDefaults(self.addon) | |
472 | self.updateText(default_conf) | |
465 | 473 | |
466 | 474 | def updateHelp(self): |
467 | 475 | txt = self.mgr.addonConfigHelp(self.addon) |
470 | 478 | else: |
471 | 479 | self.form.scrollArea.setVisible(False) |
472 | 480 | |
473 | def updateText(self): | |
481 | def updateText(self, conf): | |
474 | 482 | self.form.editor.setPlainText( |
475 | json.dumps(self.conf,sort_keys=True,indent=4, separators=(',', ': '))) | |
483 | json.dumps(conf,sort_keys=True,indent=4, separators=(',', ': '))) | |
476 | 484 | |
477 | 485 | def accept(self): |
478 | 486 | txt = self.form.editor.toPlainText() |
479 | 487 | try: |
480 | self.conf = json.loads(txt) | |
488 | new_conf = json.loads(txt) | |
481 | 489 | except Exception as e: |
482 | 490 | showInfo(_("Invalid configuration: ") + repr(e)) |
483 | 491 | return |
484 | 492 | |
485 | self.mgr.writeConfig(self.addon, self.conf) | |
493 | if new_conf != self.conf: | |
494 | self.mgr.writeConfig(self.addon, new_conf) | |
495 | # does the add-on define an action to be fired? | |
496 | act = self.mgr.configUpdatedAction(self.addon) | |
497 | if act: | |
498 | act(new_conf) | |
499 | ||
486 | 500 | super().accept() |
460 | 460 | m.addSeparator() |
461 | 461 | for act in self.form.menu_Notes.actions(): |
462 | 462 | m.addAction(act) |
463 | runHook("browser.onContextMenu", self, m) | |
463 | 464 | m.exec_(QCursor.pos()) |
464 | 465 | |
465 | 466 | def updateFont(self): |
555 | 556 | self.form.searchEdit.lineEdit().setText("deck:current ") |
556 | 557 | |
557 | 558 | # update history |
558 | txt = str(self.form.searchEdit.lineEdit().text()).strip() | |
559 | txt = str(self.form.searchEdit.lineEdit().text()) | |
559 | 560 | sh = self.mw.pm.profile['searchHistory'] |
560 | 561 | if txt in sh: |
561 | 562 | sh.remove(txt) |
575 | 576 | if "is:current" in self._lastSearchTxt: |
576 | 577 | # show current card if there is one |
577 | 578 | c = self.mw.reviewer.card |
579 | self.card = self.mw.reviewer.card | |
578 | 580 | nid = c and c.nid or 0 |
579 | 581 | self.model.search("nid:%d"%nid) |
580 | 582 | else: |
733 | 735 | self.model.beginReset() |
734 | 736 | if type in self.model.activeCols: |
735 | 737 | if len(self.model.activeCols) < 2: |
738 | self.model.endReset() | |
736 | 739 | return showInfo(_("You must have at least one column.")) |
737 | 740 | self.model.activeCols.remove(type) |
738 | 741 | adding=False |
1707 | 1710 | "%(a)d of %(b)d notes updated", len(sf)) % { |
1708 | 1711 | 'a': changed, |
1709 | 1712 | 'b': len(sf), |
1710 | }) | |
1713 | }, parent=self) | |
1711 | 1714 | |
1712 | 1715 | def onFindReplaceHelp(self): |
1713 | 1716 | openHelp("findreplace") |
21 | 21 | self.form.buttonBox.button(QDialogButtonBox.Close).setShortcut( |
22 | 22 | QKeySequence("Ctrl+Return")) |
23 | 23 | self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self) |
24 | self.editor.card = self.mw.reviewer.card | |
24 | 25 | self.editor.setNote(self.mw.reviewer.card.note(), focusTo=0) |
25 | 26 | restoreGeom(self, "editcurrent") |
26 | 27 | addHook("reset", self.onReset) |
6 | 6 | from aqt.qt import * |
7 | 7 | import aqt |
8 | 8 | from aqt.utils import getSaveFile, tooltip, showWarning, askUser, \ |
9 | checkInvalidFilename | |
9 | checkInvalidFilename, showInfo | |
10 | 10 | from anki.exporting import exporters |
11 | 11 | from anki.hooks import addHook, remHook |
12 | 12 | from anki.lang import ngettext |
84 | 84 | deck_name = self.decks[self.frm.deck.currentIndex()] |
85 | 85 | deck_name = re.sub('[\\\\/?<>:*|"^]', '_', deck_name) |
86 | 86 | |
87 | if not self.isVerbatim and self.isApkg and self.exporter.includeSched: | |
88 | showInfo("Please switch to the regular scheduler before exporting a single deck .apkg with scheduling.") | |
89 | return | |
90 | ||
87 | 91 | filename = '{0}{1}'.format(deck_name, self.exporter.ext) |
88 | 92 | while 1: |
89 | 93 | file = getSaveFile(self, _("Export"), "export", |
264 | 264 | self.menuFlag.setTitle(_("Flag")) |
265 | 265 | self.menu_Notes.setTitle(_("&Notes")) |
266 | 266 | self.actionReschedule.setText(_("&Reschedule...")) |
267 | self.actionReschedule.setShortcut(_("Ctrl+Alt+R")) | |
267 | 268 | self.actionSelectAll.setText(_("Select &All")) |
268 | 269 | self.actionSelectAll.setShortcut(_("Ctrl+Alt+A")) |
269 | 270 | self.actionUndo.setText(_("&Undo")) |
270 | 271 | self.actionUndo.setShortcut(_("Ctrl+Z")) |
271 | 272 | self.actionInvertSelection.setText(_("&Invert Selection")) |
273 | self.actionInvertSelection.setShortcut(_("Ctrl+Alt+S")) | |
272 | 274 | self.actionFind.setText(_("&Find")) |
273 | 275 | self.actionFind.setShortcut(_("Ctrl+F")) |
274 | 276 | self.actionNote.setText(_("N&ote")) |
8 | 8 | from PyQt5 import QtCore |
9 | 9 | |
10 | 10 | qt_resource_data = b"\ |
11 | \x00\x00\x02\xd7\ | |
12 | \x89\ | |
13 | \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ | |
14 | \x00\x00\x3c\x00\x00\x00\x3c\x08\x06\x00\x00\x00\x3a\xfc\xd9\x72\ | |
15 | \x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\x74\x77\x61\x72\x65\ | |
16 | \x00\x41\x64\x6f\x62\x65\x20\x49\x6d\x61\x67\x65\x52\x65\x61\x64\ | |
17 | \x79\x71\xc9\x65\x3c\x00\x00\x02\x79\x49\x44\x41\x54\x78\xda\xec\ | |
18 | \x9a\xd1\x6d\xc2\x30\x10\x40\x0d\x62\x80\x6c\x40\x3a\x01\xde\xa0\ | |
19 | \x19\x21\x23\xa4\x13\xc0\x06\xf5\x06\xa8\x13\xa4\x1b\xa4\x1b\x44\ | |
20 | \x4c\x10\x98\x20\x6c\x10\x36\x68\x8d\xe4\x48\xc8\x75\x82\xef\xb0\ | |
21 | \x0f\x87\xf8\xa4\xfb\x31\x89\xc3\xf3\x9d\xed\xbb\xb3\x17\x8c\x56\ | |
22 | \xb8\xd4\x5c\xea\x46\x6a\xa2\xda\x2e\x52\x4f\x52\x7f\xa4\x1e\xd9\ | |
23 | \x8b\xc8\x15\xb2\x91\xfa\x7b\x47\x5b\xf5\xec\x64\xe5\x6a\xc5\xca\ | |
24 | \x02\x54\xd7\xea\xc6\x03\x26\x05\xdb\x20\x60\x7b\x6d\xd4\x14\x98\ | |
25 | \x05\x6c\xaf\xdd\x54\x2c\xed\x02\xf6\xd6\xd2\x41\x8b\x70\x08\xdb\ | |
26 | \xab\x08\x15\x36\xf5\x00\xdb\x6b\x1a\x22\x70\xe9\x11\xb8\x9e\x93\ | |
27 | \x75\x9d\x59\x79\xe9\x10\x78\x4b\x30\xa8\xdb\x90\x2c\xdc\x12\x58\ | |
28 | \xb8\x0b\x05\x96\x13\xc0\xf6\xca\x43\x70\xe9\x8c\x38\x2e\x47\xcb\ | |
29 | \xea\x0e\x04\x57\x19\xcc\x59\xe9\x90\x6c\x08\x81\x37\x3e\x81\x3f\ | |
30 | \xb5\xb6\x2b\xfc\x97\xd4\x6f\xc3\x0a\x4d\xb9\x1b\xe8\x61\x6c\xae\ | |
31 | \xda\xfb\xb4\x93\xab\xb4\xf3\x0d\x02\x3c\x34\x57\xd3\x27\xbb\x34\ | |
32 | \x1f\xd8\xff\x4d\xf1\x3c\x68\x0e\xbf\x0f\xb4\x87\x96\xa4\x5f\x7c\ | |
33 | \x2f\x5a\x17\x36\x61\x59\xb2\x99\xc9\x12\x61\xc9\xf4\x55\x81\x4f\ | |
34 | \x13\x01\x4e\x7c\xbb\xf4\xfa\xc9\x0b\xd9\xd1\x62\xd5\x1e\xfc\x4f\ | |
35 | \x4b\x40\xc7\x63\x1f\x38\x3f\x71\x55\xe6\x90\x29\x89\x05\x4e\x2c\ | |
36 | \xdd\xdf\x87\x1c\x2c\x23\xaf\x03\xa6\xf3\x6e\x20\x80\xcf\x0d\x81\ | |
37 | \x07\x55\xf2\x90\x21\xff\xa3\x95\x0c\xd5\x94\x4b\xc0\xe0\xf8\x4c\ | |
38 | \x0f\xf3\x91\x67\x51\xd5\xce\x02\xd0\x61\x49\x00\x5c\x5a\x7e\x13\ | |
39 | \x5d\xe9\x4c\x98\x7d\x25\x91\x13\xbb\xf3\x58\x49\x69\xf7\xc8\x22\ | |
40 | \x51\x31\xfb\x22\x79\x4d\x58\xc4\x2b\x7d\xd5\xbe\x32\x80\x95\xb3\ | |
41 | \x00\xac\x5b\xfa\xae\x57\x71\x82\xb9\x5c\x01\x3c\xc9\x49\xaa\x3a\ | |
42 | \xb6\x1a\x36\x9a\x6b\x27\x8e\x0b\x7a\xad\xd6\xbf\x60\x44\xb5\xeb\ | |
43 | \x1a\xe0\x46\xdc\xd1\x36\xd5\x69\x1e\x94\xfb\x2c\xf0\x41\x2b\x93\ | |
44 | \xae\xa1\x75\xd8\x7b\xfd\xed\x7d\x84\x75\x02\x01\x8d\x39\x49\x6c\ | |
45 | \x80\xb0\x2d\xf3\x78\xac\x5a\x33\xd8\xe9\x7d\xa2\x06\xaa\xb3\xb4\ | |
46 | \xaa\xd0\xde\x2f\x98\xe7\x5a\xb5\x4d\x30\xd2\x58\x2c\x34\x99\xe1\ | |
47 | \xbd\x42\x0d\x48\xab\x3d\x5b\xa9\xdf\xf4\x81\xb2\xb9\x2e\x51\x50\ | |
48 | \x55\x0e\x3b\xcb\xed\x04\x13\x04\xec\x2c\xfb\x17\x94\x95\x06\xc8\ | |
49 | \xa2\x54\x1b\x2c\x68\xea\x6f\x0f\xe8\x13\x15\x60\x2c\x1e\x84\x4e\ | |
50 | \x95\x15\x21\x73\xe8\x6c\x28\x18\x40\x83\x85\x0f\xf6\xff\x30\x80\ | |
51 | \xb4\xa6\x84\xb9\x9a\x84\xdd\xaa\x82\xb9\xc7\xb5\x63\x7e\x73\xe2\ | |
52 | \x9a\x05\x78\xa3\x27\xf1\x10\x4b\x4f\xe2\x76\x5e\xaa\xc0\x1f\xb1\ | |
53 | \x78\x43\xb5\xe5\xb8\xb6\x78\xa1\xe0\x5b\x4b\x6b\x0a\x36\xa1\x1b\ | |
54 | \x78\xd8\xbc\x9a\x6c\x5f\x8d\x67\x4b\x11\x38\x02\x47\xe0\x08\x1c\ | |
55 | \x81\x03\x92\x15\xf2\x3d\x8e\x88\x6b\x6d\x82\x89\x35\x22\x73\x32\ | |
56 | \x65\x5f\xe4\x65\x1e\x4a\x15\xd1\xa5\x23\x70\x04\x9e\x8f\xfc\x09\ | |
57 | \x30\x00\xa0\x1c\x74\x67\x26\xea\x15\x76\x00\x00\x00\x00\x49\x45\ | |
58 | \x4e\x44\xae\x42\x60\x82\ | |
59 | 11 | \x00\x00\x06\xb8\ |
60 | 12 | \x89\ |
61 | 13 | \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ |
166 | 118 | \xf0\x09\xd0\xde\xc7\xe9\xb6\x11\x7f\x4b\x80\x25\xa0\xdc\xca\x39\ |
167 | 119 | \xc0\xff\x00\x27\xf2\xcd\xbe\x4f\x7b\xc5\xe3\x00\x00\x00\x00\x49\ |
168 | 120 | \x45\x4e\x44\xae\x42\x60\x82\ |
169 | \x00\x00\x04\x30\ | |
121 | \x00\x00\x04\x06\ | |
170 | 122 | \x3c\ |
171 | 123 | \x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ |
172 | 124 | \x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ |
196 | 148 | \x75\x6e\x64\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6d\x69\x74\x65\x72\ |
197 | 149 | \x6c\x69\x6d\x69\x74\x3a\x31\x2e\x35\x3b\x22\x3e\x0a\x20\x20\x20\ |
198 | 150 | \x20\x3c\x67\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x6d\ |
199 | \x61\x74\x72\x69\x78\x28\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x2d\x31\ | |
200 | \x34\x30\x2c\x30\x29\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\ | |
201 | \x3c\x67\x20\x69\x64\x3d\x22\x74\x61\x67\x22\x20\x74\x72\x61\x6e\ | |
202 | \x73\x66\x6f\x72\x6d\x3d\x22\x6d\x61\x74\x72\x69\x78\x28\x31\x2c\ | |
203 | \x30\x2c\x30\x2c\x31\x2c\x2d\x37\x34\x36\x2c\x30\x29\x22\x3e\x0a\ | |
204 | \x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x72\x65\x63\ | |
205 | \x74\x20\x78\x3d\x22\x38\x38\x36\x22\x20\x79\x3d\x22\x30\x22\x20\ | |
206 | \x77\x69\x64\x74\x68\x3d\x22\x36\x30\x22\x20\x68\x65\x69\x67\x68\ | |
207 | \x74\x3d\x22\x36\x30\x22\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\ | |
208 | \x6c\x6c\x3a\x6e\x6f\x6e\x65\x3b\x22\x2f\x3e\x0a\x20\x20\x20\x20\ | |
209 | \x20\x20\x20\x20\x20\x20\x20\x20\x3c\x67\x20\x74\x72\x61\x6e\x73\ | |
210 | \x66\x6f\x72\x6d\x3d\x22\x6d\x61\x74\x72\x69\x78\x28\x31\x2e\x30\ | |
211 | \x32\x36\x31\x33\x2c\x30\x2c\x30\x2c\x31\x2e\x32\x35\x39\x32\x36\ | |
212 | \x2c\x35\x32\x36\x2e\x35\x38\x33\x2c\x2d\x38\x2e\x34\x30\x37\x34\ | |
213 | \x31\x29\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\ | |
214 | \x20\x20\x20\x20\x20\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x33\ | |
215 | \x35\x34\x2c\x31\x37\x4c\x33\x38\x38\x2c\x31\x37\x4c\x34\x30\x35\ | |
216 | \x2e\x30\x30\x31\x2c\x32\x33\x2e\x37\x35\x4c\x34\x30\x35\x2e\x30\ | |
217 | \x30\x31\x2c\x33\x37\x2e\x32\x35\x31\x4c\x33\x38\x38\x2c\x34\x34\ | |
218 | \x4c\x33\x35\x34\x2c\x34\x34\x4c\x33\x35\x34\x2c\x31\x37\x5a\x22\ | |
151 | \x61\x74\x72\x69\x78\x28\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x2d\x32\ | |
152 | \x38\x30\x2c\x30\x29\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\ | |
153 | \x3c\x67\x20\x69\x64\x3d\x22\x68\x65\x61\x72\x74\x22\x20\x74\x72\ | |
154 | \x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x6d\x61\x74\x72\x69\x78\x28\ | |
155 | \x31\x2c\x30\x2c\x30\x2c\x31\x2c\x2d\x31\x34\x31\x31\x2c\x30\x29\ | |
156 | \x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\ | |
157 | \x72\x65\x63\x74\x20\x78\x3d\x22\x31\x36\x39\x31\x22\x20\x79\x3d\ | |
158 | \x22\x30\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x36\x30\x22\x20\x68\ | |
159 | \x65\x69\x67\x68\x74\x3d\x22\x36\x30\x22\x20\x73\x74\x79\x6c\x65\ | |
160 | \x3d\x22\x66\x69\x6c\x6c\x3a\x6e\x6f\x6e\x65\x3b\x22\x2f\x3e\x0a\ | |
161 | \x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x67\x20\x74\ | |
162 | \x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x6d\x61\x74\x72\x69\x78\ | |
163 | \x28\x30\x2e\x39\x36\x30\x32\x34\x31\x2c\x30\x2c\x30\x2c\x30\x2e\ | |
164 | \x39\x36\x30\x32\x34\x31\x2c\x31\x34\x33\x31\x2e\x30\x31\x2c\x33\ | |
165 | \x2e\x31\x34\x37\x30\x31\x29\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\ | |
166 | \x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x70\x61\x74\x68\x20\ | |
167 | \x64\x3d\x22\x4d\x33\x30\x32\x2c\x31\x30\x2e\x38\x43\x33\x30\x37\ | |
168 | \x2e\x36\x38\x34\x2c\x30\x20\x33\x31\x39\x2e\x30\x35\x33\x2c\x30\ | |
169 | \x20\x33\x32\x34\x2e\x37\x33\x37\x2c\x35\x2e\x34\x43\x33\x33\x30\ | |
170 | \x2e\x34\x32\x31\x2c\x31\x30\x2e\x38\x20\x33\x33\x30\x2e\x34\x32\ | |
171 | \x31\x2c\x32\x31\x2e\x36\x20\x33\x32\x34\x2e\x37\x33\x37\x2c\x33\ | |
172 | \x32\x2e\x34\x43\x33\x32\x30\x2e\x37\x35\x38\x2c\x34\x30\x2e\x35\ | |
173 | \x20\x33\x31\x30\x2e\x35\x32\x36\x2c\x34\x38\x2e\x36\x20\x33\x30\ | |
174 | \x32\x2c\x35\x34\x43\x32\x39\x33\x2e\x34\x37\x34\x2c\x34\x38\x2e\ | |
175 | \x36\x20\x32\x38\x33\x2e\x32\x34\x32\x2c\x34\x30\x2e\x35\x20\x32\ | |
176 | \x37\x39\x2e\x32\x36\x33\x2c\x33\x32\x2e\x34\x43\x32\x37\x33\x2e\ | |
177 | \x35\x37\x39\x2c\x32\x31\x2e\x36\x20\x32\x37\x33\x2e\x35\x37\x39\ | |
178 | \x2c\x31\x30\x2e\x38\x20\x32\x37\x39\x2e\x32\x36\x33\x2c\x35\x2e\ | |
179 | \x34\x43\x32\x38\x34\x2e\x39\x34\x37\x2c\x30\x20\x32\x39\x36\x2e\ | |
180 | \x33\x31\x36\x2c\x30\x20\x33\x30\x32\x2c\x31\x30\x2e\x38\x5a\x22\ | |
219 | 181 | \x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\x6c\x6c\x3a\x6e\x6f\x6e\ |
220 | 182 | \x65\x3b\x73\x74\x72\x6f\x6b\x65\x3a\x62\x6c\x61\x63\x6b\x3b\x73\ |
221 | \x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\x3a\x32\x2e\x31\x38\ | |
183 | \x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\x3a\x32\x2e\x38\x32\ | |
222 | 184 | \x70\x78\x3b\x22\x2f\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\ |
223 | \x20\x20\x20\x20\x20\x20\x20\x3c\x67\x20\x74\x72\x61\x6e\x73\x66\ | |
224 | \x6f\x72\x6d\x3d\x22\x6d\x61\x74\x72\x69\x78\x28\x30\x2e\x38\x39\ | |
225 | \x36\x31\x32\x38\x2c\x30\x2c\x30\x2c\x30\x2e\x38\x39\x36\x31\x32\ | |
226 | \x38\x2c\x33\x39\x2e\x33\x30\x39\x32\x2c\x33\x2e\x31\x36\x37\x34\ | |
227 | \x38\x29\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\ | |
228 | \x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x63\x69\x72\x63\x6c\x65\ | |
229 | \x20\x63\x78\x3d\x22\x34\x30\x30\x22\x20\x63\x79\x3d\x22\x33\x30\ | |
230 | \x2e\x35\x30\x31\x22\x20\x72\x3d\x22\x32\x2e\x34\x39\x39\x22\x20\ | |
231 | \x73\x74\x79\x6c\x65\x3d\x22\x73\x74\x72\x6f\x6b\x65\x3a\x62\x6c\ | |
232 | \x61\x63\x6b\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\ | |
233 | \x3a\x32\x2e\x36\x33\x70\x78\x3b\x22\x2f\x3e\x0a\x20\x20\x20\x20\ | |
234 | \x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x67\x3e\ | |
235 | \x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x67\ | |
236 | \x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x67\x3e\x0a\x20\ | |
237 | \x20\x20\x20\x3c\x2f\x67\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\x0a\ | |
185 | \x20\x20\x20\x3c\x2f\x67\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\ | |
186 | \x3c\x2f\x67\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x67\x3e\x0a\x3c\x2f\ | |
187 | \x73\x76\x67\x3e\x0a\ | |
238 | 188 | \x00\x00\x05\x69\ |
239 | 189 | \x3c\ |
240 | 190 | \x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ |
324 | 274 | \x20\x20\x20\x20\x20\x20\x3c\x2f\x67\x3e\x0a\x20\x20\x20\x20\x20\ |
325 | 275 | \x20\x20\x20\x3c\x2f\x67\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x67\x3e\ |
326 | 276 | \x0a\x3c\x2f\x73\x76\x67\x3e\x0a\ |
327 | \x00\x00\x02\x68\ | |
328 | \x00\ | |
329 | \x00\x10\x25\x78\x9c\xed\x97\x5b\x6f\xd3\x30\x14\xc7\xdf\xf7\x29\ | |
330 | \x8c\x25\x24\x90\x52\x5f\x13\x3b\xce\x9a\x4d\xda\x85\x09\x69\xc0\ | |
331 | \x24\x36\x10\xbc\x85\xc4\x6b\xcd\xd2\x24\x4a\xb2\xb6\xfb\xf6\x9c\ | |
332 | \x64\xed\xb4\x6e\xd5\x40\x88\xf1\xb2\xb4\x55\xeb\x73\x72\x7c\x2e\ | |
333 | \x3f\xfb\xff\xd0\xf1\xfe\x72\x96\xa3\xb9\xad\x1b\x57\x16\x31\xe6\ | |
334 | \x84\x61\x64\x8b\xb4\xcc\x5c\x31\x89\xf1\xc5\xf9\xbb\x51\x88\x51\ | |
335 | \xd3\x26\x45\x96\xe4\x65\x61\x63\x5c\x94\x78\x7f\x6f\x67\xfc\xea\ | |
336 | \xe8\xd3\xe1\xf9\xb7\xb3\x63\xd4\xcc\x27\xe8\xec\xe2\xe0\xf4\xfd\ | |
337 | \x21\xc2\x23\x4a\xbf\xca\x43\x4a\x8f\xce\x8f\xd0\xe7\x2f\x27\x88\ | |
338 | \x13\x4e\xe9\xf1\x47\x8c\xf0\xb4\x6d\xab\x88\xd2\xc5\x62\x41\x16\ | |
339 | \x92\x94\xf5\x84\x9e\xd4\x49\x35\x75\x69\x43\x21\x90\x76\x81\xb0\ | |
340 | \x89\x42\x32\xce\x49\xd6\x66\x18\x4a\x74\x99\x17\x2e\x6b\xa7\xd0\ | |
341 | \x16\x63\xaf\x31\x9a\x5a\x37\x99\xb6\x6b\x6b\xee\xec\xe2\xa0\x5c\ | |
342 | \xc6\x98\x21\x86\x54\xf7\xc1\xf7\xe7\xe0\x18\xc1\x64\x45\x13\x6f\ | |
343 | \xa9\x2d\x18\x63\x5d\xad\x55\x48\xb4\xcc\x5d\x71\xb5\x2d\x90\x1b\ | |
344 | \x63\x68\xff\xb4\x0f\x8d\x9a\x2a\x49\x81\x41\x55\xdb\xc6\xd6\x73\ | |
345 | \xdb\x91\xb9\xc9\xc1\x71\xe9\xf2\x7c\x54\x5f\xe7\x36\xb2\x73\x5b\ | |
346 | \x94\x59\xb6\x9b\xe6\xae\xda\xf4\x34\x6d\x5d\x5e\xd9\x11\x24\xb3\ | |
347 | \x69\x52\x45\x75\x79\x5d\x6c\x38\x7f\x96\xae\xd8\xf4\xce\x5c\x6b\ | |
348 | \xeb\xdc\xc1\x4f\xc4\x49\xb0\x0b\x4c\x10\xbc\xc6\x13\xd4\xd6\x49\ | |
349 | \xd1\x5c\x96\xf5\x2c\xc6\xb3\xa4\xad\xdd\xf2\x0d\xf7\x18\xbc\xb9\ | |
350 | \x37\xf2\xb9\xf6\xd8\xdb\x55\xe8\x2a\xdc\x65\x31\x4e\xcb\x3c\xb7\ | |
351 | \x69\x0b\x70\xf0\x53\xdb\xb9\xd0\xfe\xe6\xfe\x3e\x47\x0d\x5b\x11\ | |
352 | \xa0\xe6\xca\x00\xd7\x1b\x60\x8e\xd7\x47\xd3\x61\x5f\x1f\x4c\xb7\ | |
353 | \xbe\x47\x24\x2a\xe0\xc6\xec\x62\xfa\x20\xd9\xb6\xfe\x19\x31\x7e\ | |
354 | \xe0\x4b\xd9\xf7\x71\x67\x18\x41\x94\x62\xdc\x13\x84\x2b\xa5\xcc\ | |
355 | \xc3\xb6\x9e\xc8\xa6\x82\x80\x73\xc8\x46\x18\xe3\x4a\xfa\x42\x78\ | |
356 | \xa3\x7e\x6d\x84\x66\x06\xdc\x5a\x8b\x00\x48\x71\x61\x0c\x31\xd2\ | |
357 | \x13\x21\x91\xa1\x0e\xb7\x15\xe8\x8b\x54\x49\x3b\x45\x40\xf1\x83\ | |
358 | \xd2\xd2\xd3\xa7\x2a\x10\xab\x6f\x0e\xbd\xf2\x53\xa5\x42\x22\xa0\ | |
359 | \x4f\xb8\xbb\x8c\xcb\x5b\xd3\x97\x9e\x14\xc4\x30\x30\xf5\xc6\x52\ | |
360 | \x7f\xdf\x02\xe9\xf6\xc4\xa3\x1f\x79\x92\x5e\xad\x8f\xbf\x07\x1c\ | |
361 | \xf9\xd5\xf2\x11\xc2\xbe\x27\x3a\xf9\xe7\x34\x24\x93\xc0\xc1\x13\ | |
362 | \x01\x11\x81\x0a\x5e\x3a\x0d\x61\x14\xd1\xa1\x27\x41\x7b\xdc\xff\ | |
363 | \x0b\x1a\xff\x77\xe0\xc7\x8e\x3f\xd7\x99\x52\x83\xce\x06\x9d\x0d\ | |
364 | \x3a\x7b\x6e\x9d\xc1\xa0\x4c\x19\xe5\x8d\x84\x26\x42\x85\x6a\x10\ | |
365 | \xda\x33\x5f\xad\x41\x68\x2f\x53\x68\x1a\xe6\xf7\x75\x30\x08\x6d\ | |
366 | \x10\xda\x20\xb4\xdf\x0d\xbc\xe1\xb8\x33\xfa\xc5\xb8\xfb\x97\xbc\ | |
367 | \xb7\xf3\x0b\x46\x33\xee\x37\ | |
368 | \x00\x00\x04\x06\ | |
277 | \x00\x00\x02\xd7\ | |
278 | \x89\ | |
279 | \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ | |
280 | \x00\x00\x3c\x00\x00\x00\x3c\x08\x06\x00\x00\x00\x3a\xfc\xd9\x72\ | |
281 | \x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\x74\x77\x61\x72\x65\ | |
282 | \x00\x41\x64\x6f\x62\x65\x20\x49\x6d\x61\x67\x65\x52\x65\x61\x64\ | |
283 | \x79\x71\xc9\x65\x3c\x00\x00\x02\x79\x49\x44\x41\x54\x78\xda\xec\ | |
284 | \x9a\xd1\x6d\xc2\x30\x10\x40\x0d\x62\x80\x6c\x40\x3a\x01\xde\xa0\ | |
285 | \x19\x21\x23\xa4\x13\xc0\x06\xf5\x06\xa8\x13\xa4\x1b\xa4\x1b\x44\ | |
286 | \x4c\x10\x98\x20\x6c\x10\x36\x68\x8d\xe4\x48\xc8\x75\x82\xef\xb0\ | |
287 | \x0f\x87\xf8\xa4\xfb\x31\x89\xc3\xf3\x9d\xed\xbb\xb3\x17\x8c\x56\ | |
288 | \xb8\xd4\x5c\xea\x46\x6a\xa2\xda\x2e\x52\x4f\x52\x7f\xa4\x1e\xd9\ | |
289 | \x8b\xc8\x15\xb2\x91\xfa\x7b\x47\x5b\xf5\xec\x64\xe5\x6a\xc5\xca\ | |
290 | \x02\x54\xd7\xea\xc6\x03\x26\x05\xdb\x20\x60\x7b\x6d\xd4\x14\x98\ | |
291 | \x05\x6c\xaf\xdd\x54\x2c\xed\x02\xf6\xd6\xd2\x41\x8b\x70\x08\xdb\ | |
292 | \xab\x08\x15\x36\xf5\x00\xdb\x6b\x1a\x22\x70\xe9\x11\xb8\x9e\x93\ | |
293 | \x75\x9d\x59\x79\xe9\x10\x78\x4b\x30\xa8\xdb\x90\x2c\xdc\x12\x58\ | |
294 | \xb8\x0b\x05\x96\x13\xc0\xf6\xca\x43\x70\xe9\x8c\x38\x2e\x47\xcb\ | |
295 | \xea\x0e\x04\x57\x19\xcc\x59\xe9\x90\x6c\x08\x81\x37\x3e\x81\x3f\ | |
296 | \xb5\xb6\x2b\xfc\x97\xd4\x6f\xc3\x0a\x4d\xb9\x1b\xe8\x61\x6c\xae\ | |
297 | \xda\xfb\xb4\x93\xab\xb4\xf3\x0d\x02\x3c\x34\x57\xd3\x27\xbb\x34\ | |
298 | \x1f\xd8\xff\x4d\xf1\x3c\x68\x0e\xbf\x0f\xb4\x87\x96\xa4\x5f\x7c\ | |
299 | \x2f\x5a\x17\x36\x61\x59\xb2\x99\xc9\x12\x61\xc9\xf4\x55\x81\x4f\ | |
300 | \x13\x01\x4e\x7c\xbb\xf4\xfa\xc9\x0b\xd9\xd1\x62\xd5\x1e\xfc\x4f\ | |
301 | \x4b\x40\xc7\x63\x1f\x38\x3f\x71\x55\xe6\x90\x29\x89\x05\x4e\x2c\ | |
302 | \xdd\xdf\x87\x1c\x2c\x23\xaf\x03\xa6\xf3\x6e\x20\x80\xcf\x0d\x81\ | |
303 | \x07\x55\xf2\x90\x21\xff\xa3\x95\x0c\xd5\x94\x4b\xc0\xe0\xf8\x4c\ | |
304 | \x0f\xf3\x91\x67\x51\xd5\xce\x02\xd0\x61\x49\x00\x5c\x5a\x7e\x13\ | |
305 | \x5d\xe9\x4c\x98\x7d\x25\x91\x13\xbb\xf3\x58\x49\x69\xf7\xc8\x22\ | |
306 | \x51\x31\xfb\x22\x79\x4d\x58\xc4\x2b\x7d\xd5\xbe\x32\x80\x95\xb3\ | |
307 | \x00\xac\x5b\xfa\xae\x57\x71\x82\xb9\x5c\x01\x3c\xc9\x49\xaa\x3a\ | |
308 | \xb6\x1a\x36\x9a\x6b\x27\x8e\x0b\x7a\xad\xd6\xbf\x60\x44\xb5\xeb\ | |
309 | \x1a\xe0\x46\xdc\xd1\x36\xd5\x69\x1e\x94\xfb\x2c\xf0\x41\x2b\x93\ | |
310 | \xae\xa1\x75\xd8\x7b\xfd\xed\x7d\x84\x75\x02\x01\x8d\x39\x49\x6c\ | |
311 | \x80\xb0\x2d\xf3\x78\xac\x5a\x33\xd8\xe9\x7d\xa2\x06\xaa\xb3\xb4\ | |
312 | \xaa\xd0\xde\x2f\x98\xe7\x5a\xb5\x4d\x30\xd2\x58\x2c\x34\x99\xe1\ | |
313 | \xbd\x42\x0d\x48\xab\x3d\x5b\xa9\xdf\xf4\x81\xb2\xb9\x2e\x51\x50\ | |
314 | \x55\x0e\x3b\xcb\xed\x04\x13\x04\xec\x2c\xfb\x17\x94\x95\x06\xc8\ | |
315 | \xa2\x54\x1b\x2c\x68\xea\x6f\x0f\xe8\x13\x15\x60\x2c\x1e\x84\x4e\ | |
316 | \x95\x15\x21\x73\xe8\x6c\x28\x18\x40\x83\x85\x0f\xf6\xff\x30\x80\ | |
317 | \xb4\xa6\x84\xb9\x9a\x84\xdd\xaa\x82\xb9\xc7\xb5\x63\x7e\x73\xe2\ | |
318 | \x9a\x05\x78\xa3\x27\xf1\x10\x4b\x4f\xe2\x76\x5e\xaa\xc0\x1f\xb1\ | |
319 | \x78\x43\xb5\xe5\xb8\xb6\x78\xa1\xe0\x5b\x4b\x6b\x0a\x36\xa1\x1b\ | |
320 | \x78\xd8\xbc\x9a\x6c\x5f\x8d\x67\x4b\x11\x38\x02\x47\xe0\x08\x1c\ | |
321 | \x81\x03\x92\x15\xf2\x3d\x8e\x88\x6b\x6d\x82\x89\x35\x22\x73\x32\ | |
322 | \x65\x5f\xe4\x65\x1e\x4a\x15\xd1\xa5\x23\x70\x04\x9e\x8f\xfc\x09\ | |
323 | \x30\x00\xa0\x1c\x74\x67\x26\xea\x15\x76\x00\x00\x00\x00\x49\x45\ | |
324 | \x4e\x44\xae\x42\x60\x82\ | |
325 | \x00\x00\x04\x30\ | |
369 | 326 | \x3c\ |
370 | 327 | \x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ |
371 | 328 | \x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ |
395 | 352 | \x75\x6e\x64\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6d\x69\x74\x65\x72\ |
396 | 353 | \x6c\x69\x6d\x69\x74\x3a\x31\x2e\x35\x3b\x22\x3e\x0a\x20\x20\x20\ |
397 | 354 | \x20\x3c\x67\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x6d\ |
398 | \x61\x74\x72\x69\x78\x28\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x2d\x32\ | |
399 | \x38\x30\x2c\x30\x29\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\ | |
400 | \x3c\x67\x20\x69\x64\x3d\x22\x68\x65\x61\x72\x74\x22\x20\x74\x72\ | |
401 | \x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x6d\x61\x74\x72\x69\x78\x28\ | |
402 | \x31\x2c\x30\x2c\x30\x2c\x31\x2c\x2d\x31\x34\x31\x31\x2c\x30\x29\ | |
403 | \x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\ | |
404 | \x72\x65\x63\x74\x20\x78\x3d\x22\x31\x36\x39\x31\x22\x20\x79\x3d\ | |
405 | \x22\x30\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x36\x30\x22\x20\x68\ | |
406 | \x65\x69\x67\x68\x74\x3d\x22\x36\x30\x22\x20\x73\x74\x79\x6c\x65\ | |
407 | \x3d\x22\x66\x69\x6c\x6c\x3a\x6e\x6f\x6e\x65\x3b\x22\x2f\x3e\x0a\ | |
408 | \x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x67\x20\x74\ | |
409 | \x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x6d\x61\x74\x72\x69\x78\ | |
410 | \x28\x30\x2e\x39\x36\x30\x32\x34\x31\x2c\x30\x2c\x30\x2c\x30\x2e\ | |
411 | \x39\x36\x30\x32\x34\x31\x2c\x31\x34\x33\x31\x2e\x30\x31\x2c\x33\ | |
412 | \x2e\x31\x34\x37\x30\x31\x29\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\ | |
413 | \x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x70\x61\x74\x68\x20\ | |
414 | \x64\x3d\x22\x4d\x33\x30\x32\x2c\x31\x30\x2e\x38\x43\x33\x30\x37\ | |
415 | \x2e\x36\x38\x34\x2c\x30\x20\x33\x31\x39\x2e\x30\x35\x33\x2c\x30\ | |
416 | \x20\x33\x32\x34\x2e\x37\x33\x37\x2c\x35\x2e\x34\x43\x33\x33\x30\ | |
417 | \x2e\x34\x32\x31\x2c\x31\x30\x2e\x38\x20\x33\x33\x30\x2e\x34\x32\ | |
418 | \x31\x2c\x32\x31\x2e\x36\x20\x33\x32\x34\x2e\x37\x33\x37\x2c\x33\ | |
419 | \x32\x2e\x34\x43\x33\x32\x30\x2e\x37\x35\x38\x2c\x34\x30\x2e\x35\ | |
420 | \x20\x33\x31\x30\x2e\x35\x32\x36\x2c\x34\x38\x2e\x36\x20\x33\x30\ | |
421 | \x32\x2c\x35\x34\x43\x32\x39\x33\x2e\x34\x37\x34\x2c\x34\x38\x2e\ | |
422 | \x36\x20\x32\x38\x33\x2e\x32\x34\x32\x2c\x34\x30\x2e\x35\x20\x32\ | |
423 | \x37\x39\x2e\x32\x36\x33\x2c\x33\x32\x2e\x34\x43\x32\x37\x33\x2e\ | |
424 | \x35\x37\x39\x2c\x32\x31\x2e\x36\x20\x32\x37\x33\x2e\x35\x37\x39\ | |
425 | \x2c\x31\x30\x2e\x38\x20\x32\x37\x39\x2e\x32\x36\x33\x2c\x35\x2e\ | |
426 | \x34\x43\x32\x38\x34\x2e\x39\x34\x37\x2c\x30\x20\x32\x39\x36\x2e\ | |
427 | \x33\x31\x36\x2c\x30\x20\x33\x30\x32\x2c\x31\x30\x2e\x38\x5a\x22\ | |
355 | \x61\x74\x72\x69\x78\x28\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x2d\x31\ | |
356 | \x34\x30\x2c\x30\x29\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\ | |
357 | \x3c\x67\x20\x69\x64\x3d\x22\x74\x61\x67\x22\x20\x74\x72\x61\x6e\ | |
358 | \x73\x66\x6f\x72\x6d\x3d\x22\x6d\x61\x74\x72\x69\x78\x28\x31\x2c\ | |
359 | \x30\x2c\x30\x2c\x31\x2c\x2d\x37\x34\x36\x2c\x30\x29\x22\x3e\x0a\ | |
360 | \x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x72\x65\x63\ | |
361 | \x74\x20\x78\x3d\x22\x38\x38\x36\x22\x20\x79\x3d\x22\x30\x22\x20\ | |
362 | \x77\x69\x64\x74\x68\x3d\x22\x36\x30\x22\x20\x68\x65\x69\x67\x68\ | |
363 | \x74\x3d\x22\x36\x30\x22\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\ | |
364 | \x6c\x6c\x3a\x6e\x6f\x6e\x65\x3b\x22\x2f\x3e\x0a\x20\x20\x20\x20\ | |
365 | \x20\x20\x20\x20\x20\x20\x20\x20\x3c\x67\x20\x74\x72\x61\x6e\x73\ | |
366 | \x66\x6f\x72\x6d\x3d\x22\x6d\x61\x74\x72\x69\x78\x28\x31\x2e\x30\ | |
367 | \x32\x36\x31\x33\x2c\x30\x2c\x30\x2c\x31\x2e\x32\x35\x39\x32\x36\ | |
368 | \x2c\x35\x32\x36\x2e\x35\x38\x33\x2c\x2d\x38\x2e\x34\x30\x37\x34\ | |
369 | \x31\x29\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\ | |
370 | \x20\x20\x20\x20\x20\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x33\ | |
371 | \x35\x34\x2c\x31\x37\x4c\x33\x38\x38\x2c\x31\x37\x4c\x34\x30\x35\ | |
372 | \x2e\x30\x30\x31\x2c\x32\x33\x2e\x37\x35\x4c\x34\x30\x35\x2e\x30\ | |
373 | \x30\x31\x2c\x33\x37\x2e\x32\x35\x31\x4c\x33\x38\x38\x2c\x34\x34\ | |
374 | \x4c\x33\x35\x34\x2c\x34\x34\x4c\x33\x35\x34\x2c\x31\x37\x5a\x22\ | |
428 | 375 | \x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\x6c\x6c\x3a\x6e\x6f\x6e\ |
429 | 376 | \x65\x3b\x73\x74\x72\x6f\x6b\x65\x3a\x62\x6c\x61\x63\x6b\x3b\x73\ |
430 | \x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\x3a\x32\x2e\x38\x32\ | |
377 | \x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\x3a\x32\x2e\x31\x38\ | |
431 | 378 | \x70\x78\x3b\x22\x2f\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\ |
432 | \x20\x20\x20\x3c\x2f\x67\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\ | |
433 | \x3c\x2f\x67\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x67\x3e\x0a\x3c\x2f\ | |
434 | \x73\x76\x67\x3e\x0a\ | |
379 | \x20\x20\x20\x20\x20\x20\x20\x3c\x67\x20\x74\x72\x61\x6e\x73\x66\ | |
380 | \x6f\x72\x6d\x3d\x22\x6d\x61\x74\x72\x69\x78\x28\x30\x2e\x38\x39\ | |
381 | \x36\x31\x32\x38\x2c\x30\x2c\x30\x2c\x30\x2e\x38\x39\x36\x31\x32\ | |
382 | \x38\x2c\x33\x39\x2e\x33\x30\x39\x32\x2c\x33\x2e\x31\x36\x37\x34\ | |
383 | \x38\x29\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\ | |
384 | \x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x63\x69\x72\x63\x6c\x65\ | |
385 | \x20\x63\x78\x3d\x22\x34\x30\x30\x22\x20\x63\x79\x3d\x22\x33\x30\ | |
386 | \x2e\x35\x30\x31\x22\x20\x72\x3d\x22\x32\x2e\x34\x39\x39\x22\x20\ | |
387 | \x73\x74\x79\x6c\x65\x3d\x22\x73\x74\x72\x6f\x6b\x65\x3a\x62\x6c\ | |
388 | \x61\x63\x6b\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\ | |
389 | \x3a\x32\x2e\x36\x33\x70\x78\x3b\x22\x2f\x3e\x0a\x20\x20\x20\x20\ | |
390 | \x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x67\x3e\ | |
391 | \x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x67\ | |
392 | \x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x67\x3e\x0a\x20\ | |
393 | \x20\x20\x20\x3c\x2f\x67\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\x0a\ | |
435 | 394 | \x00\x00\x05\x55\ |
436 | 395 | \x3c\ |
437 | 396 | \x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ |
520 | 479 | \x20\x20\x3c\x2f\x67\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\ |
521 | 480 | \x2f\x67\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x67\x3e\x0a\x3c\x2f\x73\ |
522 | 481 | \x76\x67\x3e\x0a\ |
482 | \x00\x00\x02\x68\ | |
483 | \x00\ | |
484 | \x00\x10\x25\x78\x9c\xed\x97\x5b\x6f\xd3\x30\x14\xc7\xdf\xf7\x29\ | |
485 | \x8c\x25\x24\x90\x52\x5f\x13\x3b\xce\x9a\x4d\xda\x85\x09\x69\xc0\ | |
486 | \x24\x36\x10\xbc\x85\xc4\x6b\xcd\xd2\x24\x4a\xb2\xb6\xfb\xf6\x9c\ | |
487 | \x64\xed\xb4\x6e\xd5\x40\x88\xf1\xb2\xb4\x55\xeb\x73\x72\x7c\x2e\ | |
488 | \x3f\xfb\xff\xd0\xf1\xfe\x72\x96\xa3\xb9\xad\x1b\x57\x16\x31\xe6\ | |
489 | \x84\x61\x64\x8b\xb4\xcc\x5c\x31\x89\xf1\xc5\xf9\xbb\x51\x88\x51\ | |
490 | \xd3\x26\x45\x96\xe4\x65\x61\x63\x5c\x94\x78\x7f\x6f\x67\xfc\xea\ | |
491 | \xe8\xd3\xe1\xf9\xb7\xb3\x63\xd4\xcc\x27\xe8\xec\xe2\xe0\xf4\xfd\ | |
492 | \x21\xc2\x23\x4a\xbf\xca\x43\x4a\x8f\xce\x8f\xd0\xe7\x2f\x27\x88\ | |
493 | \x13\x4e\xe9\xf1\x47\x8c\xf0\xb4\x6d\xab\x88\xd2\xc5\x62\x41\x16\ | |
494 | \x92\x94\xf5\x84\x9e\xd4\x49\x35\x75\x69\x43\x21\x90\x76\x81\xb0\ | |
495 | \x89\x42\x32\xce\x49\xd6\x66\x18\x4a\x74\x99\x17\x2e\x6b\xa7\xd0\ | |
496 | \x16\x63\xaf\x31\x9a\x5a\x37\x99\xb6\x6b\x6b\xee\xec\xe2\xa0\x5c\ | |
497 | \xc6\x98\x21\x86\x54\xf7\xc1\xf7\xe7\xe0\x18\xc1\x64\x45\x13\x6f\ | |
498 | \xa9\x2d\x18\x63\x5d\xad\x55\x48\xb4\xcc\x5d\x71\xb5\x2d\x90\x1b\ | |
499 | \x63\x68\xff\xb4\x0f\x8d\x9a\x2a\x49\x81\x41\x55\xdb\xc6\xd6\x73\ | |
500 | \xdb\x91\xb9\xc9\xc1\x71\xe9\xf2\x7c\x54\x5f\xe7\x36\xb2\x73\x5b\ | |
501 | \x94\x59\xb6\x9b\xe6\xae\xda\xf4\x34\x6d\x5d\x5e\xd9\x11\x24\xb3\ | |
502 | \x69\x52\x45\x75\x79\x5d\x6c\x38\x7f\x96\xae\xd8\xf4\xce\x5c\x6b\ | |
503 | \xeb\xdc\xc1\x4f\xc4\x49\xb0\x0b\x4c\x10\xbc\xc6\x13\xd4\xd6\x49\ | |
504 | \xd1\x5c\x96\xf5\x2c\xc6\xb3\xa4\xad\xdd\xf2\x0d\xf7\x18\xbc\xb9\ | |
505 | \x37\xf2\xb9\xf6\xd8\xdb\x55\xe8\x2a\xdc\x65\x31\x4e\xcb\x3c\xb7\ | |
506 | \x69\x0b\x70\xf0\x53\xdb\xb9\xd0\xfe\xe6\xfe\x3e\x47\x0d\x5b\x11\ | |
507 | \xa0\xe6\xca\x00\xd7\x1b\x60\x8e\xd7\x47\xd3\x61\x5f\x1f\x4c\xb7\ | |
508 | \xbe\x47\x24\x2a\xe0\xc6\xec\x62\xfa\x20\xd9\xb6\xfe\x19\x31\x7e\ | |
509 | \xe0\x4b\xd9\xf7\x71\x67\x18\x41\x94\x62\xdc\x13\x84\x2b\xa5\xcc\ | |
510 | \xc3\xb6\x9e\xc8\xa6\x82\x80\x73\xc8\x46\x18\xe3\x4a\xfa\x42\x78\ | |
511 | \xa3\x7e\x6d\x84\x66\x06\xdc\x5a\x8b\x00\x48\x71\x61\x0c\x31\xd2\ | |
512 | \x13\x21\x91\xa1\x0e\xb7\x15\xe8\x8b\x54\x49\x3b\x45\x40\xf1\x83\ | |
513 | \xd2\xd2\xd3\xa7\x2a\x10\xab\x6f\x0e\xbd\xf2\x53\xa5\x42\x22\xa0\ | |
514 | \x4f\xb8\xbb\x8c\xcb\x5b\xd3\x97\x9e\x14\xc4\x30\x30\xf5\xc6\x52\ | |
515 | \x7f\xdf\x02\xe9\xf6\xc4\xa3\x1f\x79\x92\x5e\xad\x8f\xbf\x07\x1c\ | |
516 | \xf9\xd5\xf2\x11\xc2\xbe\x27\x3a\xf9\xe7\x34\x24\x93\xc0\xc1\x13\ | |
517 | \x01\x11\x81\x0a\x5e\x3a\x0d\x61\x14\xd1\xa1\x27\x41\x7b\xdc\xff\ | |
518 | \x0b\x1a\xff\x77\xe0\xc7\x8e\x3f\xd7\x99\x52\x83\xce\x06\x9d\x0d\ | |
519 | \x3a\x7b\x6e\x9d\xc1\xa0\x4c\x19\xe5\x8d\x84\x26\x42\x85\x6a\x10\ | |
520 | \xda\x33\x5f\xad\x41\x68\x2f\x53\x68\x1a\xe6\xf7\x75\x30\x08\x6d\ | |
521 | \x10\xda\x20\xb4\xdf\x0d\xbc\xe1\xb8\x33\xfa\xc5\xb8\xfb\x97\xbc\ | |
522 | \xb7\xf3\x0b\x46\x33\xee\x37\ | |
523 | 523 | " |
524 | 524 | |
525 | 525 | qt_resource_name = b"\ |
527 | 527 | \x00\x6f\xa6\x53\ |
528 | 528 | \x00\x69\ |
529 | 529 | \x00\x63\x00\x6f\x00\x6e\x00\x73\ |
530 | \x00\x08\ | |
531 | \x05\x1c\x5a\x47\ | |
532 | \x00\x61\ | |
533 | \x00\x6e\x00\x6b\x00\x69\x00\x2e\x00\x70\x00\x6e\x00\x67\ | |
534 | \x00\x09\ | |
535 | \x08\x97\x87\xa7\ | |
536 | \x00\x68\ | |
537 | \x00\x65\x00\x61\x00\x72\x00\x74\x00\x2e\x00\x73\x00\x76\x00\x67\ | |
538 | \x00\x08\ | |
539 | \x0b\x9e\x57\x87\ | |
540 | \x00\x64\ | |
541 | \x00\x65\x00\x63\x00\x6b\x00\x2e\x00\x73\x00\x76\x00\x67\ | |
530 | 542 | \x00\x10\ |
531 | 543 | \x08\x12\xae\xa7\ |
532 | 544 | \x00\x6d\ |
533 | 545 | \x00\x65\x00\x64\x00\x69\x00\x61\x00\x2d\x00\x72\x00\x65\x00\x63\x00\x6f\x00\x72\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ |
534 | \x00\x08\ | |
535 | \x05\x1c\x5a\x47\ | |
536 | \x00\x61\ | |
537 | \x00\x6e\x00\x6b\x00\x69\x00\x2e\x00\x70\x00\x6e\x00\x67\ | |
538 | 546 | \x00\x07\ |
539 | 547 | \x0a\x7a\x5a\x27\ |
540 | 548 | \x00\x74\ |
541 | 549 | \x00\x61\x00\x67\x00\x2e\x00\x73\x00\x76\x00\x67\ |
542 | \x00\x08\ | |
543 | \x0b\x9e\x57\x87\ | |
544 | \x00\x64\ | |
545 | \x00\x65\x00\x63\x00\x6b\x00\x2e\x00\x73\x00\x76\x00\x67\ | |
550 | \x00\x0c\ | |
551 | \x0e\xcd\x03\x47\ | |
552 | \x00\x6e\ | |
553 | \x00\x6f\x00\x74\x00\x65\x00\x74\x00\x79\x00\x70\x00\x65\x00\x2e\x00\x73\x00\x76\x00\x67\ | |
546 | 554 | \x00\x0e\ |
547 | 555 | \x04\x44\x35\x07\ |
548 | 556 | \x00\x63\ |
549 | 557 | \x00\x6f\x00\x6c\x00\x6c\x00\x65\x00\x63\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x2e\x00\x73\x00\x76\x00\x67\ |
550 | \x00\x09\ | |
551 | \x08\x97\x87\xa7\ | |
552 | \x00\x68\ | |
553 | \x00\x65\x00\x61\x00\x72\x00\x74\x00\x2e\x00\x73\x00\x76\x00\x67\ | |
554 | \x00\x0c\ | |
555 | \x0e\xcd\x03\x47\ | |
556 | \x00\x6e\ | |
557 | \x00\x6f\x00\x74\x00\x65\x00\x74\x00\x79\x00\x70\x00\x65\x00\x2e\x00\x73\x00\x76\x00\x67\ | |
558 | 558 | " |
559 | 559 | |
560 | 560 | qt_resource_struct_v1 = b"\ |
561 | 561 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ |
562 | 562 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x07\x00\x00\x00\x02\ |
563 | \x00\x00\x00\x76\x00\x01\x00\x00\x00\x01\x00\x00\x13\x38\ | |
564 | \x00\x00\x00\x36\x00\x00\x00\x00\x00\x01\x00\x00\x02\xdb\ | |
563 | \x00\x00\x00\xac\x00\x01\x00\x00\x00\x01\x00\x00\x1c\x9b\ | |
565 | 564 | \x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ |
566 | \x00\x00\x00\x98\x00\x00\x00\x00\x00\x01\x00\x00\x15\xa4\ | |
567 | \x00\x00\x00\x4c\x00\x00\x00\x00\x00\x01\x00\x00\x09\x97\ | |
568 | \x00\x00\x00\x60\x00\x00\x00\x00\x00\x01\x00\x00\x0d\xcb\ | |
569 | \x00\x00\x00\xb0\x00\x00\x00\x00\x00\x01\x00\x00\x19\xae\ | |
565 | \x00\x00\x00\x54\x00\x00\x00\x00\x00\x01\x00\x00\x10\x33\ | |
566 | \x00\x00\x00\x26\x00\x00\x00\x00\x00\x01\x00\x00\x06\xbc\ | |
567 | \x00\x00\x00\x7a\x00\x00\x00\x00\x00\x01\x00\x00\x13\x0e\ | |
568 | \x00\x00\x00\x3e\x00\x00\x00\x00\x00\x01\x00\x00\x0a\xc6\ | |
569 | \x00\x00\x00\x8e\x00\x00\x00\x00\x00\x01\x00\x00\x17\x42\ | |
570 | 570 | " |
571 | 571 | |
572 | 572 | qt_resource_struct_v2 = b"\ |
574 | 574 | \x00\x00\x00\x00\x00\x00\x00\x00\ |
575 | 575 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x07\x00\x00\x00\x02\ |
576 | 576 | \x00\x00\x00\x00\x00\x00\x00\x00\ |
577 | \x00\x00\x00\x76\x00\x01\x00\x00\x00\x01\x00\x00\x13\x38\ | |
577 | \x00\x00\x00\xac\x00\x01\x00\x00\x00\x01\x00\x00\x1c\x9b\ | |
578 | 578 | \x00\x00\x01\x5f\xb2\xe6\x4b\xb0\ |
579 | \x00\x00\x00\x36\x00\x00\x00\x00\x00\x01\x00\x00\x02\xdb\ | |
579 | \x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ | |
580 | 580 | \x00\x00\x01\x37\x58\x6b\x1d\xa0\ |
581 | \x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ | |
581 | \x00\x00\x00\x54\x00\x00\x00\x00\x00\x01\x00\x00\x10\x33\ | |
582 | 582 | \x00\x00\x01\x5f\xb2\xe6\x4b\xb0\ |
583 | \x00\x00\x00\x98\x00\x00\x00\x00\x00\x01\x00\x00\x15\xa4\ | |
583 | \x00\x00\x00\x26\x00\x00\x00\x00\x00\x01\x00\x00\x06\xbc\ | |
584 | 584 | \x00\x00\x01\x5f\xb2\xe6\x4b\xb0\ |
585 | \x00\x00\x00\x4c\x00\x00\x00\x00\x00\x01\x00\x00\x09\x97\ | |
585 | \x00\x00\x00\x7a\x00\x00\x00\x00\x00\x01\x00\x00\x13\x0e\ | |
586 | 586 | \x00\x00\x01\x5f\xb2\xe6\x4b\xb0\ |
587 | \x00\x00\x00\x60\x00\x00\x00\x00\x00\x01\x00\x00\x0d\xcb\ | |
587 | \x00\x00\x00\x3e\x00\x00\x00\x00\x00\x01\x00\x00\x0a\xc6\ | |
588 | 588 | \x00\x00\x01\x5f\xb2\xe6\x4b\xb0\ |
589 | \x00\x00\x00\xb0\x00\x00\x00\x00\x00\x01\x00\x00\x19\xae\ | |
589 | \x00\x00\x00\x8e\x00\x00\x00\x00\x00\x01\x00\x00\x17\x42\ | |
590 | 590 | \x00\x00\x01\x5f\xb2\xe6\x4b\xb0\ |
591 | 591 | " |
592 | 592 |
969 | 969 | Invalid property found on card. Please use Tools>Check Database, \ |
970 | 970 | and if the problem comes up again, please ask on the support site.""")) |
971 | 971 | |
972 | def onMpvWillPlay(self): | |
973 | if not self._activeWindowOnPlay: | |
974 | self._activeWindowOnPlay = self.app.activeWindow() | |
972 | def _isVideo(self, file): | |
973 | head, ext = os.path.splitext(file.lower()) | |
974 | return ext in (".mp4", ".mov", ".mpg", ".mpeg", ".mkv", ".avi") | |
975 | ||
976 | def onMpvWillPlay(self, file): | |
977 | if not self._isVideo(file): | |
978 | return | |
979 | ||
980 | self._activeWindowOnPlay = self.app.activeWindow() or self._activeWindowOnPlay | |
975 | 981 | |
976 | 982 | def onMpvIdle(self): |
977 | 983 | w = self._activeWindowOnPlay |
978 | if w and not sip.isdeleted(w) and w.isVisible(): | |
984 | if not self.app.activeWindow() and w and not sip.isdeleted(w) and w.isVisible(): | |
979 | 985 | w.activateWindow() |
980 | 986 | w.raise_() |
981 | 987 | self._activeWindowOnPlay = None |
42 | 42 | # create the thread, setup signals and start running |
43 | 43 | t = self.thread = SyncThread( |
44 | 44 | self.pm.collectionPath(), self.pm.profile['syncKey'], |
45 | auth=auth, media=self.pm.profile['syncMedia']) | |
45 | auth=auth, media=self.pm.profile['syncMedia'], | |
46 | hostNum=self.pm.profile.get("hostNum"), | |
47 | ) | |
46 | 48 | t.event.connect(self.onEvent) |
47 | 49 | self.label = _("Connecting...") |
48 | 50 | prog = self.mw.progress.start(immediate=True, label=self.label) |
63 | 65 | showText(self.thread.syncMsg) |
64 | 66 | if self.thread.uname: |
65 | 67 | self.pm.profile['syncUser'] = self.thread.uname |
68 | self.pm.profile['hostNum'] = self.thread.hostNum | |
66 | 69 | def delayedInfo(): |
67 | 70 | if self._didFullUp and not self._didError: |
68 | 71 | showInfo(_("""\ |
289 | 292 | |
290 | 293 | event = pyqtSignal(str, str) |
291 | 294 | |
292 | def __init__(self, path, hkey, auth=None, media=True): | |
295 | def __init__(self, path, hkey, auth=None, media=True, hostNum=None): | |
293 | 296 | QThread.__init__(self) |
294 | 297 | self.path = path |
295 | 298 | self.hkey = hkey |
296 | 299 | self.auth = auth |
297 | 300 | self.media = media |
301 | self.hostNum = hostNum | |
298 | 302 | self._abort = 0 # 1=flagged, 2=aborting |
299 | 303 | |
300 | 304 | def flagAbort(self): |
310 | 314 | except: |
311 | 315 | self.fireEvent("corrupt") |
312 | 316 | return |
313 | self.server = RemoteServer(self.hkey) | |
317 | self.server = RemoteServer(self.hkey, hostNum=self.hostNum) | |
314 | 318 | self.client = Syncer(self.col, self.server) |
315 | 319 | self.sentTotal = 0 |
316 | 320 | self.recvTotal = 0 |
404 | 408 | self.fireEvent("error", "Unknown sync return code.") |
405 | 409 | self.syncMsg = self.client.syncMsg |
406 | 410 | self.uname = self.client.uname |
411 | self.hostNum = self.client.hostNum | |
407 | 412 | # then move on to media sync |
408 | 413 | self._syncMedia() |
409 | 414 | |
418 | 423 | f = self.fullSyncChoice |
419 | 424 | if f == "cancel": |
420 | 425 | return |
421 | self.client = FullSyncer(self.col, self.hkey, self.server.client) | |
426 | self.client = FullSyncer(self.col, self.hkey, self.server.client, | |
427 | hostNum=self.hostNum) | |
422 | 428 | try: |
423 | 429 | if f == "upload": |
424 | 430 | if not self.client.upload(): |
436 | 442 | def _syncMedia(self): |
437 | 443 | if not self.media: |
438 | 444 | return |
439 | self.server = RemoteMediaServer(self.col, self.hkey, self.server.client) | |
445 | self.server = RemoteMediaServer(self.col, self.hkey, self.server.client, | |
446 | hostNum=self.hostNum) | |
440 | 447 | self.client = MediaSyncer(self.col, self.server) |
441 | 448 | try: |
442 | 449 | ret = self.client.sync() |
271 | 271 | cb(file) |
272 | 272 | ret.append(file) |
273 | 273 | d.accepted.connect(accept) |
274 | if key: | |
275 | restoreState(d, key) | |
274 | 276 | d.exec_() |
277 | if key: | |
278 | saveState(d, key) | |
275 | 279 | return ret and ret[0] |
276 | 280 | |
277 | 281 | def getSaveFile(parent, title, dir_description, key, ext, fname=None): |
323 | 323 | <property name="text"> |
324 | 324 | <string>&Reschedule...</string> |
325 | 325 | </property> |
326 | <property name="shortcut"> | |
327 | <string>Ctrl+Alt+R</string> | |
328 | </property> | |
326 | 329 | </action> |
327 | 330 | <action name="actionSelectAll"> |
328 | 331 | <property name="text"> |
343 | 346 | <action name="actionInvertSelection"> |
344 | 347 | <property name="text"> |
345 | 348 | <string>&Invert Selection</string> |
349 | </property> | |
350 | <property name="shortcut"> | |
351 | <string>Ctrl+Alt+S</string> | |
346 | 352 | </property> |
347 | 353 | </action> |
348 | 354 | <action name="actionFind"> |
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
135 | 135 | # pass it once |
136 | 136 | d.sched.answerCard(c, 3) |
137 | 137 | # it should by due in 3 minutes |
138 | assert round(c.due - time.time()) in (179, 180) | |
138 | dueIn = c.due - time.time() | |
139 | assert 179 <= dueIn <= 180*1.25 | |
139 | 140 | assert c.left%1000 == 2 |
140 | 141 | assert c.left//1000 == 2 |
141 | 142 | # check log is accurate |
146 | 147 | # pass again |
147 | 148 | d.sched.answerCard(c, 3) |
148 | 149 | # it should by due in 10 minutes |
149 | assert round(c.due - time.time()) in (599, 600) | |
150 | dueIn = c.due - time.time() | |
151 | assert 599 <= dueIn <= 600*1.25 | |
150 | 152 | assert c.left%1000 == 1 |
151 | 153 | assert c.left//1000 == 1 |
152 | 154 | # the next pass should graduate the card |
1106 | 1108 | c = d.sched.getCard() |
1107 | 1109 | d.sched.answerCard(c, 1) |
1108 | 1110 | assert c.ivl == 50 |
1109 | # failing again, the actual elapsed interval is 0, | |
1110 | # so the card is reset to new | |
1111 | d.sched.answerCard(c, 1) | |
1112 | assert c.ivl == 1 | |
1111 | d.sched.answerCard(c, 1) | |
1112 | assert c.ivl == 25 | |
1113 | 1113 | |
1114 | 1114 | def test_moveVersions(): |
1115 | 1115 | col = _getEmptyCol(schedVer=1) |
0 | # coding: utf-8 | |
1 | ||
2 | import nose, os, shutil, time | |
3 | ||
4 | from anki import Collection as aopen, Collection | |
5 | from anki.utils import intTime | |
6 | from anki.sync import Syncer, LocalServer | |
7 | from anki.consts import STARTING_FACTOR | |
8 | from tests.shared import getEmptyCol, getEmptyDeckWith | |
9 | import anki.stdmodels | |
10 | ||
11 | # Local tests | |
12 | ########################################################################## | |
13 | ||
14 | deck1=None | |
15 | deck2=None | |
16 | client=None | |
17 | server=None | |
18 | server2=None | |
19 | ||
20 | def setup_basic(): | |
21 | global deck1, deck2, client, server | |
22 | deck1 = getEmptyCol() | |
23 | # add a note to deck 1 | |
24 | f = deck1.newNote() | |
25 | f['Front'] = "foo"; f['Back'] = "bar"; f.tags = ["foo"] | |
26 | deck1.addNote(f) | |
27 | # answer it | |
28 | deck1.reset(); deck1.sched.answerCard(deck1.sched.getCard(), 4) | |
29 | # repeat for deck2 | |
30 | deck2 = getEmptyDeckWith(server=True) | |
31 | f = deck2.newNote() | |
32 | f['Front'] = "bar"; f['Back'] = "bar"; f.tags = ["bar"] | |
33 | deck2.addNote(f) | |
34 | deck2.reset(); deck2.sched.answerCard(deck2.sched.getCard(), 4) | |
35 | # start with same schema and sync time | |
36 | deck1.scm = deck2.scm = 0 | |
37 | # and same mod time, so sync does nothing | |
38 | t = intTime(1000) | |
39 | deck1.save(mod=t); deck2.save(mod=t) | |
40 | server = LocalServer(deck2) | |
41 | client = Syncer(deck1, server) | |
42 | ||
43 | def setup_modified(): | |
44 | setup_basic() | |
45 | # mark deck1 as changed | |
46 | time.sleep(0.1) | |
47 | deck1.setMod() | |
48 | deck1.save() | |
49 | ||
50 | @nose.with_setup(setup_basic) | |
51 | def test_nochange(): | |
52 | assert client.sync() == "noChanges" | |
53 | ||
54 | @nose.with_setup(setup_modified) | |
55 | def test_changedSchema(): | |
56 | deck1.scm += 1 | |
57 | deck1.setMod() | |
58 | assert client.sync() == "fullSync" | |
59 | ||
60 | @nose.with_setup(setup_modified) | |
61 | def test_sync(): | |
62 | def check(num): | |
63 | for d in deck1, deck2: | |
64 | for t in ("revlog", "notes", "cards"): | |
65 | assert d.db.scalar("select count() from %s" % t) == num | |
66 | assert len(d.models.all()) == num*len(anki.stdmodels.models) | |
67 | # the default deck and config have an id of 1, so always 1 | |
68 | assert len(d.decks.all()) == 1 | |
69 | assert len(d.decks.dconf) == 1 | |
70 | assert len(d.tags.all()) == num | |
71 | check(1) | |
72 | origUsn = deck1.usn() | |
73 | assert client.sync() == "success" | |
74 | # last sync times and mod times should agree | |
75 | assert deck1.mod == deck2.mod | |
76 | assert deck1._usn == deck2._usn | |
77 | assert deck1.mod == deck1.ls | |
78 | assert deck1._usn != origUsn | |
79 | # because everything was created separately it will be merged in. in | |
80 | # actual use, we use a full sync to ensure a common starting point. | |
81 | check(2) | |
82 | # repeating it does nothing | |
83 | assert client.sync() == "noChanges" | |
84 | # if we bump mod time, the decks will sync but should remain the same. | |
85 | deck1.setMod() | |
86 | deck1.save(mod=deck1.mod+1) | |
87 | assert client.sync() == "success" | |
88 | check(2) | |
89 | # crt should be synced | |
90 | deck1.crt = 123 | |
91 | deck1.setMod() | |
92 | deck1.save(mod=deck1.mod+1) | |
93 | ret = client.sync(); assert ret == "success" | |
94 | assert deck1.crt == deck2.crt | |
95 | ||
96 | @nose.with_setup(setup_modified) | |
97 | def test_models(): | |
98 | test_sync() | |
99 | # update model one | |
100 | cm = deck1.models.current() | |
101 | cm['name'] = "new" | |
102 | time.sleep(1) | |
103 | deck1.models.save(cm) | |
104 | deck1.save() | |
105 | assert deck2.models.get(cm['id'])['name'].startswith("Basic") | |
106 | assert client.sync() == "success" | |
107 | assert deck2.models.get(cm['id'])['name'] == "new" | |
108 | # deleting triggers a full sync | |
109 | deck1.scm = deck2.scm = 0 | |
110 | deck1.models.rem(cm) | |
111 | deck1.save() | |
112 | assert client.sync() == "fullSync" | |
113 | ||
114 | @nose.with_setup(setup_modified) | |
115 | def test_notes(): | |
116 | test_sync() | |
117 | # modifications should be synced | |
118 | nid = deck1.db.scalar("select id from notes") | |
119 | note = deck1.getNote(nid) | |
120 | assert note['Front'] != "abc" | |
121 | note['Front'] = "abc" | |
122 | note.flush() | |
123 | deck1.save() | |
124 | assert client.sync() == "success" | |
125 | assert deck2.getNote(nid)['Front'] == "abc" | |
126 | # deletions too | |
127 | assert deck1.db.scalar("select 1 from notes where id = ?", nid) | |
128 | deck1.remNotes([nid]) | |
129 | deck1.save() | |
130 | assert client.sync() == "success" | |
131 | assert not deck1.db.scalar("select 1 from notes where id = ?", nid) | |
132 | assert not deck2.db.scalar("select 1 from notes where id = ?", nid) | |
133 | ||
134 | @nose.with_setup(setup_modified) | |
135 | def test_cards(): | |
136 | test_sync() | |
137 | nid = deck1.db.scalar("select id from notes") | |
138 | note = deck1.getNote(nid) | |
139 | card = note.cards()[0] | |
140 | # answer the card locally | |
141 | card.startTimer() | |
142 | deck1.sched.answerCard(card, 4) | |
143 | assert card.reps == 2 | |
144 | deck1.save() | |
145 | assert deck2.getCard(card.id).reps == 1 | |
146 | assert client.sync() == "success" | |
147 | assert deck2.getCard(card.id).reps == 2 | |
148 | # if it's modified on both sides , later mod time should win | |
149 | for test in ((deck1, deck2), (deck2, deck1)): | |
150 | time.sleep(1) | |
151 | c = test[0].getCard(card.id) | |
152 | c.reps = 5; c.flush() | |
153 | test[0].save() | |
154 | time.sleep(1) | |
155 | c = test[1].getCard(card.id) | |
156 | c.reps = 3; c.flush() | |
157 | test[1].save() | |
158 | assert client.sync() == "success" | |
159 | assert test[1].getCard(card.id).reps == 3 | |
160 | assert test[0].getCard(card.id).reps == 3 | |
161 | # removals should work too | |
162 | deck1.remCards([card.id]) | |
163 | deck1.save() | |
164 | assert deck2.db.scalar("select 1 from cards where id = ?", card.id) | |
165 | assert client.sync() == "success" | |
166 | assert not deck2.db.scalar("select 1 from cards where id = ?", card.id) | |
167 | ||
168 | @nose.with_setup(setup_modified) | |
169 | def test_tags(): | |
170 | test_sync() | |
171 | def sortedTags(deck): | |
172 | return sorted(deck.tags.all()) | |
173 | assert sortedTags(deck1) == sortedTags(deck2) | |
174 | deck1.tags.register(["abc"]) | |
175 | deck2.tags.register(["xyz"]) | |
176 | assert sortedTags(deck1) != sortedTags(deck2) | |
177 | deck1.save() | |
178 | time.sleep(0.1) | |
179 | deck2.save() | |
180 | assert client.sync() == "success" | |
181 | assert sortedTags(deck1) == sortedTags(deck2) | |
182 | ||
183 | @nose.with_setup(setup_modified) | |
184 | def test_decks(): | |
185 | test_sync() | |
186 | assert len(deck1.decks.all()) == 1 | |
187 | assert len(deck1.decks.all()) == len(deck2.decks.all()) | |
188 | deck1.decks.id("new") | |
189 | assert len(deck1.decks.all()) != len(deck2.decks.all()) | |
190 | time.sleep(0.1) | |
191 | deck2.decks.id("new2") | |
192 | deck1.save() | |
193 | time.sleep(0.1) | |
194 | deck2.save() | |
195 | assert client.sync() == "success" | |
196 | assert sorted(deck1.tags.all()) == sorted(deck2.tags.all()) | |
197 | assert len(deck1.decks.all()) == len(deck2.decks.all()) | |
198 | assert len(deck1.decks.all()) == 3 | |
199 | assert deck1.decks.confForDid(1)['maxTaken'] == 60 | |
200 | deck2.decks.confForDid(1)['maxTaken'] = 30 | |
201 | deck2.decks.save(deck2.decks.confForDid(1)) | |
202 | deck2.save() | |
203 | assert client.sync() == "success" | |
204 | assert deck1.decks.confForDid(1)['maxTaken'] == 30 | |
205 | ||
206 | @nose.with_setup(setup_modified) | |
207 | def test_conf(): | |
208 | test_sync() | |
209 | assert deck2.conf['curDeck'] == 1 | |
210 | deck1.conf['curDeck'] = 2 | |
211 | time.sleep(0.1) | |
212 | deck1.setMod() | |
213 | deck1.save() | |
214 | assert client.sync() == "success" | |
215 | assert deck2.conf['curDeck'] == 2 | |
216 | ||
217 | @nose.with_setup(setup_modified) | |
218 | def test_threeway(): | |
219 | test_sync() | |
220 | deck1.close(save=False) | |
221 | d3path = deck1.path.replace(".anki", "2.anki") | |
222 | shutil.copy2(deck1.path, d3path) | |
223 | deck1.reopen() | |
224 | deck3 = aopen(d3path) | |
225 | client2 = Syncer(deck3, server) | |
226 | assert client2.sync() == "noChanges" | |
227 | # client 1 adds a card at time 1 | |
228 | time.sleep(1) | |
229 | f = deck1.newNote() | |
230 | f['Front'] = "1"; | |
231 | deck1.addNote(f) | |
232 | deck1.save() | |
233 | # at time 2, client 2 syncs to server | |
234 | time.sleep(1) | |
235 | deck3.setMod() | |
236 | deck3.save() | |
237 | assert client2.sync() == "success" | |
238 | # at time 3, client 1 syncs, adding the older note | |
239 | time.sleep(1) | |
240 | assert client.sync() == "success" | |
241 | assert deck1.noteCount() == deck2.noteCount() | |
242 | # syncing client2 should pick it up | |
243 | assert client2.sync() == "success" | |
244 | assert deck1.noteCount() == deck2.noteCount() == deck3.noteCount() | |
245 | ||
246 | def test_threeway2(): | |
247 | # for this test we want ms precision of notes so we don't have to | |
248 | # sleep a lot | |
249 | import anki.notes | |
250 | intTime = anki.notes.intTime | |
251 | anki.notes.intTime = lambda x=1: intTime(1000) | |
252 | def setup(): | |
253 | # create collection 1 with a single note | |
254 | c1 = getEmptyCol() | |
255 | f = c1.newNote() | |
256 | f['Front'] = "startingpoint" | |
257 | nid = f.id | |
258 | c1.addNote(f) | |
259 | cid = f.cards()[0].id | |
260 | c1.beforeUpload() | |
261 | # start both clients and server off in this state | |
262 | s1path = c1.path.replace(".anki2", "-s1.anki2") | |
263 | c2path = c1.path.replace(".anki2", "-c2.anki2") | |
264 | shutil.copy2(c1.path, s1path) | |
265 | shutil.copy2(c1.path, c2path) | |
266 | # open them | |
267 | c1 = Collection(c1.path) | |
268 | c2 = Collection(c2path) | |
269 | s1 = Collection(s1path, server=True) | |
270 | return c1, c2, s1, nid, cid | |
271 | c1, c2, s1, nid, cid = setup() | |
272 | # modify c1 then sync c1->s1 | |
273 | n = c1.getNote(nid) | |
274 | t = "firstmod" | |
275 | n['Front'] = t | |
276 | n.flush() | |
277 | c1.db.execute("update cards set mod=1, usn=-1") | |
278 | srv = LocalServer(s1) | |
279 | clnt1 = Syncer(c1, srv) | |
280 | clnt1.sync() | |
281 | n.load() | |
282 | assert n['Front'] == t | |
283 | assert s1.getNote(nid)['Front'] == t | |
284 | assert s1.db.scalar("select mod from cards") == 1 | |
285 | # sync s1->c2 | |
286 | clnt2 = Syncer(c2, srv) | |
287 | clnt2.sync() | |
288 | assert c2.getNote(nid)['Front'] == t | |
289 | assert c2.db.scalar("select mod from cards") == 1 | |
290 | # modify c1 and sync | |
291 | time.sleep(0.001) | |
292 | t = "secondmod" | |
293 | n = c1.getNote(nid) | |
294 | n['Front'] = t | |
295 | n.flush() | |
296 | c1.db.execute("update cards set mod=2, usn=-1") | |
297 | clnt1.sync() | |
298 | # modify c2 and sync - both c2 and server should be the same | |
299 | time.sleep(0.001) | |
300 | t2 = "thirdmod" | |
301 | n = c2.getNote(nid) | |
302 | n['Front'] = t2 | |
303 | n.flush() | |
304 | c2.db.execute("update cards set mod=3, usn=-1") | |
305 | clnt2.sync() | |
306 | n.load() | |
307 | assert n['Front'] == t2 | |
308 | assert c2.db.scalar("select mod from cards") == 3 | |
309 | n = s1.getNote(nid) | |
310 | assert n['Front'] == t2 | |
311 | assert s1.db.scalar("select mod from cards") == 3 | |
312 | # and syncing c1 again should yield the updated note as well | |
313 | clnt1.sync() | |
314 | n = s1.getNote(nid) | |
315 | assert n['Front'] == t2 | |
316 | assert s1.db.scalar("select mod from cards") == 3 | |
317 | n = c1.getNote(nid) | |
318 | assert n['Front'] == t2 | |
319 | assert c1.db.scalar("select mod from cards") == 3 | |
320 | ||
321 | def _test_speed(): | |
322 | t = time.time() | |
323 | deck1 = aopen(os.path.expanduser("~/rapid.anki")) | |
324 | for tbl in "revlog", "cards", "notes", "graves": | |
325 | deck1.db.execute("update %s set usn = -1 where usn != -1"%tbl) | |
326 | for m in deck1.models.all(): | |
327 | m['usn'] = -1 | |
328 | for tx in deck1.tags.all(): | |
329 | deck1.tags.tags[tx] = -1 | |
330 | deck1._usn = -1 | |
331 | deck1.save() | |
332 | deck2 = getEmptyDeckWith(server=True) | |
333 | deck1.scm = deck2.scm = 0 | |
334 | server = LocalServer(deck2) | |
335 | client = Syncer(deck1, server) | |
336 | print("load %d" % ((time.time() - t)*1000)); t = time.time() | |
337 | assert client.sync() == "success" | |
338 | print("sync %d" % ((time.time() - t)*1000)); t = time.time() | |
339 | ||
340 | @nose.with_setup(setup_modified) | |
341 | def test_filtered_delete(): | |
342 | test_sync() | |
343 | nid = deck1.db.scalar("select id from notes") | |
344 | note = deck1.getNote(nid) | |
345 | card = note.cards()[0] | |
346 | card.queue = 2 | |
347 | card.type = 2 | |
348 | card.ivl = 10 | |
349 | card.factor = STARTING_FACTOR | |
350 | card.due = deck1.sched.today | |
351 | card.flush() | |
352 | # put cards into a filtered deck | |
353 | did = deck1.decks.newDyn("dyn") | |
354 | deck1.sched.rebuildDyn(did) | |
355 | # sync the filtered deck | |
356 | assert client.sync() == "success" | |
357 | # answer the card locally | |
358 | time.sleep(1) | |
359 | card.load() | |
360 | card.startTimer() | |
361 | deck1.sched.answerCard(card, 4) | |
362 | assert card.ivl > 10 | |
363 | # delete the filtered deck | |
364 | deck1.decks.rem(did) | |
365 | # sync again | |
366 | assert client.sync() == "success" | |
367 | card.load() | |
368 | assert card.ivl > 10 | |
369 | return |
27 | 27 | } |
28 | 28 | } |
29 | 29 | |
30 | function triggerKeyTimer() { | |
31 | clearChangeTimer(); | |
32 | changeTimer = setTimeout(function () { | |
33 | updateButtonState(); | |
34 | saveField("key"); | |
35 | }, 600); | |
36 | } | |
37 | ||
30 | 38 | function onKey() { |
31 | 39 | // esc clears focus, allowing dialog to close |
32 | 40 | if (window.event.which === 27) { |
40 | 48 | focusPrevious(); |
41 | 49 | return; |
42 | 50 | } |
43 | clearChangeTimer(); | |
44 | changeTimer = setTimeout(function () { | |
45 | updateButtonState(); | |
46 | saveField("key"); | |
47 | }, 600); | |
51 | triggerKeyTimer(); | |
48 | 52 | } |
49 | 53 | |
50 | 54 | function insertNewline() { |
81 | 85 | return window.getComputedStyle(n).whiteSpace.startsWith("pre"); |
82 | 86 | } |
83 | 87 | |
84 | function checkForEmptyField() { | |
88 | function onInput() { | |
89 | // empty field? | |
85 | 90 | if (currentField.innerHTML === "") { |
86 | 91 | currentField.innerHTML = "<br>"; |
87 | 92 | } |
93 | ||
94 | // make sure IME changes get saved | |
95 | triggerKeyTimer(); | |
88 | 96 | } |
89 | 97 | |
90 | 98 | function updateButtonState() { |
287 | 295 | f = "<br>"; |
288 | 296 | } |
289 | 297 | txt += "<tr><td class=fname>{0}</td></tr><tr><td width=100%>".format(n); |
290 | txt += "<div id=f{0} onkeydown='onKey();' oninput='checkForEmptyField()' onmouseup='onKey();'".format(i); | |
298 | txt += "<div id=f{0} onkeydown='onKey();' oninput='onInput()' onmouseup='onKey();'".format(i); | |
291 | 299 | txt += " onfocus='onFocus(this);' onblur='onBlur();' class=field "; |
292 | 300 | txt += "ondragover='onDragOver(this);' onpaste='onPaste(this);' "; |
293 | 301 | txt += "oncopy='onCutOrCopy(this);' oncut='onCutOrCopy(this);' "; |