Live theme changes (#1497)

* Allow theme change at runtime and add hook

* Save or restore default palette on theme change

* Update aqt widget styles on theme change

* styling fixes

- drop _light_palette, as default_palette serves the same purpose
- save default platform theme, and restore it when switching away
from nightmode
- update macOS light/dark mode on theme switch
- fix unreadable menus on Windows

* update night-mode classes on theme change

This is the easy part - CSS styling that uses standard_css or our
css variables should update automatically. The main remaining issue
is JS code that sets colors based on the theme at the time it's run -
eg the graph code, and the editor.

* switch night mode value on toggle

* expose current theme via a store; switch graphs to use it

https://github.com/ankitects/anki/issues/1471#issuecomment-972402492

* start using currentTheme in editor/components

This fixes basic editing - there are still components that need updating.

* add simple xcodeproj for code completion

* add helper to get currently-active system theme on macOS

* fix setCurrentTheme not being immediately available

* live update tag color

* style().name() doesn't work on Qt5

* automatic theme switching on Windows/Mac

* currentTheme -> pageTheme

* Replace `nightModeKey` with `pageTheme`

Co-authored-by: Damien Elmes <gpg@ankiweb.net>
This commit is contained in:
RumovZ 2021-11-24 22:17:41 +01:00 committed by GitHub
parent 63404de5df
commit f2173fddb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 659 additions and 182 deletions

View File

@ -37,4 +37,8 @@ preferences-you-can-restore-backups-via-fileswitch = You can restore backups via
preferences-legacy-timezone-handling = Legacy timezone handling (buggy, but required for AnkiDroid <= 2.14)
preferences-default-search-text = Default search text
preferences-default-search-text-example = eg. 'deck:current '
preferences-theme-label = Theme: { $theme }
preferences-theme-follow-system = Follow System
preferences-theme-light = Light
preferences-theme-dark = Dark
preferences-v3-scheduler = V3 scheduler

View File

@ -19,6 +19,14 @@ class SidebarSearchBar(QLineEdit):
self.timer.setInterval(600)
self.timer.setSingleShot(True)
self.setFrame(False)
self.setup_style()
qconnect(self.timer.timeout, self.onSearch)
qconnect(self.textChanged, self.onTextChanged)
aqt.gui_hooks.theme_did_change.append(self.setup_style)
def setup_style(self) -> None:
border = theme_manager.color(colors.MEDIUM_BORDER)
styles = [
"padding: 1px",
@ -32,9 +40,6 @@ class SidebarSearchBar(QLineEdit):
self.setStyleSheet("QLineEdit { %s }" % ";".join(styles))
qconnect(self.timer.timeout, self.onSearch)
qconnect(self.textChanged, self.onTextChanged)
def onTextChanged(self, text: str) -> None:
if not self.timer.isActive():
self.timer.start()
@ -49,3 +54,6 @@ class SidebarSearchBar(QLineEdit):
self.onSearch()
else:
QLineEdit.keyPressEvent(self, evt)
def cleanup(self) -> None:
aqt.gui_hooks.theme_did_change.remove(self.setup_style)

View File

@ -31,6 +31,7 @@ class SidebarToolbar(QToolBar):
self.setIconSize(QSize(16, 16))
self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
self.setStyle(QStyleFactory.create("fusion"))
aqt.gui_hooks.theme_did_change.append(self._update_icons)
def _setup_tools(self) -> None:
for row, tool in enumerate(self._tools):
@ -48,3 +49,10 @@ class SidebarToolbar(QToolBar):
def _on_action_group_triggered(self, action: QAction) -> None:
index = self._action_group.actions().index(action)
self.sidebar.tool = self._tools[index][0]
def cleanup(self) -> None:
aqt.gui_hooks.theme_did_change.remove(self._update_icons)
def _update_icons(self) -> None:
for idx, action in enumerate(self._action_group.actions()):
action.setIcon(theme_manager.icon_from_resources(self._tools[idx][1]))

View File

@ -91,6 +91,16 @@ class SidebarTreeView(QTreeView):
qconnect(self.expanded, self._on_expansion)
qconnect(self.collapsed, self._on_collapse)
self._setup_style()
# these do not really belong here, they should be in a higher-level class
self.toolbar = SidebarToolbar(self)
self.searchBar = SidebarSearchBar(self)
gui_hooks.flag_label_did_change.append(self.refresh)
gui_hooks.theme_did_change.append(self._setup_style)
def _setup_style(self) -> None:
# match window background color and tweak style
bgcolor = QPalette().window().color().name()
border = theme_manager.color(colors.MEDIUM_BORDER)
@ -105,14 +115,11 @@ class SidebarTreeView(QTreeView):
self.setStyleSheet("QTreeView { %s }" % ";".join(styles))
# these do not really belong here, they should be in a higher-level class
self.toolbar = SidebarToolbar(self)
self.searchBar = SidebarSearchBar(self)
gui_hooks.flag_label_did_change.append(self.refresh)
def cleanup(self) -> None:
self.toolbar.cleanup()
self.searchBar.cleanup()
gui_hooks.flag_label_did_change.remove(self.refresh)
gui_hooks.theme_did_change.remove(self._setup_style)
@property
def tool(self) -> SidebarTool:

View File

@ -59,6 +59,7 @@ class Table:
def cleanup(self) -> None:
self._save_header()
gui_hooks.theme_did_change.remove(self._setup_style)
# Public Methods
######################################################################
@ -342,17 +343,10 @@ class Table:
self._view.setHorizontalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
self._view.horizontalScrollBar().setSingleStep(10)
self._update_font()
if not theme_manager.night_mode:
self._view.setStyleSheet(
"QTableView{ selection-background-color: rgba(150, 150, 150, 50); "
"selection-color: black; }"
)
elif theme_manager.macos_dark_mode():
self._view.setStyleSheet(
f"QTableView {{ gridline-color: {colors.FRAME_BG} }}"
)
self._setup_style()
self._view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
qconnect(self._view.customContextMenuRequested, self._on_context_menu)
gui_hooks.theme_did_change.append(self._setup_style)
def _update_font(self) -> None:
# we can't choose different line heights efficiently, so we need
@ -365,6 +359,19 @@ class Table:
curmax = bsize
self._view.verticalHeader().setDefaultSectionSize(curmax + 6)
def _setup_style(self) -> None:
if not theme_manager.night_mode:
self._view.setStyleSheet(
"QTableView{ selection-background-color: rgba(150, 150, 150, 50); "
"selection-color: black; }"
)
elif theme_manager.macos_dark_mode():
self._view.setStyleSheet(
f"QTableView {{ gridline-color: {colors.FRAME_BG} }}"
)
else:
self._view.setStyleSheet("")
def _setup_headers(self) -> None:
vh = self._view.verticalHeader()
hh = self._view.horizontalHeader()

View File

@ -55,7 +55,7 @@ class CardLayout(QDialog):
self.model = note.note_type()
self.templates = self.model["tmpls"]
self.fill_empty_action_toggled = fill_empty
self.night_mode_is_enabled = self.mw.pm.night_mode()
self.night_mode_is_enabled = theme_manager.night_mode
self.mobile_emulation_enabled = False
self.have_autoplayed = False
self.mm._remove_from_cache(self.model["id"])

View File

@ -55,6 +55,9 @@
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="theme"/>
</item>
<item>
<widget class="QComboBox" name="video_driver"/>
</item>
@ -86,13 +89,6 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="nightMode">
<property name="text">
<string>preferences_night_mode</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="useCurrent">
<item>
@ -602,12 +598,12 @@
</widget>
<tabstops>
<tabstop>lang</tabstop>
<tabstop>theme</tabstop>
<tabstop>video_driver</tabstop>
<tabstop>showPlayButtons</tabstop>
<tabstop>interrupt_audio</tabstop>
<tabstop>pastePNG</tabstop>
<tabstop>paste_strips_formatting</tabstop>
<tabstop>nightMode</tabstop>
<tabstop>useCurrent</tabstop>
<tabstop>default_search_text</tabstop>
<tabstop>uiScale</tabstop>

View File

@ -47,7 +47,7 @@ from aqt.qt import *
from aqt.qt import sip
from aqt.sync import sync_collection, sync_login
from aqt.taskman import TaskManager
from aqt.theme import theme_manager
from aqt.theme import Theme, theme_manager
from aqt.undo import UndoActionsInfo
from aqt.utils import (
HelpPage,
@ -145,11 +145,11 @@ class AnkiQt(QMainWindow):
self.setupMediaServer()
self.setupSound()
self.setupSpellCheck()
self.setupProgress()
self.setupStyle()
self.setupMainWindow()
self.setupSystemSpecific()
self.setupMenus()
self.setupProgress()
self.setupErrorHandler()
self.setupSignals()
self.setupAutoUpdate()
@ -1004,8 +1004,14 @@ title="{}" {}>{}</button>""".format(
return True
def setupStyle(self) -> None:
theme_manager.night_mode = self.pm.night_mode()
theme_manager.apply_style(self.app)
theme_manager.apply_style()
self.progress.timer(
5 * 1000, theme_manager.apply_style_if_system_style_changed, True, False
)
def set_theme(self, theme: Theme) -> None:
self.pm.set_theme(theme)
self.setupStyle()
# Key handling
##########################################################################

View File

@ -3,20 +3,40 @@
"""Platform-specific functionality."""
from __future__ import annotations
import os
import sys
from ctypes import CDLL
import aqt.utils
from anki.utils import isMac
from anki.utils import isMac, isWin
def set_dark_mode(enabled: bool) -> bool:
def get_windows_dark_mode() -> bool:
"True if Windows system is currently in dark mode."
if not isWin:
return False
from winreg import ( # pylint: disable=import-error
HKEY_CURRENT_USER,
OpenKey,
QueryValueEx,
)
key = OpenKey(
HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize",
)
return not QueryValueEx(key, "AppsUseLightTheme")[0]
def set_macos_dark_mode(enabled: bool) -> bool:
"True if setting successful."
if not isMac:
return False
try:
_set_dark_mode(enabled)
_ankihelper().set_darkmode_enabled(enabled)
return True
except Exception as e:
# swallow exceptions, as library will fail on macOS 10.13
@ -24,9 +44,28 @@ def set_dark_mode(enabled: bool) -> bool:
return False
def _set_dark_mode(enabled: bool) -> None:
def get_macos_dark_mode() -> bool:
"True if macOS system is currently in dark mode."
if not isMac:
return False
try:
return _ankihelper().system_is_dark()
except Exception as e:
# swallow exceptions, as library will fail on macOS 10.13
print(e)
return False
_ankihelper_dll: CDLL | None = None
def _ankihelper() -> CDLL:
global _ankihelper_dll
if _ankihelper_dll:
return _ankihelper_dll
if getattr(sys, "frozen", False):
path = os.path.join(sys.prefix, "libankihelper.dylib")
else:
path = os.path.join(aqt.utils.aqt_data_folder(), "lib", "libankihelper.dylib")
CDLL(path).set_darkmode_enabled(enabled)
_ankihelper_dll = CDLL(path)
return _ankihelper_dll

View File

@ -11,6 +11,7 @@ from aqt import AnkiQt
from aqt.operations.collection import set_preferences
from aqt.profiles import VideoDriver
from aqt.qt import *
from aqt.theme import Theme
from aqt.utils import HelpPage, disable_help_button, openHelp, showInfo, showWarning, tr
@ -199,7 +200,17 @@ class Preferences(QDialog):
def setup_global(self) -> None:
"Setup options global to all profiles."
self.form.uiScale.setValue(int(self.mw.pm.uiScale() * 100))
self.form.nightMode.setChecked(self.mw.pm.night_mode())
themes = [
tr.preferences_theme_label(theme=theme)
for theme in (
tr.preferences_theme_follow_system(),
tr.preferences_theme_light(),
tr.preferences_theme_dark(),
)
]
self.form.theme.addItems(themes)
self.form.theme.setCurrentIndex(self.mw.pm.theme().value)
qconnect(self.form.theme.currentIndexChanged, self.on_theme_changed)
self.setup_language()
self.setup_video_driver()
@ -216,15 +227,14 @@ class Preferences(QDialog):
self.mw.pm.setUiScale(newScale)
restart_required = True
if self.mw.pm.night_mode() != self.form.nightMode.isChecked():
self.mw.pm.set_night_mode(not self.mw.pm.night_mode())
restart_required = True
if restart_required:
showInfo(tr.preferences_changes_will_take_effect_when_you())
self.updateOptions()
def on_theme_changed(self, index: int) -> None:
self.mw.set_theme(Theme(index))
# legacy - one of Henrik's add-ons is currently wrapping them
def setupOptions(self) -> None:

View File

@ -23,6 +23,7 @@ from anki.sync import SyncAuth
from anki.utils import int_time, isMac, isWin
from aqt import appHelpSite
from aqt.qt import *
from aqt.theme import Theme
from aqt.utils import disable_help_button, showWarning, tr
# Profile handling
@ -515,6 +516,12 @@ create table if not exists profiles
def set_night_mode(self, on: bool) -> None:
self.meta["night_mode"] = on
def theme(self) -> Theme:
return Theme(self.meta.get("theme", 0))
def set_theme(self, theme: Theme) -> None:
self.meta["theme"] = theme.value
def dark_mode_widgets(self) -> bool:
return self.meta.get("dark_mode_widgets", False)

View File

@ -3,12 +3,14 @@
from __future__ import annotations
import enum
import platform
from dataclasses import dataclass
import aqt
from anki.utils import isMac
from aqt import QApplication, colors, gui_hooks, isWin
from aqt.platform import set_dark_mode
from aqt.platform import get_macos_dark_mode, get_windows_dark_mode, set_macos_dark_mode
from aqt.qt import (
QColor,
QGuiApplication,
@ -37,6 +39,12 @@ class ColoredIcon:
return ColoredIcon(path=self.path, color=color)
class Theme(enum.IntEnum):
FOLLOW_SYSTEM = 0
LIGHT = 1
DARK = 2
class ThemeManager:
_night_mode_preference = False
_icon_cache_light: dict[str, QIcon] = {}
@ -44,6 +52,7 @@ class ThemeManager:
_icon_size = 128
_dark_mode_available: bool | None = None
default_palette: QPalette | None = None
_default_style: str | None = None
# Qt applies a gradient to the buttons in dark mode
# from about #505050 to #606060.
@ -58,7 +67,7 @@ class ThemeManager:
return False
if self._dark_mode_available is None:
self._dark_mode_available = set_dark_mode(True)
self._dark_mode_available = set_macos_dark_mode(True)
from aqt import mw
@ -143,28 +152,57 @@ class ThemeManager:
def qcolor(self, colors: tuple[str, str]) -> QColor:
return QColor(self.color(colors))
def apply_style(self, app: QApplication) -> None:
self.default_palette = QGuiApplication.palette()
def _determine_night_mode(self) -> bool:
theme = aqt.mw.pm.theme()
if theme == Theme.LIGHT:
return False
elif theme == Theme.DARK:
return True
else:
if isWin:
return get_windows_dark_mode()
elif isMac:
return get_macos_dark_mode()
else:
# not supported on Linux
return False
def apply_style_if_system_style_changed(self) -> None:
theme = aqt.mw.pm.theme()
if theme != Theme.FOLLOW_SYSTEM:
return
if self._determine_night_mode() != self.night_mode:
self.apply_style()
def apply_style(self) -> None:
"Apply currently configured style."
app = aqt.mw.app
self.night_mode = self._determine_night_mode()
if not self.default_palette:
self.default_palette = QGuiApplication.palette()
self._default_style = app.style().objectName()
self._apply_palette(app)
self._apply_style(app)
gui_hooks.theme_did_change()
def _apply_style(self, app: QApplication) -> None:
buf = ""
if isWin and platform.release() == "10" and not self.night_mode:
# add missing bottom border to menubar
buf += """
QMenuBar {
border-bottom: 1px solid #aaa;
background: white;
}
if isWin and platform.release() == "10":
# day mode is missing a bottom border; background must be
# also set for border to apply
buf += f"""
QMenuBar {{
border-bottom: 1px solid {self.color(colors.BORDER)};
background: {self.color(colors.WINDOW_BG)};
}}
"""
# qt bug? setting the above changes the browser sidebar
# to white as well, so set it back
buf += """
QTreeWidget {
background: #eee;
}
buf += f"""
QTreeWidget {{
background: {self.color(colors.WINDOW_BG)};
}}
"""
if self.night_mode:
@ -209,7 +247,11 @@ QTabWidget {{ background-color: {}; }}
app.setStyleSheet(buf)
def _apply_palette(self, app: QApplication) -> None:
set_macos_dark_mode(self.night_mode)
if not self.night_mode:
app.setStyle(QStyleFactory.create(self._default_style)) # type: ignore
app.setPalette(self.default_palette)
return
if not self.macos_dark_mode():

View File

@ -252,6 +252,7 @@ class AnkiWebView(QWebEngineView):
context=Qt.ShortcutContext.WidgetWithChildrenShortcut,
activated=self.onEsc,
)
gui_hooks.theme_did_change.append(self.on_theme_did_change)
def set_title(self, title: str) -> None:
self.title = title # type: ignore[assignment]
@ -445,7 +446,7 @@ div[contenteditable="true"]:focus {{
lang_dir = "ltr"
return f"""
body {{ zoom: {zoom}; background-color: {body_bg}; direction: {lang_dir}; }}
body {{ zoom: {zoom}; background-color: --window-bg; direction: {lang_dir}; }}
html {{ {font} }}
{button_style}
:root {{ --window-bg: {window_bg_day} }}
@ -551,6 +552,8 @@ html {{ {font} }}
self._maybeRunActions()
def _maybeRunActions(self) -> None:
if sip.isdeleted(self):
return
while self._pendingActions and self._domDone:
name, args = self._pendingActions.pop(0)
@ -675,4 +678,29 @@ document.head.appendChild(style);
# this will fail when __del__ is called during app shutdown
return
gui_hooks.theme_did_change.remove(self.on_theme_did_change)
mw.mediaServer.clear_page_html(id(self))
def on_theme_did_change(self) -> None:
# avoid flashes if page reloaded
self._page.setBackgroundColor(
self.get_window_bg_color(theme_manager.night_mode)
)
# update night-mode class, and legacy nightMode/night-mode body classes
self.eval(
f"""
(function() {{
const doc = document.documentElement.classList;
const body = document.body.classList;
if ({1 if theme_manager.night_mode else 0}) {{
doc.add("night-mode");
body.add("night-mode");
body.add("nightMode");
}} else {{
doc.remove("night-mode");
body.remove("night-mode");
body.remove("nightMode");
}}
}})();
"""
)

View File

@ -4,6 +4,7 @@
@import Foundation;
@import AppKit;
/// Force our app to be either light or dark mode.
void set_darkmode_enabled(BOOL enabled) {
NSAppearance *appearance;
if (enabled) {
@ -14,3 +15,20 @@ void set_darkmode_enabled(BOOL enabled) {
[NSApplication sharedApplication].appearance = appearance;
}
/// True if the system is set to dark mode.
BOOL system_is_dark(void) {
BOOL styleSet = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleInterfaceStyle"] != nil;
return styleSet;
// FIXME: confirm whether this is required on 10.15/16 (it
// does not appear to be on 11)
// BOOL autoSwitch = [[NSUserDefaults standardUserDefaults] boolForKey:@"AppleInterfaceStyleSwitchesAutomatically"];
//
// if (@available(macOS 10.15, *)) {
// return autoSwitch ? !styleSet : styleSet;
// } else {
// return styleSet;
// }
}

View File

@ -0,0 +1,280 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 55;
objects = {
/* Begin PBXBuildFile section */
1327600A274613D9001D63D7 /* AnkiHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 13276009274613D8001D63D7 /* AnkiHelper.m */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
13276009274613D8001D63D7 /* AnkiHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AnkiHelper.m; sourceTree = "<group>"; };
138B770F2746137F003A3E4F /* libankihelper.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libankihelper.dylib; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
138B770D2746137F003A3E4F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
138B77062746137F003A3E4F = {
isa = PBXGroup;
children = (
13276009274613D8001D63D7 /* AnkiHelper.m */,
138B77102746137F003A3E4F /* Products */,
);
sourceTree = "<group>";
};
138B77102746137F003A3E4F /* Products */ = {
isa = PBXGroup;
children = (
138B770F2746137F003A3E4F /* libankihelper.dylib */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
138B770B2746137F003A3E4F /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
138B770E2746137F003A3E4F /* ankihelper */ = {
isa = PBXNativeTarget;
buildConfigurationList = 138B77182746137F003A3E4F /* Build configuration list for PBXNativeTarget "ankihelper" */;
buildPhases = (
138B770B2746137F003A3E4F /* Headers */,
138B770C2746137F003A3E4F /* Sources */,
138B770D2746137F003A3E4F /* Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = ankihelper;
productName = ankihelper;
productReference = 138B770F2746137F003A3E4F /* libankihelper.dylib */;
productType = "com.apple.product-type.library.dynamic";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
138B77072746137F003A3E4F /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastUpgradeCheck = 1310;
TargetAttributes = {
138B770E2746137F003A3E4F = {
CreatedOnToolsVersion = 13.1;
};
};
};
buildConfigurationList = 138B770A2746137F003A3E4F /* Build configuration list for PBXProject "ankihelper" */;
compatibilityVersion = "Xcode 13.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 138B77062746137F003A3E4F;
productRefGroup = 138B77102746137F003A3E4F /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
138B770E2746137F003A3E4F /* ankihelper */,
);
};
/* End PBXProject section */
/* Begin PBXSourcesBuildPhase section */
138B770C2746137F003A3E4F /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
1327600A274613D9001D63D7 /* AnkiHelper.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
138B77162746137F003A3E4F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 11.6;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
};
name = Debug;
};
138B77172746137F003A3E4F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 11.6;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
};
name = Release;
};
138B77192746137F003A3E4F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 7ZM8SLJM4P;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
EXECUTABLE_PREFIX = lib;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
name = Debug;
};
138B771A2746137F003A3E4F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 7ZM8SLJM4P;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
EXECUTABLE_PREFIX = lib;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
138B770A2746137F003A3E4F /* Build configuration list for PBXProject "ankihelper" */ = {
isa = XCConfigurationList;
buildConfigurations = (
138B77162746137F003A3E4F /* Debug */,
138B77172746137F003A3E4F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
138B77182746137F003A3E4F /* Build configuration list for PBXNativeTarget "ankihelper" */ = {
isa = XCConfigurationList;
buildConfigurations = (
138B77192746137F003A3E4F /* Debug */,
138B771A2746137F003A3E4F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 138B77072746137F003A3E4F /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>ankihelper.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
</dict>
</plist>

View File

@ -540,6 +540,10 @@ hooks = [
there are no outstanding ops.
""",
),
Hook(
name="theme_did_change",
doc="Called after night mode is toggled.",
),
# Webview
###################
Hook(

View File

@ -9,7 +9,6 @@ import { ChangeNotetypeState, getChangeNotetypeInfo, getNotetypeNames } from "./
import { setupI18n, ModuleName } from "../lib/i18n";
import { checkNightMode } from "../lib/nightmode";
import ChangeNotetypePage from "./ChangeNotetypePage.svelte";
import { nightModeKey } from "../components/context-keys";
export async function changeNotetypePage(
target: HTMLDivElement,
@ -28,14 +27,11 @@ export async function changeNotetypePage(
}),
]);
const nightMode = checkNightMode();
const context = new Map();
context.set(nightModeKey, nightMode);
checkNightMode();
const state = new ChangeNotetypeState(names, info);
return new ChangeNotetypePage({
target,
props: { state },
context,
} as any);
}

View File

@ -16,13 +16,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</script>
<script lang="ts">
import { getContext, setContext } from "svelte";
import { setContext } from "svelte";
import { writable } from "svelte/store";
import Item from "./Item.svelte";
import type { Registration } from "./registration";
import { sectionKey, nightModeKey } from "./context-keys";
import { sectionKey } from "./context-keys";
import { insertElement, appendElement } from "./identifier";
import { makeInterface } from "./registration";
import { pageTheme } from "../sveltelib/theme";
export let id: string | undefined = undefined;
let className: string = "";
@ -89,15 +90,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
$: if (buttonToolbarRef && api) {
createApi();
}
const nightMode = getContext<boolean>(nightModeKey);
</script>
<div
bind:this={buttonToolbarRef}
{id}
class="button-toolbar btn-toolbar {className}"
class:nightMode
class:nightMode={$pageTheme.isDark}
{style}
role="toolbar"
on:focusout

View File

@ -3,8 +3,8 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { onMount, createEventDispatcher, getContext } from "svelte";
import { nightModeKey } from "./context-keys";
import { onMount, createEventDispatcher } from "svelte";
import { pageTheme } from "../sveltelib/theme";
export let id: string | undefined = undefined;
let className = "";
@ -15,8 +15,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let buttonRef: HTMLButtonElement;
const nightMode = getContext(nightModeKey);
const dispatch = createEventDispatcher();
onMount(() => dispatch("mount", { button: buttonRef }));
</script>
@ -26,8 +24,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
tabindex={tabbable ? 0 : -1}
bind:this={buttonRef}
class="dropdown-item btn {className}"
class:btn-day={!nightMode}
class:btn-night={nightMode}
class:btn-day={!$pageTheme.isDark}
class:btn-night={$pageTheme.isDark}
title={tooltip}
on:click
on:mouseenter

View File

@ -5,7 +5,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<script lang="ts">
import IconConstrain from "./IconConstrain.svelte";
import { getContext, onMount, createEventDispatcher } from "svelte";
import { nightModeKey, dropdownKey } from "./context-keys";
import { dropdownKey } from "./context-keys";
import { pageTheme } from "../sveltelib/theme";
import type { DropdownProps } from "./dropdown";
export let id: string | undefined = undefined;
@ -23,7 +24,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let buttonRef: HTMLButtonElement;
const nightMode = getContext<boolean>(nightModeKey);
const dropdownProps = getContext<DropdownProps>(dropdownKey) ?? { dropdown: false };
const dispatch = createEventDispatcher();
@ -36,8 +36,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
class="icon-button btn {className}"
class:active
class:dropdown-toggle={dropdownProps.dropdown}
class:btn-day={!nightMode}
class:btn-night={nightMode}
class:btn-day={!$pageTheme.isDark}
class:btn-night={$pageTheme.isDark}
title={tooltip}
{...dropdownProps}
{disabled}

View File

@ -4,8 +4,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { onMount, createEventDispatcher, getContext } from "svelte";
import { nightModeKey, dropdownKey } from "./context-keys";
import { dropdownKey } from "./context-keys";
import type { DropdownProps } from "./dropdown";
import { pageTheme } from "../sveltelib/theme";
export let id: string | undefined = undefined;
let className: string = "";
@ -21,7 +22,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let disabled = false;
export let tabbable = false;
const nightMode = getContext<boolean>(nightModeKey);
const dropdownProps = getContext<DropdownProps>(dropdownKey) ?? { dropdown: false };
let buttonRef: HTMLButtonElement;
@ -36,8 +36,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
class="label-button {extendClassName(className, theme)}"
class:active
class:dropdown-toggle={dropdownProps.dropdown}
class:btn-day={theme === "anki" && !nightMode}
class:btn-night={theme === "anki" && nightMode}
class:btn-day={theme === "anki" && !$pageTheme.isDark}
class:btn-night={theme === "anki" && $pageTheme.isDark}
title={tooltip}
{...dropdownProps}
{disabled}

View File

@ -3,8 +3,8 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { onMount, createEventDispatcher, getContext } from "svelte";
import { nightModeKey } from "./context-keys";
import { onMount, createEventDispatcher } from "svelte";
import { pageTheme } from "../sveltelib/theme";
export let id: string | undefined = undefined;
let className = "";
@ -13,8 +13,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let tooltip: string | undefined = undefined;
export let disabled = false;
const nightMode = getContext<boolean>(nightModeKey);
let buttonRef: HTMLSelectElement;
const dispatch = createEventDispatcher();
@ -29,9 +27,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{id}
{disabled}
class="{className} form-select"
class:btn-day={!nightMode}
class:btn-night={nightMode}
class:visible-down-arrow={nightMode}
class:btn-day={!$pageTheme.isDark}
class:btn-night={$pageTheme.isDark}
class:visible-down-arrow={$pageTheme.isDark}
title={tooltip}
on:change
>

View File

@ -1,6 +1,5 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export const nightModeKey = Symbol("nightMode");
export const touchDeviceKey = Symbol("touchDevice");
export const sectionKey = Symbol("section");

View File

@ -3,19 +3,16 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { getContext } from "svelte";
import { nightModeKey } from "../components/context-keys";
import { pageTheme } from "../sveltelib/theme";
export let choices: string[];
export let value: number = 0;
const nightMode = getContext<boolean>(nightModeKey);
</script>
<select
bind:value
class:nightMode
class:visible-down-arrow={nightMode}
class:nightMode={$pageTheme.isDark}
class:visible-down-arrow={$pageTheme.isDark}
class="enum-selector form-select"
>
{#each choices as choice, idx}

View File

@ -3,15 +3,12 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { getContext } from "svelte";
import { nightModeKey } from "../components/context-keys";
import { pageTheme } from "../sveltelib/theme";
export let value: number;
export let min = 1;
export let max = 9999;
const nightMode = getContext<boolean>(nightModeKey);
function checkMinMax() {
if (value > max) {
value = max;
@ -29,7 +26,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{max}
bind:value
class="spin-box form-control"
class:nightMode
class:nightMode={$pageTheme.isDark}
on:blur={checkMinMax}
/>

View File

@ -3,8 +3,7 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { getContext } from "svelte";
import { nightModeKey } from "../components/context-keys";
import { pageTheme } from "../sveltelib/theme";
export let value: number;
export let min = 1;
@ -13,8 +12,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let stringValue: string;
$: stringValue = value.toFixed(2);
const nightMode = getContext<boolean>(nightModeKey);
function update(this: HTMLInputElement): void {
value = Math.min(max, Math.max(min, parseFloat(this.value)));
}
@ -23,7 +20,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<input
type="number"
class="form-control"
class:nightMode
class:nightMode={$pageTheme.isDark}
{min}
{max}
step="0.01"

View File

@ -3,8 +3,7 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { getContext } from "svelte";
import { nightModeKey } from "../components/context-keys";
import { pageTheme } from "../sveltelib/theme";
import { stepsToString, stringToSteps } from "./steps";
export let value: number[];
@ -12,8 +11,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let stringValue: string;
$: stringValue = stepsToString(value);
const nightMode = getContext<boolean>(nightModeKey);
function update(this: HTMLInputElement): void {
value = stringToSteps(this.value);
}
@ -23,7 +20,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
type="text"
value={stringValue}
class="form-control"
class:nightMode
class:nightMode={$pageTheme.isDark}
on:blur={update}
/>

View File

@ -3,14 +3,11 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { getContext } from "svelte";
import { nightModeKey } from "../components/context-keys";
import { pageTheme } from "../sveltelib/theme";
export let id: string | undefined;
export let value: boolean;
export let disabled = false;
const nightMode = getContext<boolean>(nightModeKey);
</script>
<div class="form-check form-switch">
@ -18,7 +15,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{id}
type="checkbox"
class="form-check-input"
class:nightMode
class:nightMode={$pageTheme.isDark}
bind:checked={value}
{disabled}
/>

View File

@ -4,7 +4,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { onMount, onDestroy, getContext } from "svelte";
import { nightModeKey, modalsKey } from "../components/context-keys";
import { modalsKey } from "../components/context-keys";
import { pageTheme } from "../sveltelib/theme";
import Modal from "bootstrap/js/dist/modal";
export let title: string;
@ -40,8 +41,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
onDestroy(() => {
modalRef.removeEventListener("shown.bs.modal", onShown);
});
const nightMode = getContext<boolean>(nightModeKey);
</script>
<div
@ -52,13 +51,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content" class:default-colors={nightMode}>
<div class="modal-content" class:default-colors={$pageTheme.isDark}>
<div class="modal-header">
<h5 class="modal-title" id="modalLabel">{title}</h5>
<button
type="button"
class="btn-close"
class:invert={nightMode}
class:invert={$pageTheme.isDark}
data-bs-dismiss="modal"
aria-label="Close"
/>
@ -73,7 +72,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
id="prompt-input"
bind:this={inputRef}
type="text"
class:nightMode
class:nightMode={$pageTheme.isDark}
class="form-control"
bind:value
/>

View File

@ -11,7 +11,7 @@ import { getDeckOptionsInfo, DeckOptionsState } from "./lib";
import { setupI18n, ModuleName } from "../lib/i18n";
import { checkNightMode } from "../lib/nightmode";
import DeckOptionsPage from "./DeckOptionsPage.svelte";
import { nightModeKey, touchDeviceKey, modalsKey } from "../components/context-keys";
import { touchDeviceKey, modalsKey } from "../components/context-keys";
export async function deckOptions(
target: HTMLDivElement,
@ -29,9 +29,9 @@ export async function deckOptions(
}),
]);
checkNightMode();
const context = new Map();
const nightMode = checkNightMode();
context.set(nightModeKey, nightMode);
const modals = new Map();
context.set(modalsKey, modals);

View File

@ -18,8 +18,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</script>
<script lang="ts">
import { onDestroy, getContext } from "svelte";
import { nightModeKey } from "../components/context-keys";
import { onDestroy } from "svelte";
import { pageTheme } from "../sveltelib/theme";
import { convertMathjax } from "./mathjax";
import { randomUUID } from "../lib/uuid";
import { writable } from "svelte/store";
@ -30,8 +30,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let autofocus = false;
export let fontSize = 20;
const nightMode = getContext<boolean>(nightModeKey);
$: [converted, title] = convertMathjax(mathjax, nightMode, fontSize);
$: [converted, title] = convertMathjax(mathjax, $pageTheme.isDark, fontSize);
$: empty = title === "MathJax";
$: encoded = encodeURIComponent(converted);

View File

@ -11,7 +11,6 @@ import type { DecoratedElement, DecoratedElementConstructor } from "./decorated"
import { nodeIsElement } from "../lib/dom";
import { noop } from "../lib/functional";
import { placeCaretAfter } from "../domlib/place-caret";
import { nightModeKey } from "../components/context-keys";
import Mathjax_svelte from "./Mathjax.svelte";
@ -148,12 +147,6 @@ export const Mathjax: DecoratedElementConstructor = class Mathjax
this.innerHTML = "";
this.style.whiteSpace = "normal";
const context = new Map();
context.set(
nightModeKey,
document.documentElement.classList.contains("night-mode"),
);
this.component = new Mathjax_svelte({
target: this,
props: {
@ -161,7 +154,6 @@ export const Mathjax: DecoratedElementConstructor = class Mathjax
block: this.block,
autofocus: this.hasAttribute("focusonmount"),
},
context,
} as any);
}

View File

@ -3,8 +3,8 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { onMount, createEventDispatcher, getContext } from "svelte";
import { nightModeKey } from "../components/context-keys";
import { onMount, createEventDispatcher } from "svelte";
import { pageTheme } from "../sveltelib/theme";
export let id: string | undefined = undefined;
let className = "";
@ -12,8 +12,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let selected = false;
export let active = false;
const nightMode = getContext<boolean>(nightModeKey);
const dispatch = createEventDispatcher();
let buttonRef: HTMLButtonElement;
@ -33,8 +31,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{id}
tabindex="-1"
class="autocomplete-item btn {className}"
class:btn-day={!nightMode}
class:btn-night={nightMode}
class:btn-day={!$pageTheme.isDark}
class:btn-night={$pageTheme.isDark}
class:selected
class:active
on:mousedown|preventDefault

View File

@ -3,8 +3,8 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { createEventDispatcher, getContext } from "svelte";
import { nightModeKey } from "../components/context-keys";
import { createEventDispatcher } from "svelte";
import { pageTheme } from "../sveltelib/theme";
export let offsetX = 0;
export let offsetY = 0;
@ -13,7 +13,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let activeSize = 5;
const dispatch = createEventDispatcher();
const nightMode = getContext(nightModeKey);
const onPointerdown =
(north: boolean, west: boolean) =>
@ -26,9 +25,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
class="d-contents"
style="--offsetX: {offsetX}px; --offsetY: {offsetY}px; --activeSize: {activeSize}px;"
>
<div class:nightMode class="bordered" on:mousedown|preventDefault />
<div
class:nightMode
class:nightMode={$pageTheme.isDark}
class="bordered"
on:mousedown|preventDefault
/>
<div
class:nightMode={$pageTheme.isDark}
class:active
class="control nw"
on:mousedown|preventDefault
@ -36,7 +39,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
on:pointermove
/>
<div
class:nightMode
class:nightMode={$pageTheme.isDark}
class:active
class="control ne"
on:mousedown|preventDefault
@ -44,7 +47,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
on:pointermove
/>
<div
class:nightMode
class:nightMode={$pageTheme.isDark}
class:active
class="control sw"
on:mousedown|preventDefault
@ -52,7 +55,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
on:pointermove
/>
<div
class:nightMode
class:nightMode={$pageTheme.isDark}
class:active
class="control se"
on:mousedown|preventDefault

View File

@ -7,6 +7,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { EditingInputAPI } from "./EditingArea.svelte";
import contextProperty from "../sveltelib/context-property";
import type { OnNextInsertTrigger } from "../sveltelib/input-manager";
import { pageTheme } from "../sveltelib/theme";
export interface RichTextInputAPI extends EditingInputAPI {
name: "rich-text";
@ -38,7 +39,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import SetContext from "./SetContext.svelte";
import ContentEditable from "../editable/ContentEditable.svelte";
import { onMount, getContext, getAllContexts } from "svelte";
import { onMount, getAllContexts } from "svelte";
import {
nodeIsElement,
nodeContainsInlineContent,
@ -53,7 +54,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { on } from "../lib/events";
import { nodeStore } from "../sveltelib/node-store";
import type { DecoratedElement } from "../editable/decorated";
import { nightModeKey } from "../components/context-keys";
export let hidden: boolean;
@ -235,12 +235,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
unsubscribeToEditingArea();
};
});
const nightMode = getContext<boolean>(nightModeKey);
</script>
<RichTextStyles
color={nightMode ? "white" : "black"}
color={$pageTheme.isDark ? "white" : "black"}
let:attachToShadow={attachStyles}
let:promise={stylesPromise}
let:stylesDidLoad

View File

@ -3,8 +3,8 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { onMount, getContext, createEventDispatcher } from "svelte";
import { nightModeKey } from "../components/context-keys";
import { onMount, createEventDispatcher } from "svelte";
import { pageTheme } from "../sveltelib/theme";
let className: string = "";
export { className as class };
@ -21,8 +21,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
setTimeout(() => (flashing = false), 300);
}
const nightMode = getContext<boolean>(nightModeKey);
let button: HTMLButtonElement;
onMount(() => dispatch("mount", { button }));
@ -33,8 +31,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
class="tag btn d-inline-flex align-items-center text-nowrap ps-2 pe-1 {className}"
class:selected
class:flashing
class:btn-day={!nightMode}
class:btn-night={nightMode}
class:btn-day={!$pageTheme.isDark}
class:btn-night={$pageTheme.isDark}
tabindex="-1"
title={tooltip}
on:mousemove

View File

@ -6,10 +6,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import Tag from "./Tag.svelte";
import WithTooltip from "../components/WithTooltip.svelte";
import { createEventDispatcher, getContext } from "svelte";
import { nightModeKey } from "../components/context-keys";
import { createEventDispatcher } from "svelte";
import { controlPressed, shiftPressed } from "../lib/keys";
import { delimChar } from "./tags";
import { pageTheme } from "../sveltelib/theme";
export let name: string;
let className: string = "";
@ -58,14 +58,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
function hasMultipleParts(name: string): boolean {
return name.split(delimChar).length > 1;
}
const nightMode = getContext<boolean>(nightModeKey);
const hoverClass = "tag-icon-hover";
</script>
<svelte:body on:keydown={setControlShift} on:keyup={setControlShift} />
<div class:select-mode={selectMode} class:night-mode={nightMode}>
<div class:select-mode={selectMode} class:night-mode={$pageTheme.isDark}>
{#if active}
<Tag class={className} on:mousemove={setControlShift} on:click={onClick}>
{name}

View File

@ -62,16 +62,8 @@ export const i18n = setupI18n({
import OldEditorAdapter from "./OldEditorAdapter.svelte";
import type { NoteEditorAPI } from "./OldEditorAdapter.svelte";
import { nightModeKey } from "../components/context-keys";
async function setupNoteEditor(): Promise<NoteEditorAPI> {
const context = new Map<symbol, unknown>();
context.set(
nightModeKey,
document.documentElement.classList.contains("night-mode"),
);
await i18n;
const api: Partial<NoteEditorAPI> = {};
@ -79,7 +71,6 @@ async function setupNoteEditor(): Promise<NoteEditorAPI> {
const noteEditor = new OldEditorAdapter({
target: document.body,
props: { api: api as NoteEditorAPI },
context,
});
Object.assign(globalThis, {

View File

@ -5,12 +5,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<script lang="ts">
import type { SvelteComponent } from "svelte/internal";
import { writable } from "svelte/store";
import { pageTheme } from "../sveltelib/theme";
import { bridgeCommand } from "../lib/bridgecommand";
import WithGraphData from "./WithGraphData.svelte";
export let nightMode: boolean;
export let graphs: SvelteComponent[];
export let initialSearch: string;
@ -45,7 +45,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{sourceData}
{preferences}
{revlogRange}
{nightMode}
nightMode={$pageTheme.isDark}
on:search={browserSearch}
/>
{/each}

View File

@ -31,14 +31,12 @@ export function graphs(
controller = null as SvelteComponent | null,
} = {},
): void {
const nightMode = checkNightMode();
checkNightMode();
setupI18n({ modules: [ModuleName.STATISTICS, ModuleName.SCHEDULING] }).then(() => {
new GraphsPage({
target,
props: {
graphs,
nightMode,
initialSearch: search,
initialDays: days,
controller,

View File

@ -1,8 +1,8 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
/// Add night-mode class to body if hash location is #night, and return
/// true if added.
/// Add night-mode class to documentElement if hash location is #night, and
/// return true if added.
export function checkNightMode(): boolean {
const nightMode = window.location.hash == "#night";
if (nightMode) {

35
ts/sveltelib/theme.ts Normal file
View File

@ -0,0 +1,35 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { readable, get } from "svelte/store";
import { registerPackage } from "../lib/register-package";
interface ThemeInfo {
isDark: boolean;
}
function getThemeFromRoot(): ThemeInfo {
return {
isDark: document.documentElement.classList.contains("night-mode"),
};
}
let setPageTheme: ((theme: ThemeInfo) => void) | null = null;
/// The current theme that applies to this document/shadow root. When
/// previewing cards in the card layout screen, this may not match the
/// theme Anki is using in its UI.
export const pageTheme = readable(getThemeFromRoot(), (set) => {
setPageTheme = set;
});
// ensure setPageTheme is set immediately
get(pageTheme);
// Update theme when root element's class changes.
const observer = new MutationObserver((_mutationsList, _observer) => {
setPageTheme!(getThemeFromRoot());
});
observer.observe(document.documentElement, { attributeFilter: ["class"] });
registerPackage("anki/theme", {
pageTheme,
});