basic night mode support

Forces the Fusion theme when running night mode, so we don't need
to work around platform themes that don't respond to the defined
palette.

Feedback/suggestions on the chosen colours welcome - _vars.scss is the
file to change if you want to experiment with adjustments.
This commit is contained in:
Damien Elmes 2020-01-23 15:08:10 +10:00
parent 44f2f16546
commit 7dcbc7efec
26 changed files with 430 additions and 115 deletions

View file

@ -226,11 +226,12 @@ def entsToTxt(html: str) -> str:
return reEnts.sub(fixup, html) return reEnts.sub(fixup, html)
# legacy function
def bodyClass(col, card) -> str: def bodyClass(col, card) -> str:
bodyclass = "card card%d" % (card.ord + 1) from aqt.theme import theme_manager
if col.conf.get("nightMode"):
bodyclass += " nightMode" print("bodyClass() deprecated")
return bodyclass return theme_manager.body_classes_for_card_ord(card.ord)
# IDs # IDs

View file

@ -5,7 +5,7 @@ MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules MAKEFLAGS += --no-builtin-rules
.SUFFIXES: .SUFFIXES:
BLACKARGS := -t py36 aqt tests setup.py tools/*.py --exclude='aqt/forms|buildinfo' BLACKARGS := -t py36 aqt tests setup.py tools/*.py --exclude='aqt/forms|buildinfo|colors'
ISORTARGS := aqt tests setup.py ISORTARGS := aqt tests setup.py
$(shell mkdir -p .build ../dist) $(shell mkdir -p .build ../dist)
@ -33,6 +33,7 @@ TSDEPS := $(wildcard ts/src/*.ts) $(wildcard ts/scss/*.scss)
.build/js: $(TSDEPS) .build/js: $(TSDEPS)
(cd ts && npm i && npm run build) (cd ts && npm i && npm run build)
python ./tools/extract_scss_colors.py
@touch $@ @touch $@
.build/hooks: tools/genhooks_gui.py ../pylib/tools/hookslib.py .build/hooks: tools/genhooks_gui.py ../pylib/tools/hookslib.py

1
qt/aqt/.gitignore vendored
View file

@ -1 +1,2 @@
buildinfo.py buildinfo.py
colors.py

View file

@ -20,19 +20,12 @@ from anki.consts import *
from anki.lang import _, ngettext from anki.lang import _, ngettext
from anki.models import NoteType from anki.models import NoteType
from anki.notes import Note from anki.notes import Note
from anki.utils import ( from anki.utils import fmtTimeSpan, htmlToTextLine, ids2str, intTime, isMac, isWin
bodyClass,
fmtTimeSpan,
htmlToTextLine,
ids2str,
intTime,
isMac,
isWin,
)
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.editor import Editor from aqt.editor import Editor
from aqt.qt import * from aqt.qt import *
from aqt.sound import av_player from aqt.sound import av_player
from aqt.theme import theme_manager
from aqt.utils import ( from aqt.utils import (
MenuList, MenuList,
SubMenu, SubMenu,
@ -366,16 +359,6 @@ class DataModel(QAbstractTableModel):
# Line painter # Line painter
###################################################################### ######################################################################
COLOUR_SUSPENDED = "#FFFFB2"
COLOUR_MARKED = "#ccc"
flagColours = {
1: "#ffaaaa",
2: "#ffb347",
3: "#82E0AA",
4: "#85C1E9",
}
class StatusDelegate(QItemDelegate): class StatusDelegate(QItemDelegate):
def __init__(self, browser, model): def __init__(self, browser, model):
@ -399,13 +382,13 @@ class StatusDelegate(QItemDelegate):
col = None col = None
if c.userFlag() > 0: if c.userFlag() > 0:
col = flagColours[c.userFlag()] col = theme_manager.qcolor(f"flag{c.userFlag()}-bg")
elif c.note().hasTag("Marked"): elif c.note().hasTag("Marked"):
col = COLOUR_MARKED col = theme_manager.qcolor("marked-bg")
elif c.queue == -1: elif c.queue == -1:
col = COLOUR_SUSPENDED col = theme_manager.qcolor("suspended-bg")
if col: if col:
brush = QBrush(QColor(col)) brush = QBrush(col)
painter.save() painter.save()
painter.fillRect(option.rect, brush) painter.fillRect(option.rect, brush)
painter.restore() painter.restore()
@ -450,7 +433,6 @@ class SidebarModel(QAbstractItemModel):
def __init__(self, root: SidebarItem) -> None: def __init__(self, root: SidebarItem) -> None:
super().__init__() super().__init__()
self.root = root self.root = root
self.iconCache: Dict[str, QIcon] = {}
# Qt API # Qt API
###################################################################### ######################################################################
@ -510,18 +492,11 @@ class SidebarModel(QAbstractItemModel):
elif role == Qt.ToolTipRole: elif role == Qt.ToolTipRole:
return QVariant(item.tooltip) return QVariant(item.tooltip)
else: else:
return QVariant(self.iconFromRef(item.icon)) return QVariant(theme_manager.icon_from_resources(item.icon))
# Helpers # Helpers
###################################################################### ######################################################################
def iconFromRef(self, iconRef: str) -> QIcon:
icon = self.iconCache.get(iconRef)
if icon is None:
icon = QIcon(iconRef)
self.iconCache[iconRef] = icon
return icon
def expandWhereNeccessary(self, tree: QTreeView) -> None: def expandWhereNeccessary(self, tree: QTreeView) -> None:
for row, child in enumerate(self.root.children): for row, child in enumerate(self.root.children):
if child.expanded: if child.expanded:
@ -821,8 +796,10 @@ class Browser(QMainWindow):
self.form.tableView.selectionModel() self.form.tableView.selectionModel()
self.form.tableView.setItemDelegate(StatusDelegate(self, self.model)) self.form.tableView.setItemDelegate(StatusDelegate(self, self.model))
self.form.tableView.selectionModel().selectionChanged.connect(self.onRowChanged) self.form.tableView.selectionModel().selectionChanged.connect(self.onRowChanged)
if not theme_manager.night_mode:
self.form.tableView.setStyleSheet( self.form.tableView.setStyleSheet(
"QTableView{ selection-background-color: rgba(127, 127, 127, 50); }" "QTableView{ selection-background-color: rgba(150, 150, 150, 50); "
"selection-color: black; }"
) )
self.singleCard = False self.singleCard = False
@ -1709,7 +1686,7 @@ where id in %s"""
txt = c.a() txt = c.a()
txt = re.sub(r"\[\[type:[^]]+\]\]", "", txt) txt = re.sub(r"\[\[type:[^]]+\]\]", "", txt)
bodyclass = bodyClass(self.mw.col, c) bodyclass = theme_manager.body_classes_for_card_ord(c.ord)
if self.mw.reviewer.autoplay(c): if self.mw.reviewer.autoplay(c):
# if we're showing both sides at once, play question audio first # if we're showing both sides at once, play question audio first

View file

@ -9,10 +9,11 @@ import re
import aqt import aqt
from anki.consts import * from anki.consts import *
from anki.lang import _, ngettext from anki.lang import _, ngettext
from anki.utils import bodyClass, isMac, isWin, joinFields from anki.utils import isMac, isWin, joinFields
from aqt import gui_hooks from aqt import gui_hooks
from aqt.qt import * from aqt.qt import *
from aqt.sound import av_player from aqt.sound import av_player
from aqt.theme import theme_manager
from aqt.utils import ( from aqt.utils import (
askUser, askUser,
downArrow, downArrow,
@ -336,7 +337,7 @@ Please create a new card type first."""
c = self.card c = self.card
ti = self.maybeTextInput ti = self.maybeTextInput
bodyclass = bodyClass(self.mw.col, c) bodyclass = theme_manager.body_classes_for_card_ord(c.ord)
q = ti(mungeQA(self.mw.col, c.q(reload=True))) q = ti(mungeQA(self.mw.col, c.q(reload=True)))
q = gui_hooks.card_will_show(q, c, "clayoutQuestion") q = gui_hooks.card_will_show(q, c, "clayoutQuestion")

View file

@ -209,16 +209,16 @@ where id > ?""",
name, name,
) )
# due counts # due counts
def nonzeroColour(cnt, colour): def nonzeroColour(cnt, klass):
if not cnt: if not cnt:
colour = "#e0e0e0" klass = "zero-count"
if cnt >= 1000: if cnt >= 1000:
cnt = "1000+" cnt = "1000+"
return "<font color='%s'>%s</font>" % (colour, cnt) return f'<span class="{klass}">{cnt}</span>'
buf += "<td align=right>%s</td><td align=right>%s</td>" % ( buf += "<td align=right>%s</td><td align=right>%s</td>" % (
nonzeroColour(due, "#007700"), nonzeroColour(due, "review-count"),
nonzeroColour(new, "#000099"), nonzeroColour(new, "new-count"),
) )
# options # options
buf += ( buf += (

View file

@ -442,10 +442,10 @@ class Editor:
self.web.evalWithCallback("saveNow(%d)" % keepFocus, lambda res: callback()) self.web.evalWithCallback("saveNow(%d)" % keepFocus, lambda res: callback())
def checkValid(self): def checkValid(self):
cols = ["#fff"] * len(self.note.fields) cols = [""] * len(self.note.fields)
err = self.note.dupeOrEmpty() err = self.note.dupeOrEmpty()
if err == 2: if err == 2:
cols[0] = "#fcc" cols[0] = "dupe"
self.web.eval("showDupes();") self.web.eval("showDupes();")
else: else:
self.web.eval("hideDupes();") self.web.eval("hideDupes();")

View file

@ -5,7 +5,6 @@
import faulthandler import faulthandler
import gc import gc
import os import os
import platform
import re import re
import signal import signal
import time import time
@ -37,6 +36,7 @@ from aqt.profiles import ProfileManager as ProfileManagerType
from aqt.qt import * from aqt.qt import *
from aqt.qt import sip from aqt.qt import sip
from aqt.taskman import TaskManager from aqt.taskman import TaskManager
from aqt.theme import theme_manager
from aqt.utils import ( from aqt.utils import (
askUser, askUser,
checkInvalidFilename, checkInvalidFilename,
@ -58,6 +58,7 @@ class AnkiQt(QMainWindow):
col: _Collection col: _Collection
pm: ProfileManagerType pm: ProfileManagerType
web: aqt.webview.AnkiWebView web: aqt.webview.AnkiWebView
bottomWeb: aqt.webview.AnkiWebView
def __init__( def __init__(
self, self,
@ -111,9 +112,9 @@ class AnkiQt(QMainWindow):
self.setupMediaServer() self.setupMediaServer()
self.setupSound() self.setupSound()
self.setupSpellCheck() self.setupSpellCheck()
self.setupStyle()
self.setupMainWindow() self.setupMainWindow()
self.setupSystemSpecific() self.setupSystemSpecific()
self.setupStyle()
self.setupMenus() self.setupMenus()
self.setupProgress() self.setupProgress()
self.setupErrorHandler() self.setupErrorHandler()
@ -850,33 +851,8 @@ title="%s" %s>%s</button>""" % (
return True return True
def setupStyle(self) -> None: def setupStyle(self) -> None:
buf = "" theme_manager.night_mode = self.pm.night_mode()
theme_manager.apply_style(self.app)
if isWin and platform.release() == "10":
# add missing bottom border to menubar
buf += """
QMenuBar {
border-bottom: 1px solid #aaa;
background: white;
}
"""
# qt bug? setting the above changes the browser sidebar
# to white as well, so set it back
buf += """
QTreeWidget {
background: #eee;
}
"""
# allow addons to modify the styling
buf = gui_hooks.style_did_init(buf)
# allow users to extend styling
p = os.path.join(aqt.mw.pm.base, "style.css")
if os.path.exists(p):
buf += open(p).read()
self.app.setStyleSheet(buf)
# Key handling # Key handling
########################################################################## ##########################################################################

View file

@ -189,9 +189,9 @@ to their original deck."""
<table width=400 cellpadding=5> <table width=400 cellpadding=5>
<tr><td align=center valign=top> <tr><td align=center valign=top>
<table cellspacing=5> <table cellspacing=5>
<tr><td>%s:</td><td><b><font color=#00a>%s</font></b></td></tr> <tr><td>%s:</td><td><b><span class=new-count>%s</span></b></td></tr>
<tr><td>%s:</td><td><b><font color=#C35617>%s</font></b></td></tr> <tr><td>%s:</td><td><b><font class=learn-count>%s</span></b></td></tr>
<tr><td>%s:</td><td><b><font color=#0a0>%s</font></b></td></tr> <tr><td>%s:</td><td><b><span class=review-count>%s</span></b></td></tr>
</table> </table>
</td><td align=center> </td><td align=center>
%s</td></tr></table>""" % ( %s</td></tr></table>""" % (

View file

@ -85,7 +85,6 @@ class Preferences(QDialog):
f.timeLimit.setValue(qc["timeLim"] / 60.0) f.timeLimit.setValue(qc["timeLim"] / 60.0)
f.showEstimates.setChecked(qc["estTimes"]) f.showEstimates.setChecked(qc["estTimes"])
f.showProgress.setChecked(qc["dueCounts"]) f.showProgress.setChecked(qc["dueCounts"])
f.nightMode.setChecked(qc.get("nightMode", False))
f.newSpread.addItems(list(c.newCardSchedulingLabels().values())) f.newSpread.addItems(list(c.newCardSchedulingLabels().values()))
f.newSpread.setCurrentIndex(qc["newSpread"]) f.newSpread.setCurrentIndex(qc["newSpread"])
f.useCurrent.setCurrentIndex(int(not qc.get("addToCur", True))) f.useCurrent.setCurrentIndex(int(not qc.get("addToCur", True)))
@ -113,7 +112,6 @@ class Preferences(QDialog):
qc["dueCounts"] = f.showProgress.isChecked() qc["dueCounts"] = f.showProgress.isChecked()
qc["estTimes"] = f.showEstimates.isChecked() qc["estTimes"] = f.showEstimates.isChecked()
qc["newSpread"] = f.newSpread.currentIndex() qc["newSpread"] = f.newSpread.currentIndex()
qc["nightMode"] = f.nightMode.isChecked()
qc["timeLim"] = f.timeLimit.value() * 60 qc["timeLim"] = f.timeLimit.value() * 60
qc["collapseTime"] = f.lrnCutoff.value() * 60 qc["collapseTime"] = f.lrnCutoff.value() * 60
qc["addToCur"] = not f.useCurrent.currentIndex() qc["addToCur"] = not f.useCurrent.currentIndex()
@ -227,12 +225,22 @@ Not currently enabled; click the sync button in the main window to enable."""
self.form.uiScale.setValue(self.mw.pm.uiScale() * 100) self.form.uiScale.setValue(self.mw.pm.uiScale() * 100)
self.form.pasteInvert.setChecked(self.prof.get("pasteInvert", False)) self.form.pasteInvert.setChecked(self.prof.get("pasteInvert", False))
self.form.showPlayButtons.setChecked(self.prof.get("showPlayButtons", True)) self.form.showPlayButtons.setChecked(self.prof.get("showPlayButtons", True))
self.form.nightMode.setChecked(self.mw.pm.night_mode())
def updateOptions(self): def updateOptions(self):
restart_required = False
self.prof["pastePNG"] = self.form.pastePNG.isChecked() self.prof["pastePNG"] = self.form.pastePNG.isChecked()
self.prof["pasteInvert"] = self.form.pasteInvert.isChecked() self.prof["pasteInvert"] = self.form.pasteInvert.isChecked()
newScale = self.form.uiScale.value() / 100 newScale = self.form.uiScale.value() / 100
if newScale != self.mw.pm.uiScale(): if newScale != self.mw.pm.uiScale():
self.mw.pm.setUiScale(newScale) self.mw.pm.setUiScale(newScale)
showInfo(_("Changes will take effect when you restart Anki.")) restart_required = True
self.prof["showPlayButtons"] = self.form.showPlayButtons.isChecked() self.prof["showPlayButtons"] = self.form.showPlayButtons.isChecked()
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(_("Changes will take effect when you restart Anki."))

View file

@ -495,3 +495,9 @@ please see:
def set_last_addon_update_check(self, secs): def set_last_addon_update_check(self, secs):
self.meta["last_addon_update_check"] = secs self.meta["last_addon_update_check"] = secs
def night_mode(self) -> bool:
return self.meta.get("night_mode", False)
def set_night_mode(self, on: bool) -> None:
self.meta["night_mode"] = on

View file

@ -14,10 +14,11 @@ from anki import hooks
from anki.cards import Card from anki.cards import Card
from anki.lang import _, ngettext from anki.lang import _, ngettext
from anki.sound import AVTag from anki.sound import AVTag
from anki.utils import bodyClass, stripHTML from anki.utils import stripHTML
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.qt import * from aqt.qt import *
from aqt.sound import av_player, getAudio from aqt.sound import av_player, getAudio
from aqt.theme import theme_manager
from aqt.toolbar import BottomBar from aqt.toolbar import BottomBar
from aqt.utils import ( from aqt.utils import (
askUserDialog, askUserDialog,
@ -199,7 +200,7 @@ The front of this card is empty. Please run Tools>Empty Cards."""
q = self._mungeQA(q) q = self._mungeQA(q)
q = gui_hooks.card_will_show(q, c, "reviewQuestion") q = gui_hooks.card_will_show(q, c, "reviewQuestion")
bodyclass = bodyClass(self.mw.col, c) bodyclass = theme_manager.body_classes_for_card_ord(c.ord)
self.web.eval("_showQuestion(%s,'%s');" % (json.dumps(q), bodyclass)) self.web.eval("_showQuestion(%s,'%s');" % (json.dumps(q), bodyclass))
self._drawFlag() self._drawFlag()
@ -600,9 +601,9 @@ time = %(time)d;
idx = self.mw.col.sched.countIdx(self.card) idx = self.mw.col.sched.countIdx(self.card)
counts[idx] = "<u>%s</u>" % (counts[idx]) counts[idx] = "<u>%s</u>" % (counts[idx])
space = " + " space = " + "
ctxt = '<font color="#000099">%s</font>' % counts[0] ctxt = "<span class=new-count>%s</span>" % counts[0]
ctxt += space + '<font color="#C35617">%s</font>' % counts[1] ctxt += space + "<span class=learn-count>%s</span>" % counts[1]
ctxt += space + '<font color="#007700">%s</font>' % counts[2] ctxt += space + "<span class=review-count>%s</span>" % counts[2]
return ctxt return ctxt
def _defaultEase(self): def _defaultEase(self):

126
qt/aqt/theme.py Normal file
View file

@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import platform
from typing import Dict
from aqt import QApplication, gui_hooks, isWin
from aqt.colors import colors
from aqt.qt import QColor, QIcon, QPalette, QPixmap, QStyleFactory, Qt
class ThemeManager:
night_mode = True
_icon_cache: Dict[str, QIcon] = {}
_icon_size = 128
def icon_from_resources(self, path: str) -> QIcon:
"Fetch icon from Qt resources, and invert if in night mode."
icon = self._icon_cache.get(path)
if icon:
return icon
icon = QIcon(path)
if self.night_mode:
img = icon.pixmap(self._icon_size, self._icon_size).toImage()
img.invertPixels()
icon = QIcon(QPixmap(img))
return self._icon_cache.setdefault(path, icon)
def body_class(self) -> str:
"Returns '' in normal mode, 'nightMode' in night mode."
return self.night_mode and "nightMode" or ""
def body_classes_for_card_ord(self, card_ord: int) -> str:
"Returns body classes used when showing a card."
return f"card card{card_ord+1} {self.body_class()}"
def str_color(self, key: str) -> str:
"""Get a color defined in _vars.scss
If the colour is called '$day-frame-bg', key should be
'frame-bg'.
Returns the color as a string hex code or color name."""
prefix = self.night_mode and "night-" or "day-"
c = colors.get(prefix + key)
if c is None:
raise Exception("no such color:", key)
return c
def qcolor(self, key: str) -> QColor:
"""Get a color defined in _vars.scss as a QColor."""
return QColor(self.str_color(key))
def apply_style(self, app: QApplication) -> None:
self._apply_palette(app)
self._apply_style(app)
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;
}
"""
# qt bug? setting the above changes the browser sidebar
# to white as well, so set it back
buf += """
QTreeWidget {
background: #eee;
}
"""
# allow addons to modify the styling
buf = gui_hooks.style_did_init(buf)
app.setStyleSheet(buf)
def _apply_palette(self, app: QApplication) -> None:
if not self.night_mode:
return
app.setStyle(QStyleFactory.create("fusion")) # type: ignore
palette = QPalette()
text_fg = self.qcolor("text-fg")
palette.setColor(QPalette.WindowText, text_fg)
palette.setColor(QPalette.ToolTipBase, text_fg)
palette.setColor(QPalette.ToolTipText, text_fg)
palette.setColor(QPalette.Text, text_fg)
palette.setColor(QPalette.ButtonText, text_fg)
hlbg = self.qcolor("highlight-bg")
hlbg.setAlpha(64)
palette.setColor(QPalette.HighlightedText, self.qcolor("highlight-fg"))
palette.setColor(QPalette.Highlight, hlbg)
window_bg = self.qcolor("window-bg")
palette.setColor(QPalette.Window, window_bg)
palette.setColor(QPalette.AlternateBase, window_bg)
palette.setColor(QPalette.Button, window_bg)
palette.setColor(QPalette.Base, self.qcolor("frame-bg"))
disabled_color = self.qcolor("disabled")
palette.setColor(QPalette.Disabled, QPalette.Text, disabled_color)
palette.setColor(QPalette.Disabled, QPalette.ButtonText, disabled_color)
palette.setColor(QPalette.Disabled, QPalette.HighlightedText, disabled_color)
palette.setColor(QPalette.Link, self.qcolor("link"))
palette.setColor(QPalette.BrightText, Qt.red)
app.setPalette(palette)
theme_manager = ThemeManager()

View file

@ -10,6 +10,7 @@ from anki.lang import _
from anki.utils import isLin, isMac, isWin from anki.utils import isLin, isMac, isWin
from aqt import gui_hooks from aqt import gui_hooks
from aqt.qt import * from aqt.qt import *
from aqt.theme import theme_manager
from aqt.utils import openLink from aqt.utils import openLink
# Page for debug messages # Page for debug messages
@ -308,6 +309,8 @@ div[contenteditable="true"]:focus {
head = mw.baseHTML() + head + csstxt + jstxt head = mw.baseHTML() + head + csstxt + jstxt
body_class = theme_manager.body_class()
html = """ html = """
<!doctype html> <!doctype html>
<html><head> <html><head>
@ -321,7 +324,7 @@ body {{ zoom: {}; background: {}; {} }}
{} {}
</head> </head>
<body>{}</body> <body class="{}">{}</body>
</html>""".format( </html>""".format(
self.title, self.title,
self.zoomFactor(), self.zoomFactor(),
@ -329,6 +332,7 @@ body {{ zoom: {}; background: {}; {} }}
fontspec, fontspec,
widgetspec, widgetspec,
head, head,
body_class,
body, body,
) )
# print(html) # print(html)

View file

@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>422</width> <width>422</width>
<height>579</height> <height>586</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -111,7 +111,7 @@
<item> <item>
<widget class="QCheckBox" name="nightMode"> <widget class="QCheckBox" name="nightMode">
<property name="text"> <property name="text">
<string>Show cards as white on black (night mode)</string> <string>Night mode</string>
</property> </property>
</widget> </widget>
</item> </item>

View file

@ -0,0 +1,23 @@
import re
import json
colors = {}
for line in open("ts/scss/_vars.scss"):
line = line.strip()
if not line:
continue
m = re.match(r"^\$(.+): (.+);$", line)
if not m:
print("failed to match", line)
continue
var = m.group(1)
val = m.group(2)
colors[var] = val
with open("aqt/colors.py", "w") as buf:
buf.write("# this file is auto-generated from _vars.scss\n")
buf.write("colors = " + json.dumps(colors))

27
qt/ts/scss/_buttons.scss Normal file
View file

@ -0,0 +1,27 @@
@use 'vars';
.nightMode {
button {
-webkit-appearance: none;
color: vars.$night-text-fg;
/* match the fusion button gradient */
background: linear-gradient(0deg,
vars.$fusion-button-gradient-start 0%,
vars.$fusion-button-gradient-end 100%);
box-shadow: 0 0 3px vars.$fusion-button-outline;
border: 1px solid vars.$night-faint-border;
border-radius: 3px;
height: 24px;
padding: 5px;
padding-left: 10px;
padding-right: 10px;
padding-bottom: 18px;
}
button:hover {
background: vars.$fusion-button-hover-bg;
}
}

