diff --git a/ankihub/gui/reviewer.py b/ankihub/gui/reviewer.py index 7ac31d33e..2851c0bd2 100644 --- a/ankihub/gui/reviewer.py +++ b/ankihub/gui/reviewer.py @@ -9,7 +9,7 @@ import aqt.webview from anki.cards import Card from anki.notes import Note -from aqt import colors +from aqt import QTimer, colors, qconnect from aqt.gui_hooks import ( reviewer_did_show_answer, reviewer_did_show_question, @@ -124,6 +124,12 @@ def _setup_ui(self): self.content_webview = aqt.webview.AnkiWebView() self.content_webview.setMinimumWidth(self.original_mw_min_width) + self.update_header_button_timer = QTimer(self.content_webview) + qconnect( + self.update_header_button_timer.timeout, self._update_header_button_state + ) + self.update_header_button_timer.start(200) + self.url_page = CustomWebPage(self.profile, self.content_webview._onBridgeCmd) self.interceptor = AuthenticationRequestInterceptor(self.content_webview) self.url_page.profile().setUrlRequestInterceptor(self.interceptor) @@ -190,6 +196,18 @@ def update_tabs( self._update_content_webview() self._update_header_webview() + def _update_header_button_state(self): + if not self.resources: + return + + # We only want to enable the "Open in Browser" button if the content is not hosted on AnkiHub. + enable_open_in_browser_button = not self.get_content_url().startswith( + config.app_url + ) + self.header_webview.eval( + f"setOpenInBrowserButtonState({'true' if enable_open_in_browser_button else 'false'});" + ) + def _update_content_webview(self): if not self.resources: self.set_content_url(None) @@ -357,13 +375,6 @@ def _add_ankihub_ai_and_sidebar_and_buttons(web_content: WebContent, context): return feature_flags = config.get_feature_flags() - if not (feature_flags.get("mh_integration") or feature_flags.get("chatbot")): - return - - if feature_flags.get("mh_integration"): - ankihub_ai_js_template_name = "ankihub_ai.js" - else: - ankihub_ai_js_template_name = "ankihub_ai_old.js" # TODO This condition is not placed correctly. It should be used to # show/hide the AnkiHub AI chatbot button when the reviewer_did_show_question hook is called. @@ -371,7 +382,18 @@ def _add_ankihub_ai_and_sidebar_and_buttons(web_content: WebContent, context): # shouldn't be shown for and (maybe) isn't shown for notes it should be shown for in some cases. # However, we don't have to fix it for the old chatbot implementation, because we will switch # to a new one soon. For the new implementation, we should implement the correct logic. - if _related_ah_deck_has_note_embeddings(aqt.mw.reviewer.card.note()): + if feature_flags.get("chatbot"): + if feature_flags.get("mh_integration"): + ankihub_ai_js_template_name = "ankihub_ai.js" + else: + # The new chatbot js (ankihub_ai.js) doesn't show a button, so we can always set it up. + # However, the old chatbot js (ankihub_ai_old.js) shows a button when executed, so we only + # want to execute it when the note has note embeddings. + if not _related_ah_deck_has_note_embeddings(aqt.mw.reviewer.card.note()): + return + + ankihub_ai_js_template_name = "ankihub_ai_old.js" + ankihub_ai_js = get_ankihub_ai_js( template_name=ankihub_ai_js_template_name, knox_token=config.token(), @@ -382,16 +404,17 @@ def _add_ankihub_ai_and_sidebar_and_buttons(web_content: WebContent, context): ) web_content.body += f"" - global reviewer_sidebar - if not reviewer_sidebar: - reviewer_sidebar = ReviewerSidebar(context) - reviewer_sidebar.set_on_auth_failure_hook(_handle_auth_failure) + if feature_flags.get("mh_integration"): + global reviewer_sidebar + if not reviewer_sidebar: + reviewer_sidebar = ReviewerSidebar(context) + reviewer_sidebar.set_on_auth_failure_hook(_handle_auth_failure) - reivewer_button_js = get_reviewer_buttons_js( - theme=_ankihub_theme(), - enabled_buttons=_get_enabled_buttons_list(), - ) - web_content.body += f"" + reviewer_button_js = get_reviewer_buttons_js( + theme=_ankihub_theme(), + enabled_buttons=_get_enabled_buttons_list(), + ) + web_content.body += f"" def _get_enabled_buttons_list() -> List[str]: @@ -495,11 +518,13 @@ def _notify_reviewer_buttons_of_card_change(card: Card) -> None: fa_count = len(_get_resources(note.tags, ResourceType.FIRST_AID)) is_anking_deck = _is_anking_deck(aqt.mw.reviewer.card) + show_chatbot = _related_ah_deck_has_note_embeddings(card.note()) js = _wrap_with_reviewer_buttons_check( f""" ankihubReviewerButtons.updateButtons( {bb_count}, {fa_count}, + {'true' if show_chatbot else 'false'}, {'true' if is_anking_deck else 'false'}, ); """ @@ -524,8 +549,7 @@ def _get_resources(tags: List[str], resource_type: ResourceType) -> List[Resourc if (title_and_slug := mh_tag_to_resource_title_and_slug(tag)) for title, slug in [title_and_slug] } - result = list(sorted(result, key=lambda x: x.title)) - return result + return list(sorted(result, key=lambda x: x.title)) def _get_resource_tags(tags: List[str], resource_type: ResourceType) -> Set[str]: diff --git a/ankihub/gui/web/mh_no_urls_empty_state.html b/ankihub/gui/web/mh_no_urls_empty_state.html index a93198012..12d7b74e6 100644 --- a/ankihub/gui/web/mh_no_urls_empty_state.html +++ b/ankihub/gui/web/mh_no_urls_empty_state.html @@ -4,6 +4,7 @@ box-sizing: border-box; padding: 0; margin: 0; + font-family: "Merriweather Sans", sans-serif; } .empty-state-container { max-width: 1200px; diff --git a/ankihub/gui/web/reviewer_buttons.js b/ankihub/gui/web/reviewer_buttons.js index a0feb90cb..60639c1bf 100644 --- a/ankihub/gui/web/reviewer_buttons.js +++ b/ankihub/gui/web/reviewer_buttons.js @@ -11,10 +11,12 @@ class AnkiHubReviewerButtons { this.colorButtonLight = "#F9FAFB"; this.colorButtonSelectedLight = "#C7D2FE"; this.colorButtonBorderLight = "#D1D5DB"; + this.colorButtonHoverLight = "#E5E7EB" this.colorButtonDark = "#030712"; this.colorButtonSelectedDark = "#3730A3"; this.colorButtonBorderDark = "#4b5563"; + this.colorButtonHoverDark = "#1f2937"; this.buttonsData = [ { @@ -45,7 +47,7 @@ class AnkiHubReviewerButtons { } setupButtons() { - const elementsContainer = document.createElement("div"); + this.elementsContainer = document.createElement("div"); const buttonContainer = document.createElement("div"); const toggleButtonsButton = document.createElement("button"); @@ -73,8 +75,9 @@ class AnkiHubReviewerButtons { } this.setToggleButtonsButtonStyle(toggleButtonsButton); - this.setElementsContainerStyle(elementsContainer); + this.setElementsContainerStyle(this.elementsContainer); this.setButtonContainerStyle(buttonContainer); + this.injectReviewerButtonStyleSheet() this.buttonsData.forEach((buttonData) => { const container = document.createElement("div"); @@ -84,6 +87,8 @@ class AnkiHubReviewerButtons { const buttonElement = document.createElement("button"); buttonElement.id = `ankihub-${buttonData.name}-button`; + buttonElement.classList.add("ankihub-reviewer-button"); + this.setButtonStyle( buttonElement, ( @@ -121,15 +126,29 @@ class AnkiHubReviewerButtons { buttonContainer.appendChild(container); }) - elementsContainer.appendChild(buttonContainer); - elementsContainer.appendChild(toggleButtonsButton); - document.body.appendChild(elementsContainer); + this.elementsContainer.appendChild(buttonContainer); + this.elementsContainer.appendChild(toggleButtonsButton); + document.body.appendChild(this.elementsContainer); this.injectResourceCountIndicatorStylesheet(); if (this.buttonsData.length == 0) { toggleButtonsButton.style.display = "none"; } } + injectReviewerButtonStyleSheet() { + const style = document.createElement("style"); + style.innerHTML = ` + .ankihub-reviewer-button { + background-color: ${this.theme == "light" ? this.colorButtonLight : this.colorButtonDark}; + } + + .ankihub-reviewer-button:hover { + background-color: ${this.theme == "light" ? this.colorButtonHoverLight : this.colorButtonHoverDark}; + } + ` + document.head.appendChild(style); + } + getButtonElement(buttonName) { return document.getElementById(`ankihub-${buttonName}-button`); } @@ -142,7 +161,7 @@ class AnkiHubReviewerButtons { setToggleButtonsButtonStyle(toggleButtonsButton) { const styles = ` .ankihub-toggle-buttons-button:hover { - background: ${this.theme == "light" ? "#e5e7eb" : "#1f2937"} !important; + background: ${this.theme == "light" ? this.colorButtonHoverLight : this.colorButtonHoverDark} !important; } .ankihub-toggle-buttons-button-slide { transform: translateX(100px); @@ -221,7 +240,6 @@ class AnkiHubReviewerButtons { button.style.backgroundSize = "cover"; button.style.backgroundPosition = "center"; button.style.backgroundRepeat = "no-repeat"; - button.style.backgroundColor = this.theme == "light" ? this.colorButtonLight : this.colorButtonDark; button.style.cursor = "pointer"; } @@ -269,9 +287,10 @@ class AnkiHubReviewerButtons { document.head.appendChild(style); } - updateButtons(bbCount, faCount, isAnKingDeck) { + updateButtons(bbCount, faCount, showChatbot, isAnKingDeck) { this.bbCount = bbCount; this.faCount = faCount; + this.showChatbot = showChatbot; this.isAnKingDeck = isAnKingDeck; const visibleButtons = this.getVisibleButtons(); @@ -294,10 +313,21 @@ class AnkiHubReviewerButtons { }); this.updateResourceCountIndicators(visibleButtons); + + if (visibleButtons.length === 0) { + this.elementsContainer.style.visibility = "hidden"; + } else { + this.elementsContainer.style.visibility = "visible"; + } } getVisibleButtons() { - return this.buttonsData.filter(buttonData => this.isAnKingDeck || buttonData.name === "chatbot"); + return this.buttonsData.filter( + buttonData => ( + (buttonData.name === "chatbot" && this.showChatbot) || + (buttonData.name !== "chatbot" && this.isAnKingDeck) + ) + ); } udpateButtonVisibility(buttonName, isVisible) { @@ -308,11 +338,11 @@ class AnkiHubReviewerButtons { updateButtonStyle(buttonName, isTopButton, isBottomButton) { const buttonElement = this.getButtonElement(buttonName); if (isTopButton && isBottomButton) { - buttonElement.style.borderRadius = "8px 0px 0px 8px"; + buttonElement.style.borderRadius = "4px 0px 0px 4px"; } else if (isBottomButton) { - buttonElement.style.borderRadius = "0px 0px 0px 8px"; + buttonElement.style.borderRadius = "0px 0px 0px 4px"; } else if (isTopButton) { - buttonElement.style.borderRadius = "8px 0px 0px 0px"; + buttonElement.style.borderRadius = "4px 0px 0px 0px"; } else { buttonElement.style.borderRadius = "0px"; } diff --git a/ankihub/gui/web/sidebar_tabs.html b/ankihub/gui/web/sidebar_tabs.html index e0b7fb87f..f8aad29f1 100644 --- a/ankihub/gui/web/sidebar_tabs.html +++ b/ankihub/gui/web/sidebar_tabs.html @@ -68,9 +68,29 @@ border-bottom: 2px solid transparent; color: #6b7280; transition: color 0.3s, border-color 0.3s; - flex-grow: 1; + flex: 1 1 0; height: 100%; align-items: center; + justify-content: center; + overflow: hidden; + } + + .webview-tab span { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + display: block; + width: 100%; + } + + .webview-tab:hover { + color: #4B5563; + border-bottom: 2px solid #D1D5DB; + } + + .dark .webview-tab:hover { + color: #9CA3AF; + border-bottom: 2px solid #6B7280; } .webview-tab.selected { @@ -135,8 +155,8 @@
{{ page_title }}
-