diff --git a/README.development b/README.development index c58258379..69970209a 100644 --- a/README.development +++ b/README.development @@ -1,7 +1,11 @@ Please see the README file for basic requirements. -You also need to have the python-pyqt development packages installed -(specifically, you need the binary pyuic4). +In addition to the basic requirements, you also need the PyQt development +tools (specifically pyrcc4 and pyuic4). These are often contained in a +separate package on Linux, such as 'pyqt4-dev-tools' on Debian/Ubuntu. On a Mac +they are part of the PyQt source install. + +Windows users, please see the note at the bottom of this file before proceeding. To use the development version: @@ -9,7 +13,11 @@ $ git clone https://github.com/dae/anki.git $ cd anki $ ./tools/build_ui.sh -Make sure you rebuild the UI every time you update the sources. +If you get any errors, you will not be able to proceed, so please return to +the top and check the requirements again. + +ALL USERS: Make sure you rebuild the UI every time you git pull, otherwise you +will get errors down the road. The translations are stored in a bazaar repo for integration with Launchpad's translation services. If you want to use a language other than English: @@ -31,3 +39,20 @@ Before contributing code, please read the LICENSE file. If you'd like to contribute translations, please see the translations section of http://ankisrs.net/docs/manual.html#_contributing + +WINDOWS USERS: + +I have not tested the build scripts on Windows, so you'll need to solve any +problems you encounter on your own. The easiest way is to use a source +tarball instead of git, as that way you don't need to build the UI yourself. +If you do want to use git, a user contributed the following, which should get +you most of the way there: + + 1) Install "git bash". + 2) In the tools directory, modify build_ui.sh. Locate the line that reads + "pyuic4 $i -o $py" and alter it to be of the following form: + "" "" $i -o $py + These two paths must point to your python executable, and to pyuic.py, on your + system. Typical paths would be: + = C:\\Python27\\python.exe + = C:\\Python27\\Lib\\site-packages\\PyQt4\\uic\\pyuic.py diff --git a/anki/__init__.py b/anki/__init__.py index b54587f97..d7f33c917 100644 --- a/anki/__init__.py +++ b/anki/__init__.py @@ -30,6 +30,6 @@ if arch[1] == "ELF": sys.path.insert(0, os.path.join(ext, "py2.%d-%s" % ( sys.version_info[1], arch[0][0:2]))) -version="2.0.19" # build scripts grep this line, so preserve formatting +version="2.0.26" # build scripts grep this line, so preserve formatting from anki.storage import Collection __all__ = ["Collection"] diff --git a/anki/ankiweb.certs b/anki/ankiweb.certs index 390503e10..c9ce9b4cc 100644 --- a/anki/ankiweb.certs +++ b/anki/ankiweb.certs @@ -1,4 +1,142 @@ -----BEGIN CERTIFICATE----- +MIIFFjCCA/6gAwIBAgIQRi3682cfg0pV4BcKiOriGTANBgkqhkiG9w0BAQUFADBy +MQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD +VQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01PRE8gQ0EgTGltaXRlZDEYMBYGA1UE +AxMPRXNzZW50aWFsU1NMIENBMB4XDTE0MDQxMDAwMDAwMFoXDTE3MDQwOTIzNTk1 +OVowWzEhMB8GA1UECxMYRG9tYWluIENvbnRyb2wgVmFsaWRhdGVkMR4wHAYDVQQL +ExVFc3NlbnRpYWxTU0wgV2lsZGNhcmQxFjAUBgNVBAMUDSouYW5raXdlYi5uZXQw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+A1hdxZ8NNorbzRF9qutY +7DNuKQdXRjMuRszAeAHI8Ln6+zfvMvLPvCHrZ9aDNTpKhAQBuE7NrFkR9oR0xHT8 +JtgVQKJ5zvVFfZmNoHyWYW0+dj8ay1Y/74V/I4Xhb43Fk4Q7UBgAl0kwm5vDWC6U +HsA9/KPfEbWCOLSVoA1sU60viggnfbIl0XmNkkt355KvUdJgT5LisOfi8KvH58RN +X8RHDsLI+Kv3rc11hLxwJ171NC0bPvjOIvQ5NKlNeDFZASx00kaGE3H26r1it2Gr +hrR8IlTSV967JDpYi1FO+Aom54/OZ6ozE/JNsbKlcwmdH2CO2aMizFZfoQmYEsyr +AgMBAAGjggG9MIIBuTAfBgNVHSMEGDAWgBTay+qtWwhdzP/8JlTOSeVVxjj0+DAd +BgNVHQ4EFgQU8P8DDEM/4k6bCQr8Z9BeoGw5ANgwDgYDVR0PAQH/BAQDAgWgMAwG +A1UdEwEB/wQCMAAwNAYDVR0lBC0wKwYIKwYBBQUHAwEGCCsGAQUFBwMCBgorBgEE +AYI3CgMDBglghkgBhvhCBAEwTwYDVR0gBEgwRjA6BgsrBgEEAbIxAQICBzArMCkG +CCsGAQUFBwIBFh1odHRwczovL3NlY3VyZS5jb21vZG8uY29tL0NQUzAIBgZngQwB +AgEwOwYDVR0fBDQwMjAwoC6gLIYqaHR0cDovL2NybC5jb21vZG9jYS5jb20vRXNz +ZW50aWFsU1NMQ0EuY3JsMG4GCCsGAQUFBwEBBGIwYDA4BggrBgEFBQcwAoYsaHR0 +cDovL2NydC5jb21vZG9jYS5jb20vRXNzZW50aWFsU1NMQ0FfMi5jcnQwJAYIKwYB +BQUHMAGGGGh0dHA6Ly9vY3NwLmNvbW9kb2NhLmNvbTAlBgNVHREEHjAcgg0qLmFu +a2l3ZWIubmV0ggthbmtpd2ViLm5ldDANBgkqhkiG9w0BAQUFAAOCAQEAR2JPdym8 +MsSqmi2EyB4zR/UZH7XQUdIwx/1NhKf1XTN9akTNsS2y6Vp+TvaRVknEm7Z1i8CU +xiSZicsUOUr8MCzDVtTl3KUuYNUdsv0yXwTvGc01xJ26ix+KTmmQVKBq86gYGzXI +pLG0mfG1UAdZc6MxhcPXDROppvGXWk2vb6xYryVQiqV45SCiX2a4PtIf8zqO62eD +Xqazh3fpJ685rBnvWvpi8teYlYbJpJoaFEHa5ime2VixEYPVnfY2DBbiLQVImCgF +4NLwbCh79uD90CiGXHZbVUCq8fNf2gYzhK08erbDWhK39DzkBcE1XlbsPYzN9fUL +E1Re6AD7oBwALQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFAzCCA+ugAwIBAgIQGLLLuqME8aAPwfLzJkYqSjANBgkqhkiG9w0BAQUFADCB +gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV +BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw +MDBaFw0xOTEyMzEyMzU5NTlaMHIxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVh +dGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9E +TyBDQSBMaW1pdGVkMRgwFgYDVQQDEw9Fc3NlbnRpYWxTU0wgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCt8AiwcsargxIxF3CJhakgEtSYau2A1NHf +5I5ZLdOWIY120j8YC0YZYwvHIPPlC92AGvFaoL0dds23Izp0XmEbdaqb1IX04XiR +0y3hr/yYLgbSeT1awB8hLRyuIVPGOqchfr7tZ291HRqfalsGs2rjsQuqag7nbWzD +ypWMN84hHzWQfdvaGlyoiBSyD8gSIF/F03/o4Tjg27z5H6Gq1huQByH6RSRQXScq +oChBRVt9vKCiL6qbfltTxfEFFld+Edc7tNkBdtzffRDPUanlOPJ7FAB1WfnwWdsX +Pvev5gItpHnBXaIcw5rIp6gLSApqLn8tl2X2xQScRMiZln5+pN0vAgMBAAGjggGD +MIIBfzAfBgNVHSMEGDAWgBQLWOWLxkwVN6RAqTCpIb5HNlpW/zAdBgNVHQ4EFgQU +2svqrVsIXcz//CZUzknlVcY49PgwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQI +MAYBAf8CAQAwIAYDVR0lBBkwFwYKKwYBBAGCNwoDAwYJYIZIAYb4QgQBMD4GA1Ud +IAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwczovL3NlY3VyZS5jb21v +ZG8uY29tL0NQUzBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmNvbW9kb2Nh +LmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDBsBggrBgEFBQcB +AQRgMF4wNgYIKwYBBQUHMAKGKmh0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NvbW9k +b1VUTlNHQ0NBLmNydDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2Eu +Y29tMA0GCSqGSIb3DQEBBQUAA4IBAQAtlzR6QDLqcJcvgTtLeRJ3rvuq1xqo2l/z +odueTZbLN3qo6u6bldudu+Ennv1F7Q5Slqz0J790qpL0pcRDAB8OtXj5isWMcL2a +ejGjKdBZa0wztSz4iw+SY1dWrCRnilsvKcKxudokxeRiDn55w/65g+onO7wdQ7Vu +F6r7yJiIatnyfKH2cboZT7g440LX8NqxwCPf3dfxp+0Jj1agq8MLy6SSgIGSH6lv ++Wwz3D5XxqfyH8wqfOQsTEZf6/Nh9yvENZ+NWPU6g0QO2JOsTGvMd/QDzczc4BxL +XSXaPV7Od4rhPsbXlM1wSTz/Dr0ISKvlUhQVnQ6cGodWaK2cCQBk +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEqzCCA5OgAwIBAgIQLnmDLpCIh+qLjvMabuZ6RDANBgkqhkiG9w0BAQUFADCB +kzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug +Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho +dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xGzAZBgNVBAMTElVUTiAtIERBVEFDb3Jw +IFNHQzAeFw0wNjEyMDEwMDAwMDBaFw0yMDA1MzAxMDQ4MzhaMIGBMQswCQYDVQQG +EwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxm +b3JkMRowGAYDVQQKExFDT01PRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RP +IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEA0ECLi3LjkRv3UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZ +rts7oVewdFFxze1CkU1B/qnI2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAh +TaHYujl8HJ6jJJ3ygxaYqhZ8Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23Iw +ambV4EajcNxo2f8ESIl33rXp+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVD +iOEjPqXSJDlqR6sA1KGzqSX+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ +0o7KBWFxB3NH5YoZEr0ETc5OnKVIrLsm9wIDAQABo4IBCTCCAQUwHwYDVR0jBBgw +FoAUUzLRs89/+uDxoF2FTpLSnkUdtE8wHQYDVR0OBBYEFAtY5YvGTBU3pECpMKkh +vkc2Wlb/MA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MCAGA1UdJQQZ +MBcGCisGAQQBgjcKAwMGCWCGSAGG+EIEATARBgNVHSAECjAIMAYGBFUdIAAwbQYD +VR0fBGYwZDAxoC+gLYYraHR0cDovL2NybC5jb21vZG9jYS5jb20vVVROLURBVEFD +b3JwU0dDLmNybDAvoC2gK4YpaHR0cDovL2NybC5jb21vZG8ubmV0L1VUTi1EQVRB +Q29ycFNHQy5jcmwwDQYJKoZIhvcNAQEFBQADggEBANheksSuFNxDrcKkw2dFBx35 +N6IZxxw3NZETHAfEfUKmDvCGXENrDkTPviRhOkKpzp1Mr3k5cN0OBCBOlZw83rdg +umNDQO1qD4FJRrsek8BL8/jhNkkbb7YMDfKQV4r8bZPyKMf6hgoosxcOWYoutr/N +4axMZmzyVZFWtzK/seR9teg6ti/bspzaUJOOTsWsmn5cnhI8O03GUHCzZSuO92uh +uyXAALv17BZlgQ771KMhlneaqHS8U6rCOVD/CwIJYcyVt9eIavZcxWjTFJUaR1/Z ++y3kL48ThqsxE0ATrG7ttRAwixtQqc7ujMrrfLW5Fj3U+m+SbR6ivfsCSsVwvvE= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEpjCCA46gAwIBAgIQRurwlgVMxeP6Zepun0LGZDANBgkqhkiG9w0BAQUFADBv +MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk +ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF +eHRlcm5hbCBDQSBSb290MB4XDTA1MDYwNzA4MDkxMFoXDTIwMDUzMDEwNDgzOFow +gZMxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJVVDEXMBUGA1UEBxMOU2FsdCBMYWtl +IENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0d29yazEhMB8GA1UECxMY +aHR0cDovL3d3dy51c2VydHJ1c3QuY29tMRswGQYDVQQDExJVVE4gLSBEQVRBQ29y +cCBTR0MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDf7lgQoituVcSO +vy5GCefgCA8uK3oTlBu99raAjmUFkwAevK/iD44ZDRJH7Kyto/oucPjebvtWQhWe +LlzvI94huQV2JxkPT9bDnLS+lBlj8qYRCutTSJy+8ik7FugaoEymyfQYWWjAcPJT +AMBeUIKlVm82+UrgRIagTU7WR25JSstn16bEBbmOHvT8/83nNuCcBWyyMyIV0LTg +zBfAssD0/jI/KSqVe9jyp04PVHyhDYCzCQPB/1zdXpo+vK68R4pqrnHKH7EquF9C +BQvsRjDRcgvK6VZt9e/feL5hurKlrgRMvKisaRWXve/rtIy/NfjUw9EoDlw6n3AY +MyB3xKKvAgMBAAGjggEXMIIBEzAfBgNVHSMEGDAWgBStvZh6NLQm9/rEJlTvA73g +JMtUGjAdBgNVHQ4EFgQUUzLRs89/+uDxoF2FTpLSnkUdtE8wDgYDVR0PAQH/BAQD +AgEGMA8GA1UdEwEB/wQFMAMBAf8wIAYDVR0lBBkwFwYKKwYBBAGCNwoDAwYJYIZI +AYb4QgQBMBEGA1UdIAQKMAgwBgYEVR0gADB7BgNVHR8EdDByMDigNqA0hjJodHRw +Oi8vY3JsLmNvbW9kb2NhLmNvbS9BZGRUcnVzdEV4dGVybmFsQ0FSb290LmNybDA2 +oDSgMoYwaHR0cDovL2NybC5jb21vZG8ubmV0L0FkZFRydXN0RXh0ZXJuYWxDQVJv +b3QuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQBjhpIQsRP6N76OKrYbikP1XK4OFN/3 +aUB/vxpxAAnYv9QkSr/gk/8B2AvGD+x+R5ywXfd8FJ38wDOShFvSg/RS4iJYdPxD +Gz+no1jaA/288Drk7cwSu8m5rnsEoARyv+neLdKnUWYAc9K9fqqeU5Z9abIYPo6t +VlB+99Ww/zliZYKMllfDj/dg9sKNNIf8T0Pl278cqvaGzebfET+NB/dtgxPAOIg5 +YKF+MOHjiD6ku2NvLOmKaCzulmmsBGHhT04OnXJM9nk4yMdIaW+UD3S0vMjPV025 +dXGWDYoGC+vd0PA8fcYumEZqOMcCtci4smV13tqQCLZ3uFMAJctHynNf +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs +IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 +MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux +FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h +bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v +dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt +H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 +uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX +mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX +a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN +E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 +WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD +VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 +Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU +cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx +IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN +AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH +YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 +6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC +Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX +c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a +mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- MIIFFzCCA/+gAwIBAgIRAP+ceCiXnKf8x8mBIBmTckswDQYJKoZIhvcNAQEFBQAw cTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ29tb2RvIENBIExpbWl0ZWQxFzAVBgNV @@ -53,3 +191,59 @@ Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= -----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEhjCCA26gAwIBAgIQUkIGSk83/kNpSHqWZ/9dJzANBgkqhkiG9w0BAQUFADBv +MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk +ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF +eHRlcm5hbCBDQSBSb290MB4XDTA1MDYwNzA4MDkxMFoXDTIwMDUzMDEwNDgzOFow +gZcxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJVVDEXMBUGA1UEBxMOU2FsdCBMYWtl +IENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0d29yazEhMB8GA1UECxMY +aHR0cDovL3d3dy51c2VydHJ1c3QuY29tMR8wHQYDVQQDExZVVE4tVVNFUkZpcnN0 +LUhhcmR3YXJlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsffDOD+0 +qH/POYJRZ9Btn9L/WPPnnyvsDYlUmbk4mRb34CF5SMK7YXQSlh08anLVPBBnOjnt +KxPNZuuVCTOkbJex6MbswXV5nEZejavQav25KlUXEFSzGfCa9vGxXbanbfvgcRdr +ooj7AN/+GjF3DJoBerEy4ysBBzhuw6VeI7xFm3tQwckwj9vlK3rTW/szQB6g1ZgX +vIuHw4nTXaCOsqqq9o5piAbF+okh8widaS4JM5spDUYPjMxJNLBpUb35Bs1orWZM +vD6sYb0KiA7I3z3ufARMnQpea5HW7sftKI2rTYeJc9BupNAeFosU4XZEA39jrOTN +SZzFkvSrMqFIWwIDAQABo4H0MIHxMB8GA1UdIwQYMBaAFK29mHo0tCb3+sQmVO8D +veAky1QaMB0GA1UdDgQWBBShcl8mGyiYQ5VdBzfVhZadS9LDRTAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zARBgNVHSAECjAIMAYGBFUdIAAwewYDVR0f +BHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQWRkVHJ1c3RFeHRl +cm5hbENBUm9vdC5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9BZGRU +cnVzdEV4dGVybmFsQ0FSb290LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAYGQ5WaJD +ZS79+R/WrjO76FMTxIjuIxpszthkWVNTkOg239T88055L9XmjwzvKkFtcb2beDgj +03BLhgz9EqciYhLYzOBR7y3lzQxFoura7X7s9zKa5wU1Xm7CLGhonf+M8cpVh8Qv +sUAG3IQiXG2zzdGbGgozKGYWDL0zwvYH8eOheZTg+NDQ099Shj+p4ckdPoaEsdtf +7uRJQ8E5fc8vlqd1XX5nZ4TlWSBAvzcivwdDtDDhQ4rNA11tuSnZhKf1YmOEhtY3 +vm9nu/9iVzmdDE2yKmE9HZzvmncgoC/uGnKdsJ2/eBMnBwpgEZP1Dy7J72skg/6b +kLRLaIHQwvrgPw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFAzCCA+ugAwIBAgIQTM1KmltFEyGMz5AviytRcTANBgkqhkiG9w0BAQUFADCB +lzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug +Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho +dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3Qt +SGFyZHdhcmUwHhcNMDYwOTE4MDAwMDAwWhcNMjAwNTMwMTA0ODM4WjBxMQswCQYD +VQQGEwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdT +YWxmb3JkMRowGAYDVQQKExFDb21vZG8gQ0EgTGltaXRlZDEXMBUGA1UEAxMOUG9z +aXRpdmVTU0wgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9T3lY +IpPJKD5SEQAvwKkgitctVR4Q57h/4oYqpOxe6eSSWJZUDfMXukGeFZFV78LuACAY +RYMm3yDMPbOhEzEKIVx5g3mrJBVcVvC0lZih2tIb6ha1y7ewwVP5pEba8C4kuGKe +joteK1qWoOpQ6Yj7KCpNmpxIT4O2h65Pxci12f2+P9GnncYsEw3AAcezcPOPabuw +PBDf6wkAhD9u7/zjLbTHXRHM9/Lx9uLjAH4SDt6NfQDKOj32cuh5JaYIFveriP9W +XgkXwFqCBWI0KyhIMpfQhAysExjbnmbHqhSLEWlN8QnTul2piDdi2L8Dm53X5gV+ +wmpSqo0HgOqODvMdAgMBAAGjggFuMIIBajAfBgNVHSMEGDAWgBShcl8mGyiYQ5Vd +BzfVhZadS9LDRTAdBgNVHQ4EFgQUuMoR6QYxedvDlMboGSq8uzUWMaQwDgYDVR0P +AQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwewYDVR0fBHQwcjA4oDagNIYy +aHR0cDovL2NybC5jb21vZG9jYS5jb20vVVROLVVTRVJGaXJzdC1IYXJkd2FyZS5j +cmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9VVE4tVVNFUkZpcnN0LUhh +cmR3YXJlLmNybDCBhgYIKwYBBQUHAQEEejB4MDsGCCsGAQUFBzAChi9odHRwOi8v +Y3J0LmNvbW9kb2NhLmNvbS9VVE5BZGRUcnVzdFNlcnZlckNBLmNydDA5BggrBgEF +BQcwAoYtaHR0cDovL2NydC5jb21vZG8ubmV0L1VUTkFkZFRydXN0U2VydmVyQ0Eu +Y3J0MA0GCSqGSIb3DQEBBQUAA4IBAQAdtOf5GEhd7fpawx3jt++GFclsE0kWDTGM +MVzn2odkjq8SFqRaLZIaOz4hZaoXw5V+QBz9FGkGGM2sMexq8RaeiSY9WyGN6Oj5 +qz2qPMuZ8oZfiFMVBRflqNKFp05Jfdbdx4/OiL9lBeAUtTF37r0qhujop2ot2mUZ +jGfibfZKhWaDtjJNn0IjF9dFQWp2BNStuY9u3MI+6VHyntjzf/tQKvCL/W8NIjYu +zg5G8t6P2jt9HpOs/PQyKw+rAR+lQI/jJJkfXbKqDLnioeeSDJBLU30fKO5WPa8Y +Z0nf1R7CqJgrTEeDgUwuRMLvyGPui3tbMfYmYb95HLCpTqnJUHvi +-----END CERTIFICATE----- diff --git a/anki/collection.py b/anki/collection.py index 66735af5d..c5b2a3e20 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -73,9 +73,8 @@ class _Collection(object): self.crt = int(time.mktime(d.timetuple())) self.sched = Scheduler(self) if not self.conf.get("newBury", False): - mod = self.db.mod - self.sched.unburyCards() - self.db.mod = mod + self.conf['newBury'] = True + self.setMod() def name(self): n = os.path.splitext(os.path.basename(self.path))[0] @@ -148,10 +147,6 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""", def close(self, save=True): "Disconnect from DB." if self.db: - if not self.conf.get("newBury", False): - mod = self.db.mod - self.sched.unburyCards() - self.db.mod = mod if save: self.save() else: @@ -515,13 +510,11 @@ where c.nid = n.id and c.id in %s group by nid""" % ids2str(cids)): afmt = afmt or template['afmt'] for (type, format) in (("q", qfmt), ("a", afmt)): if type == "q": - format = format.replace("{{cloze:", "{{cq:%d:" % ( - data[4]+1)) + format = re.sub("{{(?!type:)(.*?)cloze:", r"{{\1cq-%d:" % (data[4]+1), format) format = format.replace("<%cloze:", "<%%cq:%d:" % ( data[4]+1)) else: - format = format.replace("{{cloze:", "{{ca:%d:" % ( - data[4]+1)) + format = re.sub("{{(.*?)cloze:", r"{{\1ca-%d:" % (data[4]+1), format) format = format.replace("<%cloze:", "<%%ca:%d:" % ( data[4]+1)) fields['FrontSide'] = stripSounds(d['q']) @@ -613,13 +606,19 @@ where c.nid == f.id if self._undo[0] == 1: old = self._undo[2] self.clearUndo() - self._undo = [1, _("Review"), old + [copy.copy(card)]] + wasLeech = card.note().hasTag("leech") or False + self._undo = [1, _("Review"), old + [copy.copy(card)], wasLeech] def _undoReview(self): data = self._undo[2] + wasLeech = self._undo[3] c = data.pop() if not data: self.clearUndo() + # remove leech tag if it didn't have it before + if not wasLeech and c.note().hasTag("leech"): + c.note().delTag("leech") + c.note().flush() # write old data c.flush() # and delete revlog entry @@ -696,6 +695,10 @@ select id from notes where mid not in """ + ids2str(self.models.ids())) self.remNotes(ids) # for each model for m in self.models.all(): + for t in m['tmpls']: + if t['did'] == "None": + t['did'] = None + problems.append(_("Fixed AnkiDroid deck override bug.")) if m['type'] == MODEL_STD: # model with missing req specification if 'req' not in m: @@ -753,6 +756,17 @@ select id from cards where odue > 0 and (type=1 or queue=2) and not odid""") "Fixed %d cards with invalid properties.", cnt) % cnt) self.db.execute("update cards set odue=0 where id in "+ ids2str(ids)) + # cards with odid set when not in a dyn deck + dids = [id for id in self.decks.allIds() if not self.decks.isDyn(id)] + ids = self.db.list(""" +select id from cards where odid > 0 and did in %s""" % ids2str(dids)) + if ids: + cnt = len(ids) + problems.append( + ngettext("Fixed %d card with invalid properties.", + "Fixed %d cards with invalid properties.", cnt) % cnt) + self.db.execute("update cards set odid=0, odue=0 where id in "+ + ids2str(ids)) # tags self.tags.registerNotes() # field cache diff --git a/anki/consts.py b/anki/consts.py index 1b219941b..0eabc0419 100644 --- a/anki/consts.py +++ b/anki/consts.py @@ -38,6 +38,8 @@ DYN_DUE = 6 DYN_REVADDED = 7 DYN_DUEPRIORITY = 8 +DYN_MAX_SIZE = 99999 + # model types MODEL_STD = 0 MODEL_CLOZE = 1 diff --git a/anki/decks.py b/anki/decks.py index aa170b670..180eb4200 100644 --- a/anki/decks.py +++ b/anki/decks.py @@ -197,6 +197,12 @@ class DeckManager(object): deck['collapsed'] = not deck['collapsed'] self.save(deck) + def collapseBrowser(self, did): + deck = self.get(did) + collapsed = deck.get('browserCollapsed', False) + deck['browserCollapsed'] = not collapsed + self.save(deck) + def count(self): return len(self.decks) diff --git a/anki/exporting.py b/anki/exporting.py index cc12e3d5a..f61871d6c 100644 --- a/anki/exporting.py +++ b/anki/exporting.py @@ -20,10 +20,12 @@ class Exporter(object): file.close() def escapeText(self, text): - "Escape newlines, tabs and CSS." + "Escape newlines, tabs, CSS and quotechar." text = text.replace("\n", "
") text = text.replace("\t", " " * 8) text = re.sub("(?i)", "", text) + if "\"" in text: + text = "\"" + text.replace("\"", "\"\"") + "\"" return text def cardIds(self): @@ -134,8 +136,14 @@ class AnkiExporter(Exporter): data) # notes strnids = ids2str(nids.keys()) - notedata = self.src.db.all("select * from notes where id in "+ - strnids) + notedata = [] + for row in self.src.db.all( + "select * from notes where id in "+strnids): + # remove system tags if not exporting scheduling info + if not self.includeSched: + row = list(row) + row[5] = self.removeSystemTags(row[5]) + notedata.append(row) self.dst.db.executemany( "insert into notes values (?,?,?,?,?,?,?,?,?,?,?)", notedata) @@ -206,6 +214,9 @@ class AnkiExporter(Exporter): # overwrite to apply customizations to the deck before it's closed, # such as update the deck description pass + + def removeSystemTags(self, tags): + return self.src.tags.remFromStr("marked leech", tags) # Packaged Anki decks ###################################################################### diff --git a/anki/importing/mnemo.py b/anki/importing/mnemo.py index 93baedd62..1d3284679 100644 --- a/anki/importing/mnemo.py +++ b/anki/importing/mnemo.py @@ -18,7 +18,7 @@ class MnemosyneImporter(NoteImporter): db = DB(self.file) ver = db.scalar( "select value from global_variables where key='version'") - assert ver.startswith('Mnemosyne SQL 1') + assert ver.startswith('Mnemosyne SQL 1') or ver == "2" # gather facts into temp objects curid = None notes = {} diff --git a/anki/importing/supermemo_xml.py b/anki/importing/supermemo_xml.py index 60c7fa0a4..ee1b3da7f 100644 --- a/anki/importing/supermemo_xml.py +++ b/anki/importing/supermemo_xml.py @@ -140,65 +140,29 @@ class SupermemoXmlImporter(NoteImporter): #s = re.sub(u'>',u'>',s) #s = re.sub(u'<',u'<',s) - return unicode(btflsoup(s,convertEntities=btflsoup.HTML_ENTITIES )) + return unicode(btflsoup(s, selfClosingTags=['br','hr','img','wbr'], convertEntities=btflsoup.HTML_ENTITIES)) + def _afactor2efactor(self, af): + # Adapted from - def _unescape(self,s,initilize): - """Note: This method is not used, BeautifulSoup does better job. - """ + # Ranges for A-factors and E-factors + af_min = 1.2 + af_max = 6.9 + ef_min = 1.3 + ef_max = 3.3 - if self._unescape_trtable == None: - self._unescape_trtable = ( - ('€',u'€'), (' ',u' '), ('!',u'!'), ('"',u'"'), ('#',u'#'), ('$',u'$'), ('%',u'%'), ('&',u'&'), (''',u"'"), - ('(',u'('), (')',u')'), ('*',u'*'), ('+',u'+'), (',',u','), ('-',u'-'), ('.',u'.'), ('/',u'/'), ('0',u'0'), - ('1',u'1'), ('2',u'2'), ('3',u'3'), ('4',u'4'), ('5',u'5'), ('6',u'6'), ('7',u'7'), ('8',u'8'), ('9',u'9'), - (':',u':'), (';',u';'), ('<',u'<'), ('=',u'='), ('>',u'>'), ('?',u'?'), ('@',u'@'), ('A',u'A'), ('B',u'B'), - ('C',u'C'), ('D',u'D'), ('E',u'E'), ('F',u'F'), ('G',u'G'), ('H',u'H'), ('I',u'I'), ('J',u'J'), ('K',u'K'), - ('L',u'L'), ('M',u'M'), ('N',u'N'), ('O',u'O'), ('P',u'P'), ('Q',u'Q'), ('R',u'R'), ('S',u'S'), ('T',u'T'), - ('U',u'U'), ('V',u'V'), ('W',u'W'), ('X',u'X'), ('Y',u'Y'), ('Z',u'Z'), ('[',u'['), ('\',u'\\'), (']',u']'), - ('^',u'^'), ('_',u'_'), ('`',u'`'), ('a',u'a'), ('b',u'b'), ('c',u'c'), ('d',u'd'), ('e',u'e'), ('f',u'f'), - ('g',u'g'), ('h',u'h'), ('i',u'i'), ('j',u'j'), ('k',u'k'), ('l',u'l'), ('m',u'm'), ('n',u'n'), - ('o',u'o'), ('p',u'p'), ('q',u'q'), ('r',u'r'), ('s',u's'), ('t',u't'), ('u',u'u'), ('v',u'v'), - ('w',u'w'), ('x',u'x'), ('y',u'y'), ('z',u'z'), ('{',u'{'), ('|',u'|'), ('}',u'}'), ('~',u'~'), - (' ',u' '), ('¡',u'¡'), ('¢',u'¢'), ('£',u'£'), ('¤',u'¤'), ('¥',u'¥'), ('¦',u'¦'), ('§',u'§'), - ('¨',u'¨'), ('©',u'©'), ('ª',u'ª'), ('«',u'«'), ('¬',u'¬'), ('­',u'­'), ('®',u'®'), ('¯',u'¯'), - ('°',u'°'), ('±',u'±'), ('²',u'²'), ('³',u'³'), ('´',u'´'), ('µ',u'µ'), ('¶',u'¶'), ('·',u'·'), - ('¸',u'¸'), ('¹',u'¹'), ('º',u'º'), ('»',u'»'), ('¼',u'¼'), ('½',u'½'), ('¾',u'¾'), ('¿',u'¿'), - ('À',u'À'), ('Á',u'Á'), ('Â',u'Â'), ('Ã',u'Ã'), ('Ä',u'Ä'), ('Å',u'Å'), ('Å',u'Å'), ('Æ',u'Æ'), - ('Ç',u'Ç'), ('È',u'È'), ('É',u'É'), ('Ê',u'Ê'), ('Ë',u'Ë'), ('Ì',u'Ì'), ('Í',u'Í'), ('Î',u'Î'), - ('Ï',u'Ï'), ('Ð',u'Ð'), ('Ñ',u'Ñ'), ('Ò',u'Ò'), ('Ó',u'Ó'), ('Ô',u'Ô'), ('Õ',u'Õ'), ('Ö',u'Ö'), - ('×',u'×'), ('Ø',u'Ø'), ('Ù',u'Ù'), ('Ú',u'Ú'), ('Û',u'Û'), ('Ü',u'Ü'), ('Ý',u'Ý'), ('Þ',u'Þ'), - ('ß',u'ß'), ('à',u'à'), ('á',u'á'), ('â',u'â'), ('ã',u'ã'), ('ä',u'ä'), ('å',u'å'), ('æ',u'æ'), - ('ç',u'ç'), ('è',u'è'), ('é',u'é'), ('ê',u'ê'), ('ë',u'ë'), ('ì',u'ì'), ('í',u'í'), ('í',u'í'), - ('î',u'î'), ('ï',u'ï'), ('ð',u'ð'), ('ñ',u'ñ'), ('ò',u'ò'), ('ó',u'ó'), ('ô',u'ô'), ('õ',u'õ'), - ('ö',u'ö'), ('÷',u'÷'), ('ø',u'ø'), ('ù',u'ù'), ('ú',u'ú'), ('û',u'û'), ('ü',u'ü'), ('ý',u'ý'), - ('þ',u'þ'), ('ÿ',u'ÿ'), ('Ā',u'Ā'), ('ā',u'ā'), ('Ă',u'Ă'), ('ă',u'ă'), ('Ą',u'Ą'), ('ą',u'ą'), - ('Ć',u'Ć'), ('ć',u'ć'), ('Ĉ',u'Ĉ'), ('ĉ',u'ĉ'), ('Ċ',u'Ċ'), ('ċ',u'ċ'), ('Č',u'Č'), ('č',u'č'), - ('Ď',u'Ď'), ('ď',u'ď'), ('Đ',u'Đ'), ('đ',u'đ'), ('Ē',u'Ē'), ('ē',u'ē'), ('Ĕ',u'Ĕ'), ('ĕ',u'ĕ'), - ('Ė',u'Ė'), ('ė',u'ė'), ('Ę',u'Ę'), ('ę',u'ę'), ('Ě',u'Ě'), ('ě',u'ě'), ('Ĝ',u'Ĝ'), ('ĝ',u'ĝ'), - ('Ğ',u'Ğ'), ('ğ',u'ğ'), ('Ġ',u'Ġ'), ('ġ',u'ġ'), ('Ģ',u'Ģ'), ('ģ',u'ģ'), ('Ĥ',u'Ĥ'), ('ĥ',u'ĥ'), - ('Ħ',u'Ħ'), ('ħ',u'ħ'), ('Ĩ',u'Ĩ'), ('ĩ',u'ĩ'), ('Ī',u'Ī'), ('ī',u'ī'), ('Ĭ',u'Ĭ'), ('ĭ',u'ĭ'), - ('Į',u'Į'), ('į',u'į'), ('İ',u'İ'), ('ı',u'ı'), ('IJ',u'IJ'), ('ij',u'ij'), ('Ĵ',u'Ĵ'), ('ĵ',u'ĵ'), - ('Ķ',u'Ķ'), ('ķ',u'ķ'), ('ĸ',u'ĸ'), ('Ĺ',u'Ĺ'), ('ĺ',u'ĺ'), ('Ļ',u'Ļ'), ('ļ',u'ļ'), ('Ľ',u'Ľ'), - ('ľ',u'ľ'), ('Ŀ',u'Ŀ'), ('ŀ',u'ŀ'), ('Ł',u'Ł'), ('ł',u'ł'), ('Ń',u'Ń'), ('ń',u'ń'), ('Ņ',u'Ņ'), - ('ņ',u'ņ'), ('Ň',u'Ň'), ('ň',u'ň'), ('ʼn',u'ʼn'), ('Ŋ',u'Ŋ'), ('ŋ',u'ŋ'), ('Ō',u'Ō'), ('ō',u'ō'), - ('Ŏ',u'Ŏ'), ('ŏ',u'ŏ'), ('Ő',u'Ő'), ('ő',u'ő'), ('Œ',u'Œ'), ('œ',u'œ'), ('Ŕ',u'Ŕ'), ('ŕ',u'ŕ'), - ('Ŗ',u'Ŗ'), ('ŗ',u'ŗ'), ('Ř',u'Ř'), ('ř',u'ř'), ('Ś',u'Ś'), ('ś',u'ś'), ('Ŝ',u'Ŝ'), ('ŝ',u'ŝ'), - ('Ş',u'Ş'), ('ş',u'ş'), ('Š',u'Š'), ('š',u'š'), ('Ţ',u'Ţ'), ('ţ',u'ţ'), ('Ť',u'Ť'), ('ť',u'ť'), - ('Ŧ',u'Ŧ'), ('ŧ',u'ŧ'), ('Ũ',u'Ũ'), ('ũ',u'ũ'), ('Ū',u'Ū'), ('ū',u'ū'), ('Ŭ',u'Ŭ'), ('ŭ',u'ŭ'), - ('Ů',u'Ů'), ('ů',u'ů'), ('Ű',u'Ű'), ('ű',u'ű'), ('Ų',u'Ų'), ('ų',u'ų'), ('Ŵ',u'Ŵ'), ('ŵ',u'ŵ'), - ('Ŷ',u'Ŷ'), ('ŷ',u'ŷ'), ('Ÿ',u'Ÿ'), ('Ź',u'Ź'), ('ź',u'ź'), ('Ż',u'Ż'), ('ż',u'ż'), ('Ž',u'Ž'), - ('ž',u'ž'), ('ſ',u'ſ'), ('Ŕ',u'Ŕ'), ('ŕ',u'ŕ'), ('Ŗ',u'Ŗ'), ('ŗ',u'ŗ'), ('Ř',u'Ř'), ('ř',u'ř'), - ('Ś',u'Ś'), ('ś',u'ś'), ('Ŝ',u'Ŝ'), ('ŝ',u'ŝ'), ('Ş',u'Ş'), ('ş',u'ş'), ('Š',u'Š'), ('š',u'š'), - ('Ţ',u'Ţ'), ('ţ',u'ţ'), ('Ť',u'Ť'), ('Ɂ',u'ť'), ('Ŧ',u'Ŧ'), ('ŧ',u'ŧ'), ('Ũ',u'Ũ'), ('ũ',u'ũ'), - ('Ū',u'Ū'), ('ū',u'ū'), ('Ŭ',u'Ŭ'), ('ŭ',u'ŭ'), ('Ů',u'Ů'), ('ů',u'ů'), ('Ű',u'Ű'), ('ű',u'ű'), - ('Ų',u'Ų'), ('ų',u'ų'), ('Ŵ',u'Ŵ'), ('ŵ',u'ŵ'), ('Ŷ',u'Ŷ'), ('ŷ',u'ŷ'), ('Ÿ',u'Ÿ'), ('Ź',u'Ź'), - ('ź',u'ź'), ('Ż',u'Ż'), ('ż',u'ż'), ('Ž',u'Ž'), ('ž',u'ž'), ('ſ',u'ſ'), - ) + # Sanity checks for the A-factor + if af < af_min: + af = af_min + elif af > af_max: + af = af_max + # Scale af to the range 0..1 + af_scaled = (af - af_min) / (af_max - af_min) + # Rescale to the interval ef_min..ef_max + ef = ef_min + af_scaled * (ef_max - ef_min) - #m = re.match() - #s = s.replace(code[0], code[1]) + return ef ## DEFAULT IMPORTER METHODS @@ -249,7 +213,7 @@ class SupermemoXmlImporter(NoteImporter): nextDue = tLastrep + (float(item.Interval) * 86400.0) remDays = int((nextDue - time.time())/86400) card.due = self.col.sched.today+remDays - card.factor = int(float(item.AFactor.replace(',','.'))*1000) + card.factor = int(self._afactor2efactor(float(item.AFactor.replace(',','.')))*1000) note.cards[0] = card # categories & tags diff --git a/anki/latex.py b/anki/latex.py index 0d447952b..ee89d56e6 100644 --- a/anki/latex.py +++ b/anki/latex.py @@ -7,8 +7,13 @@ from anki.utils import checksum, call, namedtmp, tmpdir, isMac, stripHTML from anki.hooks import addHook from anki.lang import _ -latexCmd = ["latex", "-interaction=nonstopmode"] -latexDviPngCmd = ["dvipng", "-D", "200", "-T", "tight"] +# if you modify these in an add-on, you must make sure to take tmp.tex as the +# input, and output tmp.png as the output file +latexCmds = [ + ["latex", "-interaction=nonstopmode", "tmp.tex"], + ["dvipng", "-D", "200", "-T", "tight", "tmp.dvi", "-o", "tmp.png"] +] + build = True # if off, use existing media but don't create new regexps = { "standard": re.compile(r"\[latex\](.+?)\[/latex\]", re.DOTALL | re.IGNORECASE), @@ -89,14 +94,11 @@ package in the LaTeX header instead.""") % bad oldcwd = os.getcwd() png = namedtmp("tmp.png") try: - # generate dvi + # generate png os.chdir(tmpdir()) - if call(latexCmd + ["tmp.tex"], stdout=log, stderr=log): - return _errMsg("latex", texpath) - # and png - if call(latexDviPngCmd + ["tmp.dvi", "-o", "tmp.png"], - stdout=log, stderr=log): - return _errMsg("dvipng", texpath) + for latexCmd in latexCmds: + if call(latexCmd, stdout=log, stderr=log): + return _errMsg(latexCmd[0], texpath) # add to media shutil.copyfile(png, os.path.join(mdir, fname)) return diff --git a/anki/media.py b/anki/media.py index 9826ae6f1..89fcbe69c 100644 --- a/anki/media.py +++ b/anki/media.py @@ -217,6 +217,7 @@ class MediaManager(object): files = os.listdir(mdir) else: files = local + renamedFiles = False for file in files: if not local: if not os.path.isfile(file): @@ -236,14 +237,20 @@ class MediaManager(object): # delete if we already have the NFC form, otherwise rename if os.path.exists(nfcFile): os.unlink(file) + renamedFiles = True else: os.rename(file, nfcFile) + renamedFiles = True file = nfcFile # compare if nfcFile not in allRefs: unused.append(file) else: allRefs.discard(nfcFile) + # if we renamed any files to nfc format, we must rerun the check + # to make sure the renamed files are not marked as unused + if renamedFiles: + return self.check(local=local) nohave = [x for x in allRefs if not x.startswith("_")] return (nohave, unused, invalid) @@ -333,7 +340,7 @@ class MediaManager(object): # Illegal characters ########################################################################## - _illegalCharReg = re.compile(r'[][><:"/?*^\\|\0]') + _illegalCharReg = re.compile(r'[][><:"/?*^\\|\0\r\n]') def stripIllegal(self, str): return re.sub(self._illegalCharReg, "", str) diff --git a/anki/models.py b/anki/models.py index 8ad5b158d..5249ee128 100644 --- a/anki/models.py +++ b/anki/models.py @@ -569,7 +569,7 @@ select id from notes where mid = ?)""" % " ".join(map), sflds = splitFields(flds) map = self.fieldMap(m) ords = set() - matches = re.findall("{{cloze:(.+?)}}", m['tmpls'][0]['qfmt']) + matches = re.findall("{{[^}]*?cloze:(?:[^}]?:)*(.+?)}}", m['tmpls'][0]['qfmt']) matches += re.findall("<%cloze:(.+?)%>", m['tmpls'][0]['qfmt']) for fname in matches: if fname not in map: diff --git a/anki/sched.py b/anki/sched.py index 7e5bcc7ca..2416ba84b 100644 --- a/anki/sched.py +++ b/anki/sched.py @@ -954,7 +954,9 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)""" def _fillDyn(self, deck): search, limit, order = deck['terms'][0] orderlimit = self._dynOrder(order, limit) - search += " -is:suspended -is:buried -deck:filtered" + if search.strip(): + search = "(%s)" % search + search = "%s -is:suspended -is:buried -deck:filtered" % search try: ids = self.col.findCards(search, order=orderlimit) except: @@ -997,7 +999,7 @@ due = odue, odue = 0, odid = 0, usn = ?, mod = ? where %s""" % lim, elif o == DYN_DUE: t = "c.due" elif o == DYN_DUEPRIORITY: - t = "(case when queue=2 and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 10000+due end)" % ( + t = "(case when queue=2 and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 100000+due end)" % ( self.today, self.today) else: # if we don't understand the term, default to due order diff --git a/anki/sound.py b/anki/sound.py index ba1a7e2a9..f68ab9dd7 100644 --- a/anki/sound.py +++ b/anki/sound.py @@ -204,12 +204,12 @@ addHook("unloadProfile", stopMplayer) ########################################################################## try: + import pyaudio import wave PYAU_FORMAT = pyaudio.paInt16 PYAU_CHANNELS = 1 - PYAU_RATE = 44100 PYAU_INPUT_INDEX = None except: pass @@ -244,12 +244,16 @@ class PyAudioThreadedRecorder(threading.Thread): except NameError: raise Exception( "Pyaudio not installed (recording not supported on OSX10.3)") + + rate = int(p.get_default_input_device_info()['defaultSampleRate']) + stream = p.open(format=PYAU_FORMAT, channels=PYAU_CHANNELS, - rate=PYAU_RATE, + rate=rate, input=True, input_device_index=PYAU_INPUT_INDEX, frames_per_buffer=chunk) + all = [] while not self.finish: try: @@ -267,7 +271,7 @@ class PyAudioThreadedRecorder(threading.Thread): wf = wave.open(processingSrc, 'wb') wf.setnchannels(PYAU_CHANNELS) wf.setsampwidth(p.get_sample_size(PYAU_FORMAT)) - wf.setframerate(PYAU_RATE) + wf.setframerate(rate) wf.writeframes(data) wf.close() diff --git a/anki/stats.py b/anki/stats.py index bddf6119a..7a93dbadd 100644 --- a/anki/stats.py +++ b/anki/stats.py @@ -353,7 +353,7 @@ group by day order by day""" % (self._limit(), lim), tot, period, unit)) if total and tot: perMin = total / float(tot) - perMin = ngettext("%d card/minute", "%d cards/minute", perMin) % perMin + perMin = ngettext("%d card/minute", "%d cards/minute", perMin) % round(perMin) self._line( i, _("Average answer time"), _("%(a)0.1fs (%(b)s)") % dict(a=(tot*60)/total, b=perMin)) @@ -705,7 +705,7 @@ select sum(case when queue=2 and ivl >= 21 then 1 else 0 end), -- mtr sum(case when queue in (1,3) or (queue=2 and ivl < 21) then 1 else 0 end), -- yng/lrn sum(case when queue=0 then 1 else 0 end), -- new -sum(case when queue=-1 then 1 else 0 end) -- susp +sum(case when queue<0 then 1 else 0 end) -- susp from cards where did in %s""" % self._limit()) # Footer diff --git a/anki/stdmodels.py b/anki/stdmodels.py index ef59c718d..955afb990 100644 --- a/anki/stdmodels.py +++ b/anki/stdmodels.py @@ -48,10 +48,11 @@ def addForwardOptionalReverse(col): mm = col.models m = addBasicModel(col) m['name'] = _("Basic (optional reversed card)") - fm = mm.newField(_("Add Reverse")) + av = _("Add Reverse") + fm = mm.newField(av) mm.addField(m, fm) t = mm.newTemplate(_("Card 2")) - t['qfmt'] = "{{#Add Reverse}}{{"+_("Back")+"}}{{/Add Reverse}}" + t['qfmt'] = "{{#%s}}{{%s}}{{/%s}}" % (av, _("Back"), av) t['afmt'] = "{{FrontSide}}\n\n
\n\n"+"{{"+_("Front")+"}}" mm.addTemplate(m, t) return m diff --git a/anki/sync.py b/anki/sync.py index eb48fd591..4569e96e6 100644 --- a/anki/sync.py +++ b/anki/sync.py @@ -757,6 +757,7 @@ class MediaSyncer(object): # if the sanity check failed, force a resync self.col.media.forceResync() return "sanityCheckFailed" + return "success" def removed(self): return self.col.media.removed() diff --git a/anki/template/template.py b/anki/template/template.py index bdddb2422..89b64b52a 100644 --- a/anki/template/template.py +++ b/anki/template/template.py @@ -4,7 +4,7 @@ from anki.hooks import runFilter from anki.template import furigana; furigana.install() from anki.template import hint; hint.install() -clozeReg = r"\{\{c%s::(.*?)(::(.*?))?\}\}" +clozeReg = r"(?s)\{\{c%s::(.*?)(::(.*?))?\}\}" modifiers = {} def modifier(symbol): @@ -158,40 +158,45 @@ class Template(object): return txt # field modifiers - parts = tag_name.split(':',2) + parts = tag_name.split(':') extra = None if len(parts) == 1 or parts[0] == '': return '{unknown field %s}' % tag_name - elif len(parts) == 2: - (mod, tag) = parts - elif len(parts) == 3: - (mod, extra, tag) = parts + else: + mods, tag = parts[:-1], parts[-1] #py3k has *mods, tag = parts txt = get_or_attr(context, tag) + + #Since 'text:' and other mods can affect html on which Anki relies to + #process clozes, we need to make sure clozes are always + #treated after all the other mods, regardless of how they're specified + #in the template, so that {{cloze:text: == {{text:cloze: + #For type:, we return directly since no other mod than cloze (or other + #pre-defined mods) can be present and those are treated separately + mods.reverse() + mods.sort(key=lambda s: not s=="type") - # built-in modifiers - if mod == 'text': - # strip html - if txt: - return stripHTML(txt) - return "" - elif mod == 'type': - # type answer field; convert it to [[type:...]] for the gui code - # to process - return "[[%s]]" % tag_name - elif mod == 'cq' or mod == 'ca': - # cloze deletion - if txt and extra: - return self.clozeText(txt, extra, mod[1]) + for mod in mods: + # built-in modifiers + if mod == 'text': + # strip html + txt = stripHTML(txt) if txt else "" + elif mod == 'type': + # type answer field; convert it to [[type:...]] for the gui code + # to process + return "[[%s]]" % tag_name + elif mod.startswith('cq-') or mod.startswith('ca-'): + # cloze deletion + mod, extra = mod.split("-") + txt = self.clozeText(txt, extra, mod[1]) if txt and extra else "" else: - return "" - else: - # hook-based field modifier - txt = runFilter('fmod_' + mod, txt or '', extra, context, - tag, tag_name); - if txt is None: - return '{unknown field %s}' % tag_name - return txt + # hook-based field modifier + mod, extra = re.search("^(.*?)(?:\((.*)\))?$", mod).groups() + txt = runFilter('fmod_' + mod, txt or '', extra or '', context, + tag, tag_name); + if txt is None: + return '{unknown field %s}' % tag_name + return txt def clozeText(self, txt, ord, type): reg = clozeReg diff --git a/anki/utils.py b/anki/utils.py index 06be1de67..6fab82fa1 100644 --- a/anki/utils.py +++ b/anki/utils.py @@ -17,6 +17,7 @@ import sys import locale from hashlib import sha1 import platform +import traceback from anki.lang import _, ngettext @@ -360,6 +361,8 @@ def invalidFilename(str, dirsep=True): return "/" elif (dirsep or not isWin) and "\\" in str: return "\\" + elif str.strip().startswith("."): + return "." def platDesc(): # we may get an interrupted system call, so try this in a loop @@ -382,3 +385,15 @@ def platDesc(): except: continue return theos + +# Debugging +############################################################################## + +class TimedLog(object): + def __init__(self): + self._last = time.time() + def log(self, s): + path, num, fn, y = traceback.extract_stack(limit=2)[0] + sys.stderr.write("%5dms: %s(): %s\n" % ((time.time() - self._last)*1000, fn, s)) + self._last = time.time() + diff --git a/aqt/about.py b/aqt/about.py index 408c5fa17..97e749a79 100644 --- a/aqt/about.py +++ b/aqt/about.py @@ -31,8 +31,8 @@ system. It's free and open source.") Alex Fraser, Andreas Klauer, Andrew Wright, Bernhard Ibertsberger, Charlene Barina, Christian Krause, Christian Rusche, David Smith, Dave Druelinger, Dotan Cohen, Emilio Wuerges, Emmanuel Jarri, Frank Harper, Gregor Skumavc, H. Mijail, -Ian Lewis, Immanuel Asmus, Iroiro, Jarvik7, -Jin Eun-Deok, Jo Nakashima, Johanna Lindh, Kieran Clancy, LaC, Laurent Steffan, +Houssam Salem, Ian Lewis, Immanuel Asmus, Iroiro, Jarvik7, +Jin Eun-Deok, Jo Nakashima, Johanna Lindh, Julien Baley, Kieran Clancy, LaC, Laurent Steffan, Luca Ban, Luciano Esposito, Marco Giancotti, Marcus Rubeus, Mari Egami, Michael Jürges, Mark Wilbur, Matthew Duggan, Matthew Holtz, Meelis Vasser, Michael Keppler, Michael Montague, Michael Penkov, Michal Čadil, Morteza Salehi, Nathanael Law, Nick Cook, Niklas diff --git a/aqt/browser.py b/aqt/browser.py index 23025aa65..8a4efd980 100644 --- a/aqt/browser.py +++ b/aqt/browser.py @@ -96,10 +96,14 @@ class DataModel(QAbstractTableModel): return elif role == Qt.DisplayRole and section < len(self.activeCols): type = self.columnType(section) + txt = None for stype, name in self.browser.columns: if type == stype: txt = name break + # handle case where extension has set an invalid column type + if not txt: + txt = self.browser.columns[0][1] return txt else: return @@ -500,15 +504,18 @@ class Browser(QMainWindow): def setupSearch(self): self.filterTimer = None + self.form.searchEdit.setLineEdit(FavouritesLineEdit(self.mw, self)) self.connect(self.form.searchButton, SIGNAL("clicked()"), self.onSearch) self.connect(self.form.searchEdit.lineEdit(), SIGNAL("returnPressed()"), self.onSearch) - self.setTabOrder(self.form.searchEdit, self.form.tableView) self.form.searchEdit.setCompleter(None) self.form.searchEdit.addItems(self.mw.pm.profile['searchHistory']) + self.connect(self.form.searchEdit.lineEdit(), + SIGNAL("returnPressed()"), + self.onSearch) def onSearch(self, reset=True): "Careful: if reset is true, the current note is saved." @@ -712,11 +719,9 @@ by clicking on one on the left.""")) def setColumnSizes(self): hh = self.form.tableView.horizontalHeader() - for i in range(len(self.model.activeCols)): - if hh.visualIndex(i) == len(self.model.activeCols) - 1: - hh.setResizeMode(i, QHeaderView.Stretch) - else: - hh.setResizeMode(i, QHeaderView.Interactive) + hh.setResizeMode(QHeaderView.Interactive) + hh.setResizeMode(hh.logicalIndex(len(self.model.activeCols)-1), + QHeaderView.Stretch) # this must be set post-resize or it doesn't work hh.setCascadingSectionResizes(False) @@ -727,9 +732,10 @@ by clicking on one on the left.""")) ###################################################################### class CallbackItem(QTreeWidgetItem): - def __init__(self, root, name, onclick): + def __init__(self, root, name, onclick, oncollapse=None): QTreeWidgetItem.__init__(self, root, [name]) self.onclick = onclick + self.oncollapse = oncollapse def setupTree(self): self.connect( @@ -739,22 +745,31 @@ by clicking on one on the left.""")) p.setColor(QPalette.Base, QColor("#d6dde0")) self.form.tree.setPalette(p) self.buildTree() + self.connect( + self.form.tree, SIGNAL("itemExpanded(QTreeWidgetItem*)"), + lambda item: self.onTreeCollapse(item)) + self.connect( + self.form.tree, SIGNAL("itemCollapsed(QTreeWidgetItem*)"), + lambda item: self.onTreeCollapse(item)) def buildTree(self): self.form.tree.clear() root = self.form.tree self._systemTagTree(root) + self._favTree(root) self._decksTree(root) self._modelTree(root) self._userTagTree(root) - self.form.tree.expandAll() - self.form.tree.setItemsExpandable(False) self.form.tree.setIndentation(15) def onTreeClick(self, item, col): if getattr(item, 'onclick', None): item.onclick() + def onTreeCollapse(self, item): + if getattr(item, 'oncollapse', None): + item.oncollapse() + def setFilter(self, *args): if len(args) == 1: txt = args[0] @@ -804,6 +819,18 @@ by clicking on one on the left.""")) item.setIcon(0, QIcon(":/icons/" + icon)) return root + def _favTree(self, root): + saved = self.col.conf.get('savedFilters', []) + if not saved: + # Don't add favourites to tree if none saved + return + root = self.CallbackItem(root, _("My Searches"), None) + root.setExpanded(True) + root.setIcon(0, QIcon(":/icons/emblem-favorite-dark.png")) + for name, filt in saved.items(): + item = self.CallbackItem(root, name, lambda s=filt: self.setFilter(s)) + item.setIcon(0, QIcon(":/icons/emblem-favorite-dark.png")) + def _userTagTree(self, root): for t in sorted(self.col.tags.all()): if t.lower() == "marked" or t.lower() == "leech": @@ -817,10 +844,13 @@ by clicking on one on the left.""")) def fillGroups(root, grps, head=""): for g in grps: item = self.CallbackItem( - root, g[0], lambda g=g: self.setFilter( - "deck", head+g[0])) + root, g[0], + lambda g=g: self.setFilter("deck", head+g[0]), + lambda g=g: self.mw.col.decks.collapseBrowser(g[1])) item.setIcon(0, QIcon(":/icons/deck16.png")) newhead = head + g[0]+"::" + collapsed = self.mw.col.decks.get(g[1]).get('browserCollapsed', False) + item.setExpanded(not collapsed) fillGroups(item, g[5], newhead) fillGroups(root, grps) @@ -847,7 +877,7 @@ by clicking on one on the left.""")) d = QDialog(self) l = QVBoxLayout() l.setMargin(0) - w = AnkiWebView() + w = AnkiWebView(canCopy=True) l.addWidget(w) w.stdHtml(info + "