View file

@ -0,0 +1,28 @@
@use 'vars';
.review-count {
color: vars.$day-review-count;
}
.new-count {
color: vars.$day-new-count;
}
.learn-count {
color: vars.$day-learn-count;
}
.nightMode {
.review-count {
color: vars.$night-review-count;
}
.new-count {
color: vars.$night-new-count;
}
.learn-count {
color: vars.$night-learn-count;
}
}

46
qt/ts/scss/_vars.scss Normal file
View file

@ -0,0 +1,46 @@
$day-text-fg: black;
$day-window-bg: white;
$day-frame-bg: white;
$day-border: #aaa;
$day-faint-border: #e7e7e7;
$day-link: #00a;
$day-review-count: #0a0;
$day-new-count: #00a;
$day-learn-count: #C35617;
$day-zero-count: #aaa;
$day-slightly-grey-text: #333;
$day-highlight-bg: #77ccff;
$day-highlight-fg: black;
$day-disabled: #777;
$day-flag1-bg: #ffaaaa;
$day-flag2-bg: #ffb347;
$day-flag3-bg: #82E0AA;
$day-flag4-bg: #85C1E9;
$day-suspended-bg: #FFFFB2;
$day-marked-bg: #cce;
$night-text-fg: white;
$night-window-bg: #2f2f31;
$night-frame-bg: #3a3a3a;
$night-border: #777;
$night-faint-border: #444;
$night-link: #aaf;
$night-review-count: #7CFC00;
$night-new-count: #77ccff;
$night-learn-count: #FF935B;
$night-zero-count: #777;
$night-slightly-grey-text: #ccc;
$night-highlight-bg: #77ccff;
$night-highlight-fg: white;
$night-disabled: #777;
$night-flag1-bg: #aa5555;
$night-flag2-bg: #aa6337;
$night-flag3-bg: #33a055;
$night-flag4-bg: #3581a9;
$night-suspended-bg: #aaaa33;
$night-marked-bg: #77c;
$fusion-button-gradient-start: #363636;
$fusion-button-gradient-end: #404040;
$fusion-button-outline: #000;
$fusion-button-hover-bg: #454545;

