2019-02-05 04:59:03 +01:00
# Copyright: Ankitects Pty Ltd and contributors
2012-12-21 08:51:59 +01:00
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
2020-02-08 23:59:29 +01:00
from __future__ import annotations
2021-01-05 11:43:37 +01:00
from concurrent . futures import Future
2019-12-20 10:19:03 +01:00
from copy import deepcopy
2020-02-17 16:26:21 +01:00
from dataclasses import dataclass
2021-01-30 11:37:29 +01:00
from typing import Any
2012-12-21 08:51:59 +01:00
import aqt
2021-01-31 06:55:08 +01:00
from anki . decks import DeckTreeNode
2021-02-26 11:32:40 +01:00
from anki . errors import DeckIsFilteredError
2021-02-21 06:50:41 +01:00
from anki . utils import intTime
2020-01-22 01:46:35 +01:00
from aqt import AnkiQt , gui_hooks
2019-12-20 10:19:03 +01:00
from aqt . qt import *
2020-01-20 11:10:38 +01:00
from aqt . sound import av_player
2020-01-22 01:46:35 +01:00
from aqt . toolbar import BottomBar
2021-02-21 06:50:41 +01:00
from aqt . utils import (
TR ,
askUser ,
getOnlyText ,
openLink ,
shortcut ,
showInfo ,
2021-02-26 11:32:40 +01:00
showWarning ,
2021-02-21 06:50:41 +01:00
tr ,
)
2019-12-20 10:19:03 +01:00
2012-12-21 08:51:59 +01:00
2020-02-08 23:59:29 +01:00
class DeckBrowserBottomBar :
2021-02-01 14:28:21 +01:00
def __init__ ( self , deck_browser : DeckBrowser ) - > None :
2020-02-08 23:59:29 +01:00
self . deck_browser = deck_browser
2020-02-17 16:26:21 +01:00
@dataclass
class DeckBrowserContent :
""" Stores sections of HTML content that the deck browser will be
populated with .
2020-08-31 05:29:28 +02:00
2020-02-17 16:26:21 +01:00
Attributes :
tree { str } - - HTML of the deck tree section
stats { str } - - HTML of the stats section
"""
tree : str
stats : str
2020-02-16 19:29:01 +01:00
2020-05-16 02:52:14 +02:00
@dataclass
class RenderDeckNodeContext :
current_deck_id : int
2017-02-06 23:21:33 +01:00
class DeckBrowser :
2020-05-16 02:52:14 +02:00
_dueTree : DeckTreeNode
2012-12-21 08:51:59 +01:00
2020-01-22 01:46:35 +01:00
def __init__ ( self , mw : AnkiQt ) - > None :
2012-12-21 08:51:59 +01:00
self . mw = mw
self . web = mw . web
2020-01-22 01:46:35 +01:00
self . bottom = BottomBar ( mw , mw . bottomWeb )
2014-02-17 03:04:36 +01:00
self . scrollPos = QPoint ( 0 , 0 )
2021-02-21 06:50:41 +01:00
self . _v1_message_dismissed_at = 0
2012-12-21 08:51:59 +01:00
2021-02-01 14:28:21 +01:00
def show ( self ) - > None :
2020-01-20 11:10:38 +01:00
av_player . stop_and_clear_queue ( )
2020-02-08 23:59:29 +01:00
self . web . set_bridge_command ( self . _linkHandler , self )
2012-12-21 08:51:59 +01:00
self . _renderPage ( )
2020-01-31 04:31:31 +01:00
# redraw top bar for theme change
2020-06-02 05:23:01 +02:00
self . mw . toolbar . redraw ( )
2012-12-21 08:51:59 +01:00
2021-02-01 14:28:21 +01:00
def refresh ( self ) - > None :
2012-12-21 08:51:59 +01:00
self . _renderPage ( )
# Event handlers
##########################################################################
2021-01-30 11:37:29 +01:00
def _linkHandler ( self , url : str ) - > Any :
2012-12-21 08:51:59 +01:00
if " : " in url :
2021-01-25 16:33:18 +01:00
( cmd , arg ) = url . split ( " : " , 1 )
2012-12-21 08:51:59 +01:00
else :
cmd = url
if cmd == " open " :
self . _selDeck ( arg )
elif cmd == " opts " :
self . _showOptions ( arg )
elif cmd == " shared " :
self . _onShared ( )
elif cmd == " import " :
self . mw . onImport ( )
elif cmd == " create " :
2021-02-24 13:59:38 +01:00
self . _on_create ( )
2012-12-21 08:51:59 +01:00
elif cmd == " drag " :
2021-01-30 11:37:29 +01:00
source , target = arg . split ( " , " )
self . _handle_drag_and_drop ( int ( source ) , int ( target or 0 ) )
2012-12-21 08:51:59 +01:00
elif cmd == " collapse " :
2020-05-16 02:52:14 +02:00
self . _collapse ( int ( arg ) )
2021-02-21 06:50:41 +01:00
elif cmd == " v2upgrade " :
self . _confirm_upgrade ( )
elif cmd == " v2upgradeinfo " :
openLink ( " https://faqs.ankiweb.net/the-anki-2.1-scheduler.html " )
elif cmd == " v2upgradelater " :
self . _v1_message_dismissed_at = intTime ( )
self . refresh ( )
2016-05-31 10:51:40 +02:00
return False
2012-12-21 08:51:59 +01:00
2021-02-02 15:00:29 +01:00
def _selDeck ( self , did : str ) - > None :
self . mw . col . decks . select ( int ( did ) )
2012-12-21 08:51:59 +01:00
self . mw . onOverview ( )
# HTML generation
##########################################################################
_body = """
< center >
< table cellspacing = 0 cellpading = 3 >
% ( tree ) s
< / table >
< br >
% ( stats ) s
< / center >
"""
2021-02-02 15:00:29 +01:00
def _renderPage ( self , reuse : bool = False ) - > None :
2012-12-21 08:51:59 +01:00
if not reuse :
2020-05-16 02:52:14 +02:00
self . _dueTree = self . mw . col . sched . deck_due_tree ( )
2019-04-29 08:46:13 +02:00
self . __renderPage ( None )
2019-06-01 08:35:19 +02:00
return
2018-06-12 05:46:15 +02:00
self . web . evalWithCallback ( " window.pageYOffset " , self . __renderPage )
2021-02-02 15:00:29 +01:00
def __renderPage ( self , offset : int ) - > None :
2020-02-17 16:26:21 +01:00
content = DeckBrowserContent (
2020-08-31 05:29:28 +02:00
tree = self . _renderDeckTree ( self . _dueTree ) ,
stats = self . _renderStats ( ) ,
2020-02-16 19:29:01 +01:00
)
2020-02-17 16:26:21 +01:00
gui_hooks . deck_browser_will_render_content ( self , content )
2019-12-23 01:34:10 +01:00
self . web . stdHtml (
2021-02-21 06:50:41 +01:00
self . _v1_upgrade_message ( ) + self . _body % content . __dict__ ,
2020-11-01 05:26:58 +01:00
css = [ " css/deckbrowser.css " ] ,
2020-12-21 04:20:55 +01:00
js = [
2020-12-28 14:18:07 +01:00
" js/vendor/jquery.min.js " ,
2020-12-30 12:07:02 +01:00
" js/vendor/jquery-ui.min.js " ,
2020-12-21 04:20:55 +01:00
" js/deckbrowser.js " ,
] ,
2020-02-12 22:00:13 +01:00
context = self ,
2019-12-23 01:34:10 +01:00
)
2012-12-21 08:51:59 +01:00
self . web . key = " deckBrowser "
self . _drawButtons ( )
2019-04-29 08:46:13 +02:00
if offset is not None :
self . _scrollToOffset ( offset )
2020-04-09 05:40:19 +02:00
gui_hooks . deck_browser_did_render ( self )
2012-12-21 08:51:59 +01:00
2021-02-02 15:00:29 +01:00
def _scrollToOffset ( self , offset : int ) - > None :
2018-06-12 05:46:15 +02:00
self . web . eval ( " $(function() { window.scrollTo(0, %d , ' instant ' ); }); " % offset )
2012-12-21 08:51:59 +01:00
2021-02-01 14:28:21 +01:00
def _renderStats ( self ) - > str :
2021-01-03 14:54:44 +01:00
return ' <div id= " studiedToday " ><span> {} </span></div> ' . format (
self . mw . col . studied_today ( ) ,
)
2012-12-21 08:51:59 +01:00
2020-05-16 02:52:14 +02:00
def _renderDeckTree ( self , top : DeckTreeNode ) - > str :
buf = """
2020-07-22 03:00:39 +02:00
< tr > < th colspan = 5 align = start > % s < / th > < th class = count > % s < / th >
2016-05-31 10:51:40 +02:00
< th class = count > % s < / th > < th class = optscol > < / th > < / tr > """ % (
2020-11-17 08:42:43 +01:00
tr ( TR . DECKS_DECK ) ,
2020-05-16 02:52:14 +02:00
tr ( TR . STATISTICS_DUE_COUNT ) ,
2020-11-17 08:42:43 +01:00
tr ( TR . ACTIONS_NEW ) ,
2020-05-16 02:52:14 +02:00
)
buf + = self . _topLevelDragRow ( )
ctx = RenderDeckNodeContext ( current_deck_id = self . mw . col . conf [ " curDeck " ] )
for child in top . children :
buf + = self . _render_deck_node ( child , ctx )
2012-12-21 08:51:59 +01:00
return buf
2020-05-16 02:52:14 +02:00
def _render_deck_node ( self , node : DeckTreeNode , ctx : RenderDeckNodeContext ) - > str :
if node . collapsed :
2012-12-21 08:51:59 +01:00
prefix = " + "
2020-05-16 02:02:08 +02:00
else :
prefix = " - "
2020-05-16 02:52:14 +02:00
due = node . review_count + node . learn_count
2019-12-23 01:34:10 +01:00
2021-02-01 14:28:21 +01:00
def indent ( ) - > str :
2020-05-16 02:52:14 +02:00
return " " * 6 * ( node . level - 1 )
2019-12-23 01:34:10 +01:00
2020-05-16 02:52:14 +02:00
if node . deck_id == ctx . current_deck_id :
2019-12-23 01:34:10 +01:00
klass = " deck current "
2012-12-21 08:51:59 +01:00
else :
2019-12-23 01:34:10 +01:00
klass = " deck "
2020-05-16 02:52:14 +02:00
buf = " <tr class= ' %s ' id= ' %d ' > " % ( klass , node . deck_id )
2012-12-21 08:51:59 +01:00
# deck link
2020-05-16 02:52:14 +02:00
if node . children :
2019-12-23 01:34:10 +01:00
collapse = (
" <a class=collapse href=# onclick= ' return pycmd( \" collapse: %d \" ) ' > %s </a> "
2020-05-16 02:52:14 +02:00
% ( node . deck_id , prefix )
2019-12-23 01:34:10 +01:00
)
2012-12-21 08:51:59 +01:00
else :
collapse = " <span class=collapse></span> "
2020-05-16 02:52:14 +02:00
if node . filtered :
2012-12-21 08:51:59 +01:00
extraclass = " filtered "
else :
extraclass = " "
buf + = """
2016-05-31 10:51:40 +02:00
< td class = decktd colspan = 5 > % s % s < a class = " deck %s "
2019-12-23 01:34:10 +01:00
href = # onclick="return pycmd('open:%d')">%s</a></td>""" % (
indent ( ) ,
collapse ,
extraclass ,
2020-05-16 02:52:14 +02:00
node . deck_id ,
node . name ,
2019-12-23 01:34:10 +01:00
)
2012-12-21 08:51:59 +01:00
# due counts
2021-02-01 14:28:21 +01:00
def nonzeroColour ( cnt : int , klass : str ) - > str :
2012-12-21 08:51:59 +01:00
if not cnt :
2020-01-23 06:08:10 +01:00
klass = " zero-count "
return f ' <span class= " { klass } " > { cnt } </span> '
2019-12-23 01:34:10 +01:00
2012-12-21 08:51:59 +01:00
buf + = " <td align=right> %s </td><td align=right> %s </td> " % (
2020-01-23 06:08:10 +01:00
nonzeroColour ( due , " review-count " ) ,
2020-05-16 02:52:14 +02:00
nonzeroColour ( node . new_count , " new-count " ) ,
2019-12-23 01:34:10 +01:00
)
2012-12-21 08:51:59 +01:00
# options
2019-12-23 01:34:10 +01:00
buf + = (
" <td align=center class=opts><a onclick= ' return pycmd( \" opts: %d \" ); ' > "
2020-05-16 02:52:14 +02:00
" <img src= ' /_anki/imgs/gears.svg ' class=gears></a></td></tr> " % node . deck_id
2019-12-23 01:34:10 +01:00
)
2012-12-21 08:51:59 +01:00
# children
2020-05-16 02:52:14 +02:00
if not node . collapsed :
for child in node . children :
buf + = self . _render_deck_node ( child , ctx )
2012-12-21 08:51:59 +01:00
return buf
2021-02-01 14:28:21 +01:00
def _topLevelDragRow ( self ) - > str :
2012-12-21 08:51:59 +01:00
return " <tr class= ' top-level-drag-row ' ><td colspan= ' 6 ' > </td></tr> "
# Options
##########################################################################
2020-05-22 02:53:20 +02:00
def _showOptions ( self , did : str ) - > None :
2012-12-21 08:51:59 +01:00
m = QMenu ( self . mw )
2020-11-17 08:42:43 +01:00
a = m . addAction ( tr ( TR . ACTIONS_RENAME ) )
2020-05-22 02:53:20 +02:00
qconnect ( a . triggered , lambda b , did = did : self . _rename ( int ( did ) ) )
2020-11-17 08:42:43 +01:00
a = m . addAction ( tr ( TR . ACTIONS_OPTIONS ) )
2020-01-15 22:41:23 +01:00
qconnect ( a . triggered , lambda b , did = did : self . _options ( did ) )
2020-11-17 08:42:43 +01:00
a = m . addAction ( tr ( TR . ACTIONS_EXPORT ) )
2021-02-01 11:59:18 +01:00
qconnect ( a . triggered , lambda b , did = did : self . _export ( int ( did ) ) )
2020-11-17 08:42:43 +01:00
a = m . addAction ( tr ( TR . ACTIONS_DELETE ) )
2020-05-22 02:53:20 +02:00
qconnect ( a . triggered , lambda b , did = did : self . _delete ( int ( did ) ) )
2020-05-22 03:27:40 +02:00
gui_hooks . deck_browser_will_show_options_menu ( m , int ( did ) )
2012-12-21 08:51:59 +01:00
m . exec_ ( QCursor . pos ( ) )
2021-02-01 11:59:18 +01:00
def _export ( self , did : int ) - > None :
2014-06-20 02:13:12 +02:00
self . mw . onExport ( did = did )
2020-05-22 02:53:20 +02:00
def _rename ( self , did : int ) - > None :
2020-11-17 08:42:43 +01:00
self . mw . checkpoint ( tr ( TR . ACTIONS_RENAME_DECK ) )
2012-12-21 08:51:59 +01:00
deck = self . mw . col . decks . get ( did )
2019-12-23 01:34:10 +01:00
oldName = deck [ " name " ]
2020-11-17 08:42:43 +01:00
newName = getOnlyText ( tr ( TR . DECKS_NEW_DECK_NAME ) , default = oldName )
2013-06-12 04:12:03 +02:00
newName = newName . replace ( ' " ' , " " )
2012-12-21 08:51:59 +01:00
if not newName or newName == oldName :
return
try :
self . mw . col . decks . rename ( deck , newName )
2020-05-22 02:47:14 +02:00
gui_hooks . sidebar_should_refresh_decks ( )
2021-02-26 11:32:40 +01:00
except DeckIsFilteredError as err :
showWarning ( str ( err ) )
2021-02-01 11:23:48 +01:00
return
2012-12-21 08:51:59 +01:00
self . show ( )
2021-02-02 15:00:29 +01:00
def _options ( self , did : str ) - > None :
2012-12-21 08:51:59 +01:00
# select the deck first, because the dyn deck conf assumes the deck
# we're editing is the current one
2021-02-02 15:00:29 +01:00
self . mw . col . decks . select ( int ( did ) )
2012-12-21 08:51:59 +01:00
self . mw . onDeckConf ( )
2020-05-16 02:52:14 +02:00
def _collapse ( self , did : int ) - > None :
2012-12-21 08:51:59 +01:00
self . mw . col . decks . collapse ( did )
2020-05-16 05:05:20 +02:00
node = self . mw . col . decks . find_deck_in_tree ( self . _dueTree , did )
if node :
2020-05-16 02:52:14 +02:00
node . collapsed = not node . collapsed
2020-05-16 05:05:20 +02:00
self . _renderPage ( reuse = True )
2020-05-16 02:52:14 +02:00
2021-01-30 11:37:29 +01:00
def _handle_drag_and_drop ( self , source : int , target : int ) - > None :
2021-03-01 00:58:12 +01:00
try :
self . mw . col . decks . drag_drop_decks ( [ source ] , target )
except Exception as e :
showWarning ( str ( e ) )
return
2021-01-30 11:37:29 +01:00
gui_hooks . sidebar_should_refresh_decks ( )
2012-12-21 08:51:59 +01:00
self . show ( )
2021-01-05 11:28:19 +01:00
def ask_delete_deck ( self , did : int ) - > bool :
2012-12-21 08:51:59 +01:00
deck = self . mw . col . decks . get ( did )
2021-01-05 11:50:54 +01:00
if deck [ " dyn " ] :
return True
count = self . mw . col . decks . card_count ( did , include_subdecks = True )
if not count :
return True
extra = tr ( TR . DECKS_IT_HAS_CARD , count = count )
if askUser (
2021-02-11 01:09:06 +01:00
f " { tr ( TR . DECKS_ARE_YOU_SURE_YOU_WISH_TO , val = deck [ ' name ' ] ) } { extra } "
2019-12-23 01:34:10 +01:00
) :
2021-01-05 11:28:19 +01:00
return True
return False
def _delete ( self , did : int ) - > None :
if self . ask_delete_deck ( did ) :
2021-01-05 11:43:37 +01:00
2021-02-01 14:28:21 +01:00
def do_delete ( ) - > None :
2021-01-05 11:43:37 +01:00
return self . mw . col . decks . rem ( did , True )
2021-02-01 14:28:21 +01:00
def on_done ( fut : Future ) - > None :
2021-01-05 11:43:37 +01:00
self . show ( )
res = fut . result ( ) # Required to check for errors
2021-01-05 11:28:19 +01:00
self . mw . checkpoint ( tr ( TR . DECKS_DELETE_DECK ) )
2021-01-05 11:43:37 +01:00
self . mw . taskman . with_progress ( do_delete , on_done )
2012-12-21 08:51:59 +01:00
# Top buttons
######################################################################
2017-07-15 15:39:01 +02:00
drawLinks = [
2020-11-17 08:42:43 +01:00
[ " " , " shared " , tr ( TR . DECKS_GET_SHARED ) ] ,
[ " " , " create " , tr ( TR . DECKS_CREATE_DECK ) ] ,
[ " Ctrl+Shift+I " , " import " , tr ( TR . DECKS_IMPORT_FILE ) ] ,
2017-07-15 15:39:01 +02:00
]
2021-02-01 14:28:21 +01:00
def _drawButtons ( self ) - > None :
2012-12-21 08:51:59 +01:00
buf = " "
2017-08-06 17:03:11 +02:00
drawLinks = deepcopy ( self . drawLinks )
for b in drawLinks :
2012-12-21 08:51:59 +01:00
if b [ 0 ] :
2020-11-17 12:47:47 +01:00
b [ 0 ] = tr ( TR . ACTIONS_SHORTCUT_KEY , val = shortcut ( b [ 0 ] ) )
2012-12-21 08:51:59 +01:00
buf + = """
2019-12-23 01:34:10 +01:00
< button title = ' %s ' onclick = ' pycmd( \" %s \" ); ' > % s < / button > """ % tuple(
b
)
2020-02-12 22:00:13 +01:00
self . bottom . draw (
buf = buf ,
link_handler = self . _linkHandler ,
web_context = DeckBrowserBottomBar ( self ) ,
2020-02-08 23:59:29 +01:00
)
2012-12-21 08:51:59 +01:00
2021-02-01 14:28:21 +01:00
def _onShared ( self ) - > None :
2021-02-11 01:09:06 +01:00
openLink ( f " { aqt . appShared } decks/ " )
2021-02-21 06:50:41 +01:00
2021-02-24 13:59:38 +01:00
def _on_create ( self ) - > None :
deck = getOnlyText ( tr ( TR . DECKS_NAME_FOR_DECK ) )
if deck :
try :
self . mw . col . decks . id ( deck )
2021-02-26 11:32:40 +01:00
except DeckIsFilteredError as err :
showWarning ( str ( err ) )
2021-02-24 13:59:38 +01:00
return
gui_hooks . sidebar_should_refresh_decks ( )
self . refresh ( )
2021-02-21 06:50:41 +01:00
######################################################################
def _v1_upgrade_message ( self ) - > str :
if self . mw . col . schedVer ( ) == 2 :
return " "
if ( intTime ( ) - self . _v1_message_dismissed_at ) < 86_400 :
return " "
return f """
< center >
< div class = callout >
< div >
{ tr ( TR . SCHEDULING_UPDATE_SOON ) }
< / div >
< div >
< button onclick = ' pycmd( " v2upgrade " ) ' >
{ tr ( TR . SCHEDULING_UPDATE_BUTTON ) }
< / button >
< button onclick = ' pycmd( " v2upgradeinfo " ) ' >
{ tr ( TR . SCHEDULING_UPDATE_MORE_INFO_BUTTON ) }
< / button >
< button onclick = ' pycmd( " v2upgradelater " ) ' >
{ tr ( TR . SCHEDULING_UPDATE_LATER_BUTTON ) }
< / button >
< / div >
< / div >
< / center >
"""
def _confirm_upgrade ( self ) - > None :
self . mw . col . modSchema ( check = True )
self . mw . col . upgrade_to_v2_scheduler ( )
# not translated, as 2.15 should not be too far off
2021-02-21 10:38:26 +01:00
if askUser (
" Do you use AnkiDroid <= 2.14, or plan to use it in the near future? If unsure, choose No. You can adjust the setting later in the preferences screen. " ,
defaultno = True ,
) :
2021-02-21 06:50:41 +01:00
prefs = self . mw . col . get_preferences ( )
prefs . sched . new_timezone = False
self . mw . col . set_preferences ( prefs )
showInfo ( tr ( TR . SCHEDULING_UPDATE_DONE ) )
self . refresh ( )