" + reps) bb = QDialogButtonBox(QDialogButtonBox.Close) @@ -982,8 +1012,7 @@ where id in %s""" % ids2str(sf)) c(self._previewWindow, SIGNAL("finished(int)"), self._onPreviewFinished) vbox = QVBoxLayout() vbox.setMargin(0) - self._previewWeb = AnkiWebView() - self._previewWeb.setFocusPolicy(Qt.NoFocus) + self._previewWeb = AnkiWebView(True) vbox.addWidget(self._previewWeb) bbox = QDialogButtonBox() self._previewPrev = bbox.addButton("<", QDialogButtonBox.ActionRole) @@ -1193,7 +1222,9 @@ update cards set usn=?, mod=?, did=? where id in """ + scids, frm = aqt.forms.reposition.Ui_Dialog() frm.setupUi(d) (pmin, pmax) = self.col.db.first( - "select min(due), max(due) from cards where type=0") + "select min(due), max(due) from cards where type=0 and odid=0") + pmin = pmin or 0 + pmax = pmax or 0 txt = _("Queue top: %d") % pmin txt += "\n" + _("Queue bottom: %d") % pmax frm.label.setText(txt) @@ -1708,3 +1739,87 @@ a { margin-right: 1em; } self.browser.addTags() elif l == "deletetag": self.browser.deleteTags() + + +# Favourites button +###################################################################### +class FavouritesLineEdit(QLineEdit): + buttonClicked = pyqtSignal(bool) + + def __init__(self, mw, browser, parent=None): + super(FavouritesLineEdit, self).__init__(parent) + self.mw = mw + self.browser = browser + # add conf if missing + if not self.mw.col.conf.has_key('savedFilters'): + self.mw.col.conf['savedFilters'] = {} + self.button = QToolButton(self) + self.button.setStyleSheet('border: 0px;') + self.button.setCursor(Qt.ArrowCursor) + self.button.clicked.connect(self.buttonClicked.emit) + self.setIcon(':/icons/emblem-favorite-off.png') + # flag to raise save or delete dialog on button click + self.doSave = True + # name of current saved filter (if query matches) + self.name = None + self.buttonClicked.connect(self.onClicked) + self.connect(self, SIGNAL("textChanged(QString)"), self.updateButton) + + def resizeEvent(self, event): + buttonSize = self.button.sizeHint() + frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth) + self.button.move(self.rect().right() - frameWidth - buttonSize.width(), + (self.rect().bottom() - buttonSize.height() + 1) / 2) + super(FavouritesLineEdit, self).resizeEvent(event) + + def setIcon(self, path): + self.button.setIcon(QIcon(path)) + + def setText(self, txt): + super(FavouritesLineEdit, self).setText(txt) + self.updateButton() + + def updateButton(self, reset=True): + # If search text is a saved query, switch to the delete button. + # Otherwise show save button. + txt = unicode(self.text()).strip() + for key, value in self.mw.col.conf['savedFilters'].items(): + if txt == value: + self.doSave = False + self.name = key + self.setIcon(QIcon(":/icons/emblem-favorite.png")) + return + self.doSave = True + self.setIcon(QIcon(":/icons/emblem-favorite-off.png")) + + def onClicked(self): + if self.doSave: + self.saveClicked() + else: + self.deleteClicked() + + def saveClicked(self): + txt = unicode(self.text()).strip() + dlg = QInputDialog(self) + dlg.setInputMode(QInputDialog.TextInput) + dlg.setLabelText(_("The current search terms will be added as a new " + "item in the sidebar.\n" + "Search name:")) + dlg.setWindowTitle(_("Save search")) + ok = dlg.exec_() + name = dlg.textValue() + if ok: + self.mw.col.conf['savedFilters'][name] = txt + + self.updateButton() + self.browser.setupTree() + + def deleteClicked(self): + msg = _('Remove "%s" from your saved searches?') % self.name + ok = QMessageBox.question(self, _('Remove search'), + msg, QMessageBox.Yes, QMessageBox.No) + + if ok == QMessageBox.Yes: + self.mw.col.conf['savedFilters'].pop(self.name, None) + self.updateButton() + self.browser.setupTree() diff --git a/aqt/clayout.py b/aqt/clayout.py index 2d3b858e1..aa5f423e6 100644 --- a/aqt/clayout.py +++ b/aqt/clayout.py @@ -120,9 +120,9 @@ class CardLayout(QDialog): self.model, joinFields(self.note.fields))) for g in pform.groupBox, pform.groupBox_2: g.setTitle(g.title() + _(" (1 of %d)") % max(cnt, 1)) - pform.frontWeb = AnkiWebView() + pform.frontWeb = AnkiWebView(True) pform.frontPrevBox.addWidget(pform.frontWeb) - pform.backWeb = AnkiWebView() + pform.backWeb = AnkiWebView(True) pform.backPrevBox.addWidget(pform.backWeb) for wig in pform.frontWeb, pform.backWeb: wig.page().setLinkDelegationPolicy( diff --git a/aqt/customstudy.py b/aqt/customstudy.py index 8da5b7a1a..a33f728fa 100644 --- a/aqt/customstudy.py +++ b/aqt/customstudy.py @@ -41,7 +41,7 @@ class CustomStudy(QDialog): def onRadioChange(self, idx): f = self.form; sp = f.spin - smin = 1; smax = 9999; sval = 1 + smin = 1; smax = DYN_MAX_SIZE; sval = 1 post = _("cards") tit = "" spShow = True @@ -127,15 +127,15 @@ class CustomStudy(QDialog): # and then set various options if i == RADIO_FORGOT: dyn['delays'] = [1] - dyn['terms'][0] = ['rated:%d:1' % spin, 9999, DYN_RANDOM] + dyn['terms'][0] = ['rated:%d:1' % spin, DYN_MAX_SIZE, DYN_RANDOM] dyn['resched'] = False elif i == RADIO_AHEAD: dyn['delays'] = None - dyn['terms'][0] = ['prop:due<=%d' % spin, 9999, DYN_DUE] + dyn['terms'][0] = ['prop:due<=%d' % spin, DYN_MAX_SIZE, DYN_DUE] dyn['resched'] = True elif i == RADIO_PREVIEW: dyn['delays'] = None - dyn['terms'][0] = ['is:new added:%s'%spin, 9999, DYN_OLDEST] + dyn['terms'][0] = ['is:new added:%s'%spin, DYN_MAX_SIZE, DYN_OLDEST] dyn['resched'] = False elif i == RADIO_CRAM: dyn['delays'] = None diff --git a/aqt/deckbrowser.py b/aqt/deckbrowser.py index 04357d969..74ad1b0a3 100644 --- a/aqt/deckbrowser.py +++ b/aqt/deckbrowser.py @@ -17,6 +17,7 @@ class DeckBrowser(object): self.mw = mw self.web = mw.web self.bottom = aqt.toolbar.BottomBar(mw, mw.bottomWeb) + self.scrollPos = QPoint(0, 0) def show(self): clearAudioQueue() @@ -65,6 +66,7 @@ class DeckBrowser(object): key = unicode(evt.text()) def _selDeck(self, did): + self.scrollPos = self.web.page().mainFrame().scrollPosition() self.mw.col.decks.select(did) self.mw.onOverview() @@ -152,7 +154,7 @@ body { margin: 1em; -webkit-user-select: none; } if self.web.key == "deckBrowser": return self.web.page().mainFrame().scrollPosition() else: - return QPoint(0,0) + return self.scrollPos def _renderStats(self): cards, thetime = self.mw.col.db.first(""" diff --git a/aqt/editor.py b/aqt/editor.py index e347d6d7e..bd509338c 100644 --- a/aqt/editor.py +++ b/aqt/editor.py @@ -21,7 +21,7 @@ import anki.js from BeautifulSoup import BeautifulSoup pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg") -audio = ("wav", "mp3", "ogg", "flac", "mp4", "swf", "mov", "mpeg", "mkv") +audio = ("wav", "mp3", "ogg", "flac", "mp4", "swf", "mov", "mpeg", "mkv", "m4a") _html = """ %s