View file

@ -1,8 +1,11 @@
/* Copyright: Ankitects Pty Ltd and contributors /* Copyright: Ankitects Pty Ltd and contributors
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
@use 'vars';
@use 'card_counts';
a.deck { a.deck {
color: #000; color: vars.$day-text-fg;
text-decoration: none; text-decoration: none;
min-width: 5em; min-width: 5em;
display: inline-block; display: inline-block;
@ -13,7 +16,7 @@ a.deck:hover {
} }
tr.deck td { tr.deck td {
border-bottom: 1px solid #e7e7e7; border-bottom: 1px solid vars.$day-faint-border;
} }
tr.top-level-drag-row td { tr.top-level-drag-row td {
@ -25,7 +28,7 @@ td {
} }
tr.drag-hover td { tr.drag-hover td {
border-bottom: 1px solid #aaa; border-bottom: 1px solid vars.$day-border;
} }
body { body {
@ -34,7 +37,7 @@ body {
} }
.current { .current {
background-color: #e7e7e7; background-color: vars.$day-faint-border;
} }
.decktd { .decktd {
@ -51,14 +54,14 @@ body {
} }
.collapse { .collapse {
color: #000; color: vars.$day-text-fg;
text-decoration: none; text-decoration: none;
display: inline-block; display: inline-block;
width: 1em; width: 1em;
} }
.filtered { .filtered {
color: #00a !important; color: vars.$day-link !important;
} }
.gears { .gears {
@ -67,3 +70,29 @@ body {
opacity: .5; opacity: .5;
padding-top: 0.2em; padding-top: 0.2em;
} }
.nightMode {
a.deck {
color: vars.$night-text-fg;
}
tr.deck td {
border-bottom-color: vars.$night-faint-border;
}
tr.drag-hover td {
border-bottom-color: vars.$night-border;
}
.current {
background-color: vars.$night-faint-border;
}
.collapse {
color: vars.$night-text-fg;
}
.gears {
filter: invert(180);
}
}

View file

@ -1,10 +1,12 @@
/* Copyright: Ankitects Pty Ltd and contributors /* Copyright: Ankitects Pty Ltd and contributors
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
@use 'vars';
.field { .field {
border: 1px solid #aaa; border: 1px solid vars.$day-border;
background: #fff; background: vars.$day-frame-bg;
color: #000; color: vars.$day-text-fg;
padding: 5px; padding: 5px;
overflow-wrap: break-word; overflow-wrap: break-word;
} }
@ -49,9 +51,10 @@ body {
color-stop(100%, #77f)); color-stop(100%, #77f));
} }
.linkb { button.linkb {
-webkit-appearance: none; -webkit-appearance: none;
border: 0; border: 0;
box-shadow: none;
padding: 0px 2px; padding: 0px 2px;
background: transparent; background: transparent;
} }
@ -68,3 +71,27 @@ body {
#fields { #fields {
margin-top: 35px; margin-top: 35px;
} }
.dupe {
background: #fcc;
}
.nightMode {
.field {
border-color: vars.$night-border;
background: vars.$night-frame-bg;
color: vars.$night-text-fg;
}
button.linkb > img {
filter: invert(180);
}
.dupe {
background: #a00;
}
#dupes a {
color: vars.$night-link;
}
}

View file

@ -1,6 +1,9 @@
/* Copyright: Ankitects Pty Ltd and contributors /* Copyright: Ankitects Pty Ltd and contributors
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
@use 'vars';
@use 'card_counts';
.smallLink { .smallLink {
font-size: 10px; font-size: 10px;
} }
@ -11,7 +14,7 @@ h3 {
.descfont { .descfont {
padding: 1em; padding: 1em;
color: #333; color: vars.$day-slightly-grey-text;
} }
.description { .description {
@ -31,3 +34,9 @@ h3 {
.dyn { .dyn {
text-align: center; text-align: center;
} }
.nightMode {
.descfont {
color: vars.$night-slightly-grey-text;
}
}

View file

@ -1,8 +1,11 @@
/* Copyright: Ankitects Pty Ltd and contributors /* Copyright: Ankitects Pty Ltd and contributors
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
@use 'vars';
@use 'card_counts';
body { body {
margin: 0px; margin: 0;
padding: 0; padding: 0;
} }
@ -48,9 +51,15 @@ button {
} }
#outer { #outer {
border-top: 1px solid #aaa; border-top: 1px solid vars.$day-border;
} }
#innertable { #innertable {
padding: 3px; padding: 3px;
} }
.nightMode {
#outer {
border-top-color: vars.$night-border;
}
}

View file

@ -1,10 +1,12 @@
/* Copyright: Ankitects Pty Ltd and contributors /* Copyright: Ankitects Pty Ltd and contributors
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
@use 'vars';
#header { #header {
padding: 3px; padding: 3px;
font-weight: bold; font-weight: bold;
border-bottom: 1px solid #aaa; border-bottom: 1px solid vars.$day-border;
} }
.tdcenter { .tdcenter {
@ -26,11 +28,7 @@ body {
padding-right: 12px; padding-right: 12px;
padding-left: 12px; padding-left: 12px;
text-decoration: none; text-decoration: none;
color: #000; color: vars.$day-text-fg;
}
.nightMode .hitem {
color: white;
} }
.hitem:hover { .hitem:hover {
@ -38,3 +36,13 @@ body {
} }
.hitem:focus { outline: 0; } .hitem:focus { outline: 0; }
.nightMode {
.hitem {
color: vars.$night-text-fg;
}
#header {
border-bottom-color: vars.$night-border;
}
}

View file

@ -1,6 +1,9 @@
/* Copyright: Ankitects Pty Ltd and contributors /* Copyright: Ankitects Pty Ltd and contributors
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
@use 'vars';
@use 'buttons';
body { body {
margin: 2em; margin: 2em;
} }
@ -10,6 +13,6 @@ h1 {
} }
body.nightMode { body.nightMode {
color: white; color: vars.$night-text-fg;
background: #2f2f31; background: vars.$night-window-bg;
} }

View file

@ -352,7 +352,11 @@ function setFields(fields) {
function setBackgrounds(cols) { function setBackgrounds(cols) {
for (let i = 0; i < cols.length; i++) { for (let i = 0; i < cols.length; i++) {
$("#f" + i).css("background", cols[i]); if (cols[i] == "dupe") {
$("#f" + i).addClass("dupe");
} else {
$("#f" + i).removeClass("dupe");
}
} }
} }