mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
feat(koreader): colour the widget board on colour screens
Add a Colour setting to Settings > Home: Accents, the default, colours each widget's marks and draws Continue reading in its cover's deep shade; Tinted also washes every tile in its widget's colour; Off keeps the greys. Black-and- white screens draw as before, colour e-readers get stronger shades, and night mode keeps each colour's hue.
This commit is contained in:
@@ -263,7 +263,13 @@ function GlyphWidget:paintTo(bb, x, y)
|
||||
self.dimen.x, self.dimen.y = x, y
|
||||
local g = renderGlyph(self.file, self.codepoint, self.size)
|
||||
if not g then return end
|
||||
bb:colorblitFrom(g.bb, x, y, 0, 0, g.bb:getWidth(), g.bb:getHeight(), self.color or Blitbuffer.COLOR_BLACK)
|
||||
local color = self.color or Blitbuffer.COLOR_BLACK
|
||||
-- The plain colour blit keeps only gray; an RGB colour takes the RGB one.
|
||||
if Blitbuffer.isColor8 and not Blitbuffer.isColor8(color) and bb.colorblitFromRGB32 then
|
||||
bb:colorblitFromRGB32(g.bb, x, y, 0, 0, g.bb:getWidth(), g.bb:getHeight(), color)
|
||||
else
|
||||
bb:colorblitFrom(g.bb, x, y, 0, 0, g.bb:getWidth(), g.bb:getHeight(), color)
|
||||
end
|
||||
end
|
||||
|
||||
-- One character's bitmap at a pixel size: { bb, l, t }, or nil. Drawn by
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
--[[--
|
||||
Colour for the widget board on colour screens.
|
||||
|
||||
The Colour setting chooses how much of the board is coloured:
|
||||
accents the default. Each widget's marks (bars, dots, chart cells, the
|
||||
switched-on control, its header icon) take the widget's own
|
||||
colour, and Continue reading takes the deep shade of its cover.
|
||||
tinted accents, with every tile washed in its widget's colour; the
|
||||
tile style draws the edge in that colour, and the black clock
|
||||
and moon tiles take the deep shade.
|
||||
off the sixteen greys.
|
||||
Black-and-white screens always draw the greys: every colour here is asked
|
||||
for together with the grey it replaces.
|
||||
|
||||
Text stays black or white and controls stay black, so nothing depends on
|
||||
colour to be read. Kaleido 3 shows colour at half the resolution of black
|
||||
and about half its saturation, so a colour e-reader gets every shade with its
|
||||
chroma pushed out (KOReader's highlight colours are fully saturated for the
|
||||
same reason) and icons drawn a third heavier. Night mode inverts the screen,
|
||||
so each colour is painted with its hue turned half way round: it shows in its
|
||||
own hue at the inverse lightness.
|
||||
]]
|
||||
|
||||
local Colour = {}
|
||||
|
||||
Colour.SCHEMES = { "accents", "tinted", "off" }
|
||||
Colour.DEFAULT = "accents"
|
||||
|
||||
-- ink: bars, dots, icons. deep: a filled tile with white text on it.
|
||||
-- tint: tracks and pale marks. wash: a whole tile's background.
|
||||
local FAM = {
|
||||
red = { ink = "#de4127", deep = "#8c2716", tint = "#f8c9bd", wash = "#fbdcd3" },
|
||||
orange = { ink = "#ee8410", deep = "#8a4806", tint = "#fbd6a6", wash = "#fde5c6" },
|
||||
gold = { ink = "#d9a40c", deep = "#6b5006", tint = "#f6e3a0", wash = "#faedc2" },
|
||||
green = { ink = "#1e9857", deep = "#135a37", tint = "#c2e8d1", wash = "#d9f1e2" },
|
||||
teal = { ink = "#12908b", deep = "#0c5653", tint = "#bfe6e3", wash = "#d7f0ee" },
|
||||
blue = { ink = "#2b61d6", deep = "#1b397d", tint = "#c8d7f6", wash = "#dde6f9" },
|
||||
violet = { ink = "#7848c8", deep = "#45277c", tint = "#dbcff2", wash = "#e8e0f7" },
|
||||
rose = { ink = "#d0386f", deep = "#801f46", tint = "#f4c8d8", wash = "#f8dbe5" },
|
||||
brown = { ink = "#99602f", deep = "#57381c", tint = "#e8d3bf", wash = "#f1e4d7" },
|
||||
yellow = { ink = "#e9b800", deep = "#6b5500", tint = "#f9e57f", wash = "#fcefae" },
|
||||
night = { ink = "#7f8bd6", deep = "#1c2147", tint = "#c9cdee", wash = "#dfe1f5" },
|
||||
}
|
||||
Colour.FAM = FAM
|
||||
|
||||
-- Which family each widget wears. Tiles that touch on a starter page, on any
|
||||
-- screen, never share a family or a look-alike wash (gold beside yellow, red
|
||||
-- beside rose, green beside teal); a dark ink tile may sit beside a pale tile
|
||||
-- of its own family.
|
||||
local HUE = {
|
||||
hero = "cover", also = "blue", streak = "orange", week = "green", today = "green", goalpace = "rose",
|
||||
toggles = "violet", highlight = "yellow", heatmap = "green", forecast = "blue", hours = "violet", records = "gold",
|
||||
yearago = "blue", word = "rose", sync = "orange", clock = "blue", light = "gold", calendar = "red",
|
||||
buddy = "violet", analog = "blue", moon = "night", odometer = "red", glyph = "red", rings = "brown",
|
||||
chapters = "cover", badge = "violet", dust = "green",
|
||||
["link:library"] = "orange", ["link:authors"] = "rose", ["link:series"] = "violet", ["link:device"] = "gold",
|
||||
["link:collections"] = "orange", ["link:scopes"] = "teal", ["link:search"] = "blue",
|
||||
["shelf:random"] = "brown", ["shelf:recently-added"] = "teal", ["shelf:pick-back-up"] = "gold",
|
||||
["shelf:up-next-in-series"] = "violet", ["shelf:want-to-read"] = "red", ["shelf:finished"] = "teal",
|
||||
["shelf:in-progress"] = "blue",
|
||||
}
|
||||
Colour.HUE = HUE
|
||||
|
||||
local RARITY = { common = "green", rare = "blue", epic = "violet", legendary = "gold", ribbon = "red" }
|
||||
|
||||
-- -------------------------------------------------------------- colour math
|
||||
local function hexRGB(hex)
|
||||
return tonumber(hex:sub(2, 3), 16), tonumber(hex:sub(4, 5), 16), tonumber(hex:sub(6, 7), 16)
|
||||
end
|
||||
|
||||
local function clampByte(v)
|
||||
v = math.floor(v + 0.5)
|
||||
if v < 0 then return 0 end
|
||||
if v > 255 then return 255 end
|
||||
return v
|
||||
end
|
||||
|
||||
local function rgbHex(r, g, b)
|
||||
return string.format("#%02x%02x%02x", clampByte(r), clampByte(g), clampByte(b))
|
||||
end
|
||||
|
||||
function Colour.mix(a, b, t)
|
||||
local ar, ag, ab = hexRGB(a)
|
||||
local br, bg, bb = hexRGB(b)
|
||||
return rgbHex(ar + (br - ar) * t, ag + (bg - ag) * t, ab + (bb - ab) * t)
|
||||
end
|
||||
|
||||
-- Hue, lightness, saturation in 0..1, as Python's colorsys has them.
|
||||
local function rgbToHls(r, g, b)
|
||||
local maxc, minc = math.max(r, g, b), math.min(r, g, b)
|
||||
local l = (minc + maxc) / 2
|
||||
if maxc == minc then return 0, l, 0 end
|
||||
local s = l <= 0.5 and (maxc - minc) / (maxc + minc) or (maxc - minc) / (2 - maxc - minc)
|
||||
local rc, gc, bc = (maxc - r) / (maxc - minc), (maxc - g) / (maxc - minc), (maxc - b) / (maxc - minc)
|
||||
local h
|
||||
if r == maxc then h = bc - gc
|
||||
elseif g == maxc then h = 2 + rc - bc
|
||||
else h = 4 + gc - rc end
|
||||
return (h / 6) % 1, l, s
|
||||
end
|
||||
Colour.rgbToHls = rgbToHls
|
||||
|
||||
local function hueValue(m1, m2, hue)
|
||||
hue = hue % 1
|
||||
if hue < 1 / 6 then return m1 + (m2 - m1) * hue * 6 end
|
||||
if hue < 0.5 then return m2 end
|
||||
if hue < 2 / 3 then return m1 + (m2 - m1) * (2 / 3 - hue) * 6 end
|
||||
return m1
|
||||
end
|
||||
|
||||
local function hlsToRgb(h, l, s)
|
||||
if s == 0 then return l, l, l end
|
||||
local m2 = l <= 0.5 and l * (1 + s) or l + s - l * s
|
||||
local m1 = 2 * l - m2
|
||||
return hueValue(m1, m2, h + 1 / 3), hueValue(m1, m2, h), hueValue(m1, m2, h - 1 / 3)
|
||||
end
|
||||
|
||||
local function hlsHex(h, l, s)
|
||||
local r, g, b = hlsToRgb(h, l, s)
|
||||
return rgbHex(r * 255, g * 255, b * 255)
|
||||
end
|
||||
|
||||
-- Chroma pushed out from the colour's own luma.
|
||||
function Colour.boost(hex, k)
|
||||
local r, g, b = hexRGB(hex)
|
||||
local y = 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||
return rgbHex(y + (r - y) * k, y + (g - y) * k, y + (b - y) * k)
|
||||
end
|
||||
|
||||
-- What to paint so the inverted screen shows `hex`'s hue at the inverse
|
||||
-- lightness: the same lightness and saturation, the hue turned half way.
|
||||
function Colour.forNight(hex)
|
||||
local r, g, b = hexRGB(hex)
|
||||
local h, l, s = rgbToHls(r / 255, g / 255, b / 255)
|
||||
return hlsHex(h + 0.5, l, s)
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------- cover colours
|
||||
-- One family per cover: pixels that are nearly grey, nearly black or nearly
|
||||
-- white are dropped and the rest vote by hue, weighted by saturation. The
|
||||
-- winning hue gives the deep, ink and tint shades. A jacket with too few
|
||||
-- coloured pixels has none. pixels: a list of { r, g, b } in 0..255.
|
||||
function Colour.familyFromPixels(pixels)
|
||||
local BINS = 36
|
||||
local bins, sats = {}, {}
|
||||
for i = 0, BINS - 1 do bins[i], sats[i] = 0, {} end
|
||||
local total = #pixels
|
||||
if total == 0 then return nil end
|
||||
for _i, p in ipairs(pixels) do
|
||||
local h, l, s = rgbToHls(p[1] / 255, p[2] / 255, p[3] / 255)
|
||||
if s >= 0.28 and l >= 0.12 and l <= 0.9 then
|
||||
local k = math.floor(h * BINS) % BINS
|
||||
bins[k] = bins[k] + s * (1 - math.abs(l - 0.5))
|
||||
table.insert(sats[k], s)
|
||||
end
|
||||
end
|
||||
-- Neighbouring bins vote together, so a hue split across a bin edge wins.
|
||||
local best, best_v = 0, -1
|
||||
for i = 0, BINS - 1 do
|
||||
local v = bins[(i - 1) % BINS] * 0.5 + bins[i] + bins[(i + 1) % BINS] * 0.5
|
||||
if v > best_v then best, best_v = i, v end
|
||||
end
|
||||
if best_v / total < 0.06 then return nil end
|
||||
local hue = (best + 0.5) / BINS
|
||||
local list = sats[best]
|
||||
table.sort(list)
|
||||
local sat = #list > 0 and list[math.floor(#list / 2) + 1] or 0.5
|
||||
local tint = hlsHex(hue, 0.85, 0.7)
|
||||
return {
|
||||
deep = hlsHex(hue, 0.22, math.min(0.7, math.max(0.4, sat))),
|
||||
ink = hlsHex(hue, 0.44, math.min(0.85, math.max(0.6, sat))),
|
||||
tint = tint,
|
||||
wash = Colour.mix(tint, "#ffffff", 0.45),
|
||||
}
|
||||
end
|
||||
|
||||
local cover_cache, cover_count = {}, 0
|
||||
local NO_COVER = false
|
||||
|
||||
-- The family of the cover at `path`, worked out once per cover and kept.
|
||||
function Colour.coverFamily(path)
|
||||
if not path then return nil end
|
||||
local hit = cover_cache[path]
|
||||
if hit ~= nil then return hit or nil end
|
||||
local fam = nil
|
||||
local ok, bb = pcall(function() return require("ui/renderimage"):renderImageFile(path, false, 24, 36) end)
|
||||
if ok and bb then
|
||||
local pixels = {}
|
||||
for y = 0, bb:getHeight() - 1 do
|
||||
for x = 0, bb:getWidth() - 1 do
|
||||
local c = bb:getPixel(x, y):getColorRGB32()
|
||||
table.insert(pixels, { c.r, c.g, c.b })
|
||||
end
|
||||
end
|
||||
bb:free()
|
||||
fam = Colour.familyFromPixels(pixels)
|
||||
end
|
||||
if cover_count > 200 then cover_cache, cover_count = {}, 0 end
|
||||
cover_cache[path] = fam or NO_COVER
|
||||
cover_count = cover_count + 1
|
||||
return fam
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------- the screen
|
||||
-- Specs set Colour._env to { colour, eink, night } instead of a device.
|
||||
local function env()
|
||||
if Colour._env then return Colour._env end
|
||||
local Device = require("device")
|
||||
local Screen = Device.screen
|
||||
local Blitbuffer = require("ffi/blitbuffer")
|
||||
return {
|
||||
-- Older KOReader builds cannot paint RGB fills and borders.
|
||||
colour = Screen:isColorEnabled() and Blitbuffer.isColor8 ~= nil and Blitbuffer.ColorRGB32 ~= nil,
|
||||
eink = Device:hasEinkScreen() and true or false,
|
||||
night = Screen.night_mode and true or false,
|
||||
}
|
||||
end
|
||||
Colour.env = env
|
||||
|
||||
function Colour.normalize(value)
|
||||
for _i, s in ipairs(Colour.SCHEMES) do
|
||||
if s == value then return value end
|
||||
end
|
||||
return Colour.DEFAULT
|
||||
end
|
||||
|
||||
function Colour.scheme(ctx)
|
||||
return ctx and ctx.colour or "off"
|
||||
end
|
||||
|
||||
-- Colour reaches only screens that show it.
|
||||
function Colour.on(ctx)
|
||||
return Colour.scheme(ctx) ~= "off" and env().colour == true
|
||||
end
|
||||
|
||||
function Colour.tinted(ctx)
|
||||
return Colour.on(ctx) and ctx.colour == "tinted"
|
||||
end
|
||||
|
||||
-- The key a tile's colour is looked up by.
|
||||
function Colour.keyFor(type_, props)
|
||||
if type_ == "link" and props and props.link then return "link:" .. props.link end
|
||||
if type_ == "shelf" and props and props.source then return "shelf:" .. props.source end
|
||||
return type_
|
||||
end
|
||||
|
||||
local boosted = {}
|
||||
local function forPanel(fam)
|
||||
if not fam or not env().eink then return fam end
|
||||
local hit = boosted[fam]
|
||||
if hit then return hit end
|
||||
hit = {}
|
||||
for k, v in pairs(fam) do hit[k] = Colour.boost(v, 1.4) end
|
||||
boosted[fam] = hit
|
||||
return hit
|
||||
end
|
||||
|
||||
local function leadCover(ctx)
|
||||
local book = ctx and ctx.d and ctx.d.continue_reading and ctx.d.continue_reading[1]
|
||||
return book and Colour.coverFamily(book.cover_path) or nil
|
||||
end
|
||||
|
||||
-- The family for a widget key (the tile's own when nil) or a family name.
|
||||
function Colour.fam(ctx, key)
|
||||
key = key or (ctx and ctx.tile_key)
|
||||
if key == "hero" or key == "chapters" then return forPanel(leadCover(ctx) or FAM.blue) end
|
||||
if FAM[key] then return forPanel(FAM[key]) end
|
||||
return forPanel(FAM[HUE[key] or "blue"])
|
||||
end
|
||||
|
||||
function Colour.rarity(rarity)
|
||||
return forPanel(FAM[RARITY[rarity] or "violet"])
|
||||
end
|
||||
|
||||
-- -------------------------------------------------------- what is painted
|
||||
local paint_cache = {}
|
||||
|
||||
-- A hex colour as the screen should be given it: night mode turns its hue.
|
||||
function Colour.hex(hex)
|
||||
if env().night then return Colour.forNight(hex) end
|
||||
return hex
|
||||
end
|
||||
|
||||
-- A hex colour as a Blitbuffer colour, one object per colour, so a tile's
|
||||
-- chrome fingerprints the same way on every rebuild.
|
||||
function Colour.paint(hex)
|
||||
hex = Colour.hex(hex)
|
||||
local hit = paint_cache[hex]
|
||||
if hit then return hit end
|
||||
local Blitbuffer = require("ffi/blitbuffer")
|
||||
local r, g, b = hexRGB(hex)
|
||||
hit = Blitbuffer.ColorRGB32(r, g, b, 0xFF)
|
||||
paint_cache[hex] = hit
|
||||
return hit
|
||||
end
|
||||
|
||||
-- Each shade, or the grey given for a screen without colour.
|
||||
function Colour.ink(ctx, grey, key)
|
||||
if not Colour.on(ctx) then return grey end
|
||||
return Colour.paint(Colour.fam(ctx, key).ink)
|
||||
end
|
||||
|
||||
function Colour.deep(ctx, grey, key)
|
||||
if not Colour.on(ctx) then return grey end
|
||||
return Colour.paint(Colour.fam(ctx, key).deep)
|
||||
end
|
||||
|
||||
function Colour.tint(ctx, grey, key)
|
||||
if not Colour.on(ctx) then return grey end
|
||||
return Colour.paint(Colour.fam(ctx, key).tint)
|
||||
end
|
||||
|
||||
-- Between tint and ink: the other days beside today's full-colour bar.
|
||||
function Colour.mid(ctx, grey, key)
|
||||
if not Colour.on(ctx) then return grey end
|
||||
local f = Colour.fam(ctx, key)
|
||||
return Colour.paint(Colour.mix(f.tint, f.ink, 0.45))
|
||||
end
|
||||
|
||||
-- A bar's empty part: the pale shade on white, white on a washed tile.
|
||||
function Colour.track(ctx, grey, key)
|
||||
if not Colour.on(ctx) then return grey end
|
||||
if Colour.tinted(ctx) then return Colour.paint("#ffffff") end
|
||||
return Colour.paint(Colour.fam(ctx, key).tint)
|
||||
end
|
||||
|
||||
-- A five-step ramp from none to most (Year in reading).
|
||||
function Colour.ramp(ctx, greys, key)
|
||||
if not Colour.on(ctx) then return greys end
|
||||
local f = Colour.fam(ctx, key)
|
||||
local empty = Colour.tinted(ctx) and Colour.paint(Colour.mix(f.wash, "#ffffff", 0.55)) or greys[1]
|
||||
return { empty, Colour.paint(f.tint), Colour.paint(Colour.mix(f.tint, f.ink, 0.5)), Colour.paint(f.ink), Colour.paint(f.deep) }
|
||||
end
|
||||
|
||||
-- An icon's options: the widget's colour, heavier on a colour e-reader.
|
||||
function Colour.icon(ctx, sw, key)
|
||||
if not Colour.on(ctx) then return { sw = sw } end
|
||||
return { sw = env().eink and math.floor(sw * 135 + 0.5) / 100 or sw, col = Colour.hex(Colour.fam(ctx, key).ink) }
|
||||
end
|
||||
|
||||
-- A hex shade for SVG art, or the grey's hex.
|
||||
function Colour.artHex(ctx, grey_hex, shade, key)
|
||||
if not Colour.on(ctx) then return grey_hex end
|
||||
return Colour.hex(Colour.fam(ctx, key)[shade or "ink"])
|
||||
end
|
||||
|
||||
-- Ink tiles (Continue reading, the clocks, the moon) keep black in accents,
|
||||
-- except Continue reading, which takes its cover's deep shade; tinted gives
|
||||
-- every one its deep shade. A jacket without colour keeps black.
|
||||
function Colour.inkFill(ctx, key)
|
||||
key = key or (ctx and ctx.tile_key)
|
||||
local Blitbuffer = require("ffi/blitbuffer")
|
||||
if not Colour.on(ctx) then return Blitbuffer.COLOR_BLACK end
|
||||
if key == "hero" then
|
||||
local lead = leadCover(ctx)
|
||||
return lead and Colour.paint(forPanel(lead).deep) or Blitbuffer.COLOR_BLACK
|
||||
end
|
||||
if Colour.tinted(ctx) then return Colour.paint(Colour.fam(ctx, key).deep) end
|
||||
return Blitbuffer.COLOR_BLACK
|
||||
end
|
||||
|
||||
local function inkFillHex(ctx, key)
|
||||
if not Colour.on(ctx) then return nil end
|
||||
if key == "hero" then
|
||||
local lead = leadCover(ctx)
|
||||
return lead and forPanel(lead).deep or nil
|
||||
end
|
||||
return Colour.tinted(ctx) and Colour.fam(ctx, key).deep or nil
|
||||
end
|
||||
|
||||
-- Quiet text on an ink tile: the grey on black, a pale shade on colour.
|
||||
function Colour.onInk(ctx, grey, key)
|
||||
key = key or (ctx and ctx.tile_key)
|
||||
if not inkFillHex(ctx, key) then return grey end
|
||||
return Colour.paint(Colour.mix(Colour.fam(ctx, key).tint, "#ffffff", 0.35))
|
||||
end
|
||||
|
||||
-- The same for an icon: an INK key on black, the pale shade's hex on colour.
|
||||
function Colour.onInkHex(ctx, grey_key, key)
|
||||
key = key or (ctx and ctx.tile_key)
|
||||
if not inkFillHex(ctx, key) then return grey_key end
|
||||
return Colour.hex(Colour.mix(Colour.fam(ctx, key).tint, "#ffffff", 0.35))
|
||||
end
|
||||
|
||||
-- A bar's track on an ink tile.
|
||||
function Colour.inkTrack(ctx, grey, key)
|
||||
key = key or (ctx and ctx.tile_key)
|
||||
local fill = inkFillHex(ctx, key)
|
||||
return fill and Colour.paint(Colour.mix(fill, "#ffffff", 0.3)) or grey
|
||||
end
|
||||
|
||||
-- Tile chrome. base is the tile style's own; tinted keeps its edge, drawn in
|
||||
-- the widget's colour: Outline a hairline a step darker than the wash, Soft no
|
||||
-- edge, Ink a firm edge in the deep shade. Highlight of the day is yellow,
|
||||
-- the colour KOReader highlights in, in both schemes.
|
||||
function Colour.chrome(ctx, chrome, key, base, hairline, firm)
|
||||
if not Colour.on(ctx) or chrome == "open" then return base end
|
||||
local out = {}
|
||||
for k, v in pairs(base) do out[k] = v end
|
||||
if chrome == "ink" then
|
||||
out.bg = Colour.inkFill(ctx, key)
|
||||
return out
|
||||
end
|
||||
local tinted = Colour.tinted(ctx)
|
||||
if not tinted and key ~= "highlight" then return base end
|
||||
local f = Colour.fam(ctx, key)
|
||||
out.bg = Colour.paint(f.wash)
|
||||
out.border, out.border_col = 0, nil
|
||||
if tinted then
|
||||
local tiles = ctx.tiles or "outline"
|
||||
if tiles == "outline" then
|
||||
out.border, out.border_col = hairline, Colour.paint(Colour.mix(f.wash, f.ink, 0.4))
|
||||
elseif tiles == "ink" then
|
||||
out.border, out.border_col = firm, Colour.paint(f.deep)
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- Whatever sits on a tile and assumed white or grey (the halo between fanned
|
||||
-- covers, a cover's hairline) takes the tile's own colour on a washed tile.
|
||||
function Colour.halo(ctx, grey)
|
||||
if not Colour.tinted(ctx) then return grey end
|
||||
return Colour.paint(Colour.fam(ctx).wash)
|
||||
end
|
||||
|
||||
function Colour.coverEdge(ctx, grey)
|
||||
if not Colour.tinted(ctx) then return grey end
|
||||
local f = Colour.fam(ctx)
|
||||
return Colour.paint(Colour.mix(f.wash, f.deep, 0.3))
|
||||
end
|
||||
|
||||
function Colour.clearCache()
|
||||
cover_cache, cover_count, boosted, paint_cache = {}, 0, {}, {}
|
||||
end
|
||||
|
||||
return Colour
|
||||
@@ -23,6 +23,7 @@ local T = require("ffi/util").template
|
||||
local _ = require("gettext")
|
||||
|
||||
local BoardArt = require("bookorbit_board_art")
|
||||
local BoardColour = require("bookorbit_board_colour")
|
||||
local BoardData = require("bookorbit_board_data")
|
||||
local BoardIcons = require("bookorbit_board_icons")
|
||||
local BoardMetrics = require("bookorbit_board_metrics")
|
||||
@@ -38,6 +39,12 @@ local Decor = {}
|
||||
-- Set by install: the board's own header row and number formatting.
|
||||
local head, formatCount
|
||||
|
||||
-- A hex for decor art: `colour` on a screen showing the Colour setting, the
|
||||
-- grey otherwise.
|
||||
local function artHex(ctx, grey, colour)
|
||||
return BoardColour.on(ctx) and BoardColour.hex(colour) or grey
|
||||
end
|
||||
|
||||
local function clamp(v, lo, hi)
|
||||
if v < lo then return lo end
|
||||
if v > hi then return hi end
|
||||
@@ -135,10 +142,11 @@ local function bubble(w, lines, s, tail, second_is_data)
|
||||
local bw = math.max(1, px(1.5))
|
||||
local pad_x, pad_y = px(10), px(7)
|
||||
local tw = w - 2 * pad_x - 2 * bw
|
||||
local items = { text(lines[1], { s = s, b = true, w = tw, lines = 2 }) }
|
||||
-- The bubble is white whatever the tile under it is, and so is its text's ground.
|
||||
local items = { text(lines[1], { s = s, b = true, w = tw, lines = 2, bg = INK.white }) }
|
||||
if lines[2] then
|
||||
table.insert(items, space(nil, px(2)))
|
||||
table.insert(items, text(lines[2], { s = math.max(11, s - 2), col = INK.g4, w = tw, lines = 2, data = second_is_data }))
|
||||
table.insert(items, text(lines[2], { s = math.max(11, s - 2), col = INK.g4, w = tw, lines = 2, data = second_is_data, bg = INK.white }))
|
||||
end
|
||||
local inner = vstack({ w = tw }, items)
|
||||
local h = inner:getSize().h + 2 * pad_y + 2 * bw
|
||||
@@ -270,7 +278,7 @@ function Decor.buddy(ctx, w, hh, props)
|
||||
end
|
||||
|
||||
-- ================================================================ analog clock
|
||||
local function clockFace(dia, hour, minute)
|
||||
local function clockFace(dia, hour, minute, accent)
|
||||
local r = dia / 2
|
||||
local cx, cy = r, r
|
||||
local body = { '<circle cx="' .. n(cx) .. '" cy="' .. n(cy) .. '" r="' .. n(r - 0.5) .. '" fill="#ffffff"/>' }
|
||||
@@ -280,7 +288,7 @@ local function clockFace(dia, hour, minute)
|
||||
local r1 = r - px(major and 4 or 5)
|
||||
local r2 = r - (major and r * 0.2 or r * 0.12) - px(3)
|
||||
table.insert(body, '<line x1="' .. n(cx + r1 * math.sin(a)) .. '" y1="' .. n(cy - r1 * math.cos(a)) .. '" x2="' .. n(cx + r2 * math.sin(a))
|
||||
.. '" y2="' .. n(cy - r2 * math.cos(a)) .. '" stroke="' .. (major and "#000000" or "#555555") .. '" stroke-width="'
|
||||
.. '" y2="' .. n(cy - r2 * math.cos(a)) .. '" stroke="' .. (major and accent or "#555555") .. '" stroke-width="'
|
||||
.. n(major and math.max(2, px(2.5)) or math.max(1, px(1.25))) .. '" stroke-linecap="round"/>')
|
||||
end
|
||||
local ah = ((hour % 12) + minute / 60) * math.pi / 6
|
||||
@@ -291,13 +299,14 @@ local function clockFace(dia, hour, minute)
|
||||
end
|
||||
table.insert(body, hand(ah, r * 0.5, math.max(3, px(4.5)), r * 0.08))
|
||||
table.insert(body, hand(am, r * 0.76, math.max(2, px(2.75)), r * 0.1))
|
||||
table.insert(body, '<circle cx="' .. n(cx) .. '" cy="' .. n(cy) .. '" r="' .. n(math.max(3, px(4))) .. '" fill="#000000"/>')
|
||||
table.insert(body, '<circle cx="' .. n(cx) .. '" cy="' .. n(cy) .. '" r="' .. n(math.max(3, px(4))) .. '" fill="' .. accent .. '"/>')
|
||||
table.insert(body, '<circle cx="' .. n(cx) .. '" cy="' .. n(cy) .. '" r="' .. n(math.max(1, px(1.25))) .. '" fill="#ffffff"/>')
|
||||
return svg(round(dia), round(dia), table.concat(body))
|
||||
end
|
||||
|
||||
function Decor.analog(ctx, w, hh)
|
||||
local d = ctx.d
|
||||
local accent = BoardColour.artHex(ctx, "#000000", "ink", "red")
|
||||
local pad = px(12)
|
||||
local iw, ih = w - 2 * pad, hh - 2 * pad
|
||||
local now = d.now or os.time()
|
||||
@@ -311,12 +320,12 @@ function Decor.analog(ctx, w, hh)
|
||||
if tw < px(70) then return skip("no room for the date") end
|
||||
return padded(w, hh, {
|
||||
hstack({ w = iw, h = ih, gap = px(14) }, {
|
||||
clockFace(dia, hour, minute),
|
||||
clockFace(dia, hour, minute, accent),
|
||||
vstack({ w = tw }, {
|
||||
text(d.weekday, { s = s, b = true, w = tw, col = INK.white }),
|
||||
text(d.day_month, { s = 13, w = tw, col = INK.gc }),
|
||||
text(d.day_month, { s = 13, w = tw, col = BoardColour.onInk(ctx, INK.gc) }),
|
||||
space(nil, px(4)),
|
||||
text(d.clock, { s = 13, b = true, w = tw, col = INK.gc }),
|
||||
text(d.clock, { s = 13, b = true, w = tw, col = BoardColour.onInk(ctx, INK.gc) }),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
@@ -324,7 +333,7 @@ function Decor.analog(ctx, w, hh)
|
||||
end
|
||||
end
|
||||
table.insert(variants, function()
|
||||
return padded(w, hh, { flex(0), clockFace(math.max(1, math.min(iw, ih)), hour, minute), flex(0) }, "center")
|
||||
return padded(w, hh, { flex(0), clockFace(math.max(1, math.min(iw, ih)), hour, minute, accent), flex(0) }, "center")
|
||||
end)
|
||||
return pick(variants)
|
||||
end
|
||||
@@ -343,10 +352,10 @@ end
|
||||
-- Procellarum and Nubium, drawn only where wholly in the light.
|
||||
local MARIA = { { -0.26, -0.36, 0.24, 0.17, -0.3 }, { 0.2, -0.3, 0.13, 0.12, 0 }, { 0.32, -0.02, 0.15, 0.12, 0.4 }, { -0.5, 0.06, 0.15, 0.3, 0.1 }, { -0.08, 0.42, 0.15, 0.09, 0.2 } }
|
||||
|
||||
local function moonDisc(dia, m)
|
||||
local function moonDisc(dia, m, ctx)
|
||||
local R = dia / 2 - math.max(1, px(1))
|
||||
local cx, cy = dia / 2, dia / 2
|
||||
local body = { '<circle cx="' .. n(cx) .. '" cy="' .. n(cy) .. '" r="' .. n(R) .. '" fill="#222222" stroke="#666666" stroke-width="' .. n(math.max(1, px(1))) .. '"/>' }
|
||||
local body = { '<circle cx="' .. n(cx) .. '" cy="' .. n(cy) .. '" r="' .. n(R) .. '" fill="' .. artHex(ctx, "#222222", "#2a3062") .. '" stroke="' .. artHex(ctx, "#666666", "#5a628f") .. '" stroke-width="' .. n(math.max(1, px(1))) .. '"/>' }
|
||||
if m.lit > 0.004 then
|
||||
local k = math.cos(2 * math.pi * m.p)
|
||||
local waxing = m.p < 0.5
|
||||
@@ -356,7 +365,7 @@ local function moonDisc(dia, m)
|
||||
if waxing then term_sweep = k > 0 and 0 or 1 else term_sweep = k > 0 and 1 or 0 end
|
||||
local trx = math.max(0.01, math.abs(k) * R)
|
||||
table.insert(body, '<path d="M' .. top .. ' A' .. n(R) .. ' ' .. n(R) .. ' 0 0 ' .. limb_sweep .. ' ' .. bottom .. ' A' .. n(trx) .. ' ' .. n(R)
|
||||
.. ' 0 0 ' .. term_sweep .. ' ' .. top .. ' Z" fill="#ffffff"/>')
|
||||
.. ' 0 0 ' .. term_sweep .. ' ' .. top .. ' Z" fill="' .. artHex(ctx, "#ffffff", "#f7efcf") .. '"/>')
|
||||
for _i, mare in ipairs(MARIA) do
|
||||
local mx, my, ex, ey, rot = mare[1], mare[2], mare[3], mare[4], mare[5]
|
||||
local ok = true
|
||||
@@ -368,7 +377,7 @@ local function moonDisc(dia, m)
|
||||
end
|
||||
if ok then
|
||||
table.insert(body, '<ellipse cx="' .. n(cx + mx * R) .. '" cy="' .. n(cy + my * R) .. '" rx="' .. n(ex * R) .. '" ry="' .. n(ey * R)
|
||||
.. '" transform="rotate(' .. n(rot * 57.3) .. ' ' .. n(cx + mx * R) .. ' ' .. n(cy + my * R) .. ')" fill="#dddddd"/>')
|
||||
.. '" transform="rotate(' .. n(rot * 57.3) .. ' ' .. n(cx + mx * R) .. ' ' .. n(cy + my * R) .. ')" fill="' .. artHex(ctx, "#dddddd", "#e2d3a2") .. '"/>')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -415,13 +424,13 @@ function Decor.moon(ctx, w, hh)
|
||||
if tw < px(80) then return skip("no room for words") end
|
||||
local words = vstack({ w = tw }, {
|
||||
text(m.name, { s = v[1], b = true, w = tw, col = INK.white }),
|
||||
text(T(_("%1% lit"), lit), { s = 13, w = tw, col = INK.gc }),
|
||||
text(T(_("%1% lit"), lit), { s = 13, w = tw, col = BoardColour.onInk(ctx, INK.gc, "moon") }),
|
||||
space(nil, px(4)),
|
||||
text(m.next, { s = 12, w = tw, col = INK.gc }),
|
||||
text(m.next, { s = 12, w = tw, col = BoardColour.onInk(ctx, INK.gc, "moon") }),
|
||||
})
|
||||
return overlay(w, hh, {
|
||||
{ widget = sky(w, hh, { x = pad + dia / 2, y = hh / 2, r = dia / 2 }, { x = pad + dia, y = 0, w = w, h = hh }) },
|
||||
{ widget = moonDisc(dia, m), x = pad, y = round((hh - dia) / 2) },
|
||||
{ widget = moonDisc(dia, m, ctx), x = pad, y = round((hh - dia) / 2) },
|
||||
{ widget = vstack({ w = tw, h = hh }, { flex(0), words, flex(0) }), x = pad + dia + px(16), y = 0 },
|
||||
})
|
||||
end)
|
||||
@@ -436,7 +445,7 @@ function Decor.moon(ctx, w, hh)
|
||||
local top = pad + (ih - cap_h - dia) / 2
|
||||
local layers = {
|
||||
{ widget = sky(w, hh, { x = w / 2, y = top + dia / 2, r = dia / 2 }, caption and { x = 0, y = hh - pad - lh1(12), w = w, h = lh1(12) } or nil) },
|
||||
{ widget = moonDisc(dia, m), x = round((w - dia) / 2), y = round(top) },
|
||||
{ widget = moonDisc(dia, m, ctx), x = round((w - dia) / 2), y = round(top) },
|
||||
}
|
||||
if caption then
|
||||
table.insert(layers, { widget = text(m.name, { s = 12, b = true, w = iw, col = INK.white, align = "center" }), x = pad, y = hh - pad - lh1(12) })
|
||||
@@ -450,7 +459,7 @@ end
|
||||
-- ================================================================ odometer
|
||||
-- The last wheel, caught mid-turn: the bottom of its digit and the top of
|
||||
-- the next, both drawn into the wheel's own box so neither spills out.
|
||||
local RollingWheel = Widget:extend{ digit = 0, size = 10, roll = 0, pitch = 0 }
|
||||
local RollingWheel = Widget:extend{ digit = 0, size = 10, roll = 0, pitch = 0, color = nil }
|
||||
|
||||
function RollingWheel:getSize()
|
||||
return self.dimen
|
||||
@@ -467,7 +476,7 @@ function RollingWheel:paintTo(bb, x, y)
|
||||
local gy = y + round(center_y - gh / 2)
|
||||
local top, bottom = math.max(gy, y), math.min(gy + gh, y + h)
|
||||
if bottom <= top then return end
|
||||
bb:colorblitFrom(g.bb, gx, top, 0, top - gy, gw, bottom - top, INK.black)
|
||||
bb:colorblitFrom(g.bb, gx, top, 0, top - gy, gw, bottom - top, self.color or INK.black)
|
||||
end
|
||||
draw(self.digit, h / 2 - self.roll)
|
||||
if self.roll > 0 then draw((self.digit + 1) % 10, h / 2 - self.roll + self.pitch) end
|
||||
@@ -482,14 +491,15 @@ function Decor.odometer(ctx, w, hh)
|
||||
local digits = string.format("%06d", total)
|
||||
local count = #digits
|
||||
local g = px(3)
|
||||
local hot = BoardColour.on(ctx)
|
||||
local function drums(dw, dh)
|
||||
local fs = math.floor(dh * 0.62 / BoardMetrics.get().dp)
|
||||
local items = {}
|
||||
for i = 1, count do
|
||||
local ch = digits:sub(i, i)
|
||||
local shade_col = i < count and INK.g3 or INK.gd
|
||||
local shade_col = i < count and INK.g3 or (hot and BoardColour.paint(BoardColour.mix(BoardColour.fam(ctx).ink, "#ffffff", 0.3)) or INK.gd)
|
||||
local layers = {
|
||||
{ widget = BoardUI.box({ w = dw, h = dh, radius = px(3), bg = i < count and INK.black or INK.white }) },
|
||||
{ widget = BoardUI.box({ w = dw, h = dh, radius = px(3), bg = i < count and INK.black or BoardColour.ink(ctx, INK.white) }) },
|
||||
{ widget = BoardUI.rect(dw, math.max(1, px(1)), shade_col), y = round(dh * 0.16) },
|
||||
{ widget = BoardUI.rect(dw, math.max(1, px(1)), shade_col), y = round(dh * 0.84) },
|
||||
}
|
||||
@@ -499,8 +509,10 @@ function Decor.odometer(ctx, w, hh)
|
||||
local pitch = dh * 0.8
|
||||
-- Nothing read today: the wheel rests on its digit.
|
||||
local roll = today > 0 and round(pitch * 0.3) or 0
|
||||
table.insert(layers, { widget = RollingWheel:new{ digit = tonumber(ch), size = BoardMetrics.fs(fs), roll = roll, pitch = pitch, dimen = Geom:new{ w = dw, h = dh } } })
|
||||
table.insert(layers, { widget = BoardUI.box({ w = dw, h = dh, radius = px(3), border = math.max(1, px(1.5)), border_col = INK.black }) })
|
||||
table.insert(layers, { widget = RollingWheel:new{ digit = tonumber(ch), size = BoardMetrics.fs(fs), roll = roll, pitch = pitch, color = hot and INK.white or INK.black, dimen = Geom:new{ w = dw, h = dh } } })
|
||||
if not hot then
|
||||
table.insert(layers, { widget = BoardUI.box({ w = dw, h = dh, radius = px(3), border = math.max(1, px(1.5)), border_col = INK.black }) })
|
||||
end
|
||||
end
|
||||
table.insert(items, overlay(dw, dh, layers))
|
||||
end
|
||||
@@ -531,7 +543,7 @@ function Decor.odometer(ctx, w, hh)
|
||||
local dw_fit = math.min(dw, round(dh / 1.32))
|
||||
local right = nil
|
||||
if mode == "beside" then right = todayRuns(12)
|
||||
elseif mode ~= "bare" then right = icon("book-open", px(17), { sw = 2.25 }) end
|
||||
elseif mode ~= "bare" then right = icon("book-open", px(17), BoardColour.icon(ctx, 2.25)) end
|
||||
return padded(w, hh, {
|
||||
head(iw, _("Pages turned"), right),
|
||||
flex(0),
|
||||
@@ -545,8 +557,10 @@ function Decor.odometer(ctx, w, hh)
|
||||
end
|
||||
|
||||
-- ================================================================ glyph
|
||||
local function glyphArt(g, max_w, max_h)
|
||||
return BoardArt.glyph(g.font, g.cp, math.max(1, max_w), math.max(1, max_h))
|
||||
local function glyphArt(g, max_w, max_h, col)
|
||||
local art = BoardArt.glyph(g.font, g.cp, math.max(1, max_w), math.max(1, max_h))
|
||||
if art then art.color = col end
|
||||
return art
|
||||
end
|
||||
|
||||
-- A line of the alphabet in the glyph's own font.
|
||||
@@ -573,7 +587,7 @@ function Decor.glyph(ctx, w, hh)
|
||||
for _i, s in ipairs({ 15, 14 }) do
|
||||
table.insert(variants, function()
|
||||
local gw_max = math.min(ih, round(iw * 0.4))
|
||||
local art = glyphArt(g, gw_max, ih - px(4))
|
||||
local art = glyphArt(g, gw_max, ih - px(4), BoardColour.ink(ctx, INK.black))
|
||||
if not art then return skip("no glyph") end
|
||||
local tw = iw - gw_max - px(14)
|
||||
if tw < px(90) then return skip("no room for words") end
|
||||
@@ -594,7 +608,7 @@ function Decor.glyph(ctx, w, hh)
|
||||
if ih > px(150) then
|
||||
table.insert(variants, function()
|
||||
local foot = lh1(15) + lh1(12) + (g.sample and lh1(14) + px(8) or 0) + px(10)
|
||||
local art = glyphArt(g, iw - px(20), ih - foot - px(10))
|
||||
local art = glyphArt(g, iw - px(20), ih - foot - px(10), BoardColour.ink(ctx, INK.black))
|
||||
if not art then return skip("no glyph") end
|
||||
return padded(w, hh, {
|
||||
flex(0), art, flex(0),
|
||||
@@ -608,7 +622,7 @@ function Decor.glyph(ctx, w, hh)
|
||||
for _i, two in ipairs({ true, false }) do
|
||||
table.insert(variants, function()
|
||||
local foot = lh1(12) + (two and lh1(11) or 0) + px(6)
|
||||
local art = glyphArt(g, iw, ih - foot)
|
||||
local art = glyphArt(g, iw, ih - foot, BoardColour.ink(ctx, INK.black))
|
||||
if not art then return skip("no glyph") end
|
||||
return padded(w, hh, {
|
||||
flex(0), art, flex(0),
|
||||
@@ -618,7 +632,7 @@ function Decor.glyph(ctx, w, hh)
|
||||
end)
|
||||
end
|
||||
table.insert(variants, function()
|
||||
local art = glyphArt(g, iw, ih) or text(name, { s = 14, b = true, w = iw, align = "center" })
|
||||
local art = glyphArt(g, iw, ih, BoardColour.ink(ctx, INK.black)) or text(name, { s = 14, b = true, w = iw, align = "center" })
|
||||
return padded(w, hh, { flex(0), art, flex(0) }, "center")
|
||||
end)
|
||||
return pick(variants)
|
||||
@@ -628,7 +642,16 @@ end
|
||||
-- One ring a month, the oldest at the pith; a ring is as wide as that
|
||||
-- month's reading. The pith sits a little off centre and every ring wobbles
|
||||
-- a little, as in real wood.
|
||||
local function ringsArt(dia, months, seed)
|
||||
-- Bark, rings and heartwood: wood colours on a colour screen, grays otherwise.
|
||||
local function woodColours(ctx)
|
||||
return {
|
||||
bark = artHex(ctx, "#444444", "#6b4426"), edge = artHex(ctx, "#000000", "#3b2512"), ring = artHex(ctx, "#ffffff", "#f4e4c6"),
|
||||
peak = artHex(ctx, "#dddddd", "#e2b56a"), line = artHex(ctx, "#666666", "#a37a52"), pith = artHex(ctx, "#555555", "#7a4f2c"),
|
||||
crack = artHex(ctx, "#222222", "#3b2512"),
|
||||
}
|
||||
end
|
||||
|
||||
local function ringsArt(dia, months, seed, wood)
|
||||
local r = rng(seed)
|
||||
local bark = math.max(3, px(5))
|
||||
-- Room for the wobble and the off-centre pith, so the bark never clips.
|
||||
@@ -666,26 +689,26 @@ local function ringsArt(dia, months, seed)
|
||||
for i, m in ipairs(months) do
|
||||
if m.min > months[peak].min then peak = i end
|
||||
end
|
||||
local body = { '<path d="' .. loop(R, bark) .. '" fill="#444444" stroke="#000000" stroke-width="' .. n(math.max(1, px(1.25))) .. '"/>' }
|
||||
local body = { '<path d="' .. loop(R, bark) .. '" fill="' .. wood.bark .. '" stroke="' .. wood.edge .. '" stroke-width="' .. n(math.max(1, px(1.25))) .. '"/>' }
|
||||
for i = #months, 1, -1 do
|
||||
table.insert(body, '<path d="' .. loop(bounds[i]) .. '" fill="' .. (i == peak and "#dddddd" or "#ffffff") .. '" stroke="#666666" stroke-width="'
|
||||
table.insert(body, '<path d="' .. loop(bounds[i]) .. '" fill="' .. (i == peak and wood.peak or wood.ring) .. '" stroke="' .. wood.line .. '" stroke-width="'
|
||||
.. n(math.max(1, px(i == #months and 1.25 or 1))) .. '"/>')
|
||||
end
|
||||
table.insert(body, '<path d="' .. loop(pith_r * 0.9) .. '" fill="#555555"/>')
|
||||
table.insert(body, '<path d="' .. loop(pith_r * 0.9) .. '" fill="' .. wood.pith .. '"/>')
|
||||
-- A drying check: a thin crack from the bark towards the heart.
|
||||
local ca = -0.62
|
||||
local x1, y1 = cx + (R + bark) * math.cos(ca), cy + (R + bark) * math.sin(ca)
|
||||
local x2, y2 = cx + R * 0.45 * math.cos(ca + 0.05), cy + R * 0.45 * math.sin(ca + 0.05)
|
||||
local nx, ny = -math.sin(ca) * px(1.6), math.cos(ca) * px(1.6)
|
||||
table.insert(body, '<path d="M' .. n(x1 - nx) .. ' ' .. n(y1 - ny) .. ' L' .. n(x2) .. ' ' .. n(y2) .. ' L' .. n(x1 + nx) .. ' ' .. n(y1 + ny) .. ' Z" fill="#222222"/>')
|
||||
table.insert(body, '<path d="M' .. n(x1 - nx) .. ' ' .. n(y1 - ny) .. ' L' .. n(x2) .. ' ' .. n(y2) .. ' L' .. n(x1 + nx) .. ' ' .. n(y1 + ny) .. ' Z" fill="' .. wood.crack .. '"/>')
|
||||
return svg(round(dia), round(dia), table.concat(body))
|
||||
end
|
||||
|
||||
local function sapling(dia)
|
||||
local function sapling(dia, wood)
|
||||
local h = dia / 2
|
||||
return svg(dia, dia, '<circle cx="' .. n(h) .. '" cy="' .. n(h) .. '" r="' .. n(h - 1) .. '" fill="#444444" stroke="#000000" stroke-width="' .. n(math.max(1, px(1)))
|
||||
.. '"/><circle cx="' .. n(h) .. '" cy="' .. n(h) .. '" r="' .. n(h - math.max(3, px(4))) .. '" fill="#ffffff" stroke="#666666" stroke-width="' .. n(math.max(1, px(1)))
|
||||
.. '"/><circle cx="' .. n(h) .. '" cy="' .. n(h) .. '" r="' .. n(math.max(2, dia * 0.08)) .. '" fill="#555555"/>')
|
||||
return svg(dia, dia, '<circle cx="' .. n(h) .. '" cy="' .. n(h) .. '" r="' .. n(h - 1) .. '" fill="' .. wood.bark .. '" stroke="' .. wood.edge .. '" stroke-width="' .. n(math.max(1, px(1)))
|
||||
.. '"/><circle cx="' .. n(h) .. '" cy="' .. n(h) .. '" r="' .. n(h - math.max(3, px(4))) .. '" fill="' .. wood.ring .. '" stroke="' .. wood.line .. '" stroke-width="' .. n(math.max(1, px(1)))
|
||||
.. '"/><circle cx="' .. n(h) .. '" cy="' .. n(h) .. '" r="' .. n(math.max(2, dia * 0.08)) .. '" fill="' .. wood.pith .. '"/>')
|
||||
end
|
||||
|
||||
function Decor.rings(ctx, w, hh)
|
||||
@@ -708,7 +731,7 @@ function Decor.rings(ctx, w, hh)
|
||||
space(nil, px(6)),
|
||||
text(_("A ring grows for every month you read."), { s = 13, col = INK.g4, lines = 3, w = iw }),
|
||||
with_art and flex(0) or false,
|
||||
with_art and hstack({ w = iw }, { flex(0), sapling(dia), flex(0) }) or false,
|
||||
with_art and hstack({ w = iw }, { flex(0), sapling(dia, woodColours(ctx)), flex(0) }) or false,
|
||||
with_art and flex(0) or false,
|
||||
})
|
||||
end)
|
||||
@@ -737,7 +760,7 @@ function Decor.rings(ctx, w, hh)
|
||||
if tw < px(100) then return skip("no room for words") end
|
||||
return padded(w, hh, {
|
||||
hstack({ w = iw, h = ih, gap = px(14) }, {
|
||||
ringsArt(dia, months, 3),
|
||||
ringsArt(dia, months, 3, woodColours(ctx)),
|
||||
vstack({ w = tw }, {
|
||||
text(_("Tree rings"), { s = v[1], b = true, w = tw }),
|
||||
v[2] and text(_("One ring a month"), { s = 12, col = INK.g4, w = tw }) or false,
|
||||
@@ -759,7 +782,7 @@ function Decor.rings(ctx, w, hh)
|
||||
return padded(w, hh, {
|
||||
head(iw, _("Tree rings"), text(_("12 months"), { s = 12, col = INK.g4 })),
|
||||
flex(0),
|
||||
ringsArt(dia, months, 3),
|
||||
ringsArt(dia, months, 3, woodColours(ctx)),
|
||||
flex(0),
|
||||
caption and labelRuns(_("Widest ring"), peak, true, 12, iw, "center") or false,
|
||||
}, "center")
|
||||
@@ -800,16 +823,16 @@ local function lineArt(w, cs, o)
|
||||
local track_y = px(10)
|
||||
local art_h = px(20)
|
||||
local body = {
|
||||
'<line x1="' .. n(train_x) .. '" y1="' .. track_y .. '" x2="' .. n(x1) .. '" y2="' .. track_y .. '" stroke="#bbbbbb" stroke-width="' .. lw .. '" stroke-linecap="round"/>',
|
||||
'<line x1="' .. n(x0) .. '" y1="' .. track_y .. '" x2="' .. n(train_x) .. '" y2="' .. track_y .. '" stroke="#000000" stroke-width="' .. lw .. '" stroke-linecap="round"/>',
|
||||
'<line x1="' .. n(train_x) .. '" y1="' .. track_y .. '" x2="' .. n(x1) .. '" y2="' .. track_y .. '" stroke="' .. (o.ahead or "#bbbbbb") .. '" stroke-width="' .. lw .. '" stroke-linecap="round"/>',
|
||||
'<line x1="' .. n(x0) .. '" y1="' .. track_y .. '" x2="' .. n(train_x) .. '" y2="' .. track_y .. '" stroke="' .. (o.line or "#000000") .. '" stroke-width="' .. lw .. '" stroke-linecap="round"/>',
|
||||
}
|
||||
for i, stop in ipairs(cs.stops) do
|
||||
local past = stop.pct <= cs.pos
|
||||
local last = i == count
|
||||
local rr = math.min(last and r * 1.25 or r, (x1 - x0) / (count - 1) * 0.36)
|
||||
table.insert(body, '<circle cx="' .. n(xi(i)) .. '" cy="' .. track_y .. '" r="' .. n(rr) .. '" fill="' .. (past and "#000000" or "#ffffff")
|
||||
.. '" stroke="#000000" stroke-width="' .. n(math.max(1, px(1.5))) .. '"/>')
|
||||
if last then table.insert(body, '<circle cx="' .. n(xi(i)) .. '" cy="' .. track_y .. '" r="' .. n(rr * 0.42) .. '" fill="#000000"/>') end
|
||||
table.insert(body, '<circle cx="' .. n(xi(i)) .. '" cy="' .. track_y .. '" r="' .. n(rr) .. '" fill="' .. (past and (o.line or "#000000") or "#ffffff")
|
||||
.. '" stroke="' .. (o.line or "#000000") .. '" stroke-width="' .. n(math.max(1, px(1.5))) .. '"/>')
|
||||
if last then table.insert(body, '<circle cx="' .. n(xi(i)) .. '" cy="' .. track_y .. '" r="' .. n(rr * 0.42) .. '" fill="' .. (o.line or "#000000") .. '"/>') end
|
||||
end
|
||||
table.insert(body, tram(train_x, track_y, 1))
|
||||
local layers = { { widget = svg(w, art_h, table.concat(body)) } }
|
||||
@@ -843,7 +866,7 @@ end
|
||||
-- The next stations as a timetable down a vertical line, with how long
|
||||
-- until each. The stretch the train is on is taller, so the car stands
|
||||
-- clear of both its stations. Nil when fewer than three rows fit.
|
||||
local function timetable(w, avail, cs)
|
||||
local function timetable(w, avail, cs, line_col, ahead_col)
|
||||
local row_h = lh1(14) + px(6)
|
||||
local stretch = 0.7
|
||||
local rows = math.floor(avail / row_h - stretch)
|
||||
@@ -864,22 +887,22 @@ local function timetable(w, avail, cs)
|
||||
local clear = r + t_h / 2 + px(1)
|
||||
local seg = cy(1) - cy(0)
|
||||
local ty = clamp(cy(0) + seg * trainFraction(cs), cy(0) + clear, cy(1) - clear)
|
||||
local body = { '<line x1="' .. lx .. '" y1="' .. n(cy(0)) .. '" x2="' .. lx .. '" y2="' .. n(ty) .. '" stroke="#000000" stroke-width="' .. lw .. '"/>' }
|
||||
local body = { '<line x1="' .. lx .. '" y1="' .. n(cy(0)) .. '" x2="' .. lx .. '" y2="' .. n(ty) .. '" stroke="' .. line_col .. '" stroke-width="' .. lw .. '"/>' }
|
||||
local last_y = cy(#list - 1)
|
||||
if show_end then
|
||||
local gap_y = cy(#list - 2)
|
||||
table.insert(body, '<line x1="' .. lx .. '" y1="' .. n(ty) .. '" x2="' .. lx .. '" y2="' .. n(gap_y) .. '" stroke="#bbbbbb" stroke-width="' .. lw .. '"/>')
|
||||
table.insert(body, '<line x1="' .. lx .. '" y1="' .. n(gap_y) .. '" x2="' .. lx .. '" y2="' .. n(last_y) .. '" stroke="#bbbbbb" stroke-width="' .. lw
|
||||
table.insert(body, '<line x1="' .. lx .. '" y1="' .. n(ty) .. '" x2="' .. lx .. '" y2="' .. n(gap_y) .. '" stroke="' .. ahead_col .. '" stroke-width="' .. lw .. '"/>')
|
||||
table.insert(body, '<line x1="' .. lx .. '" y1="' .. n(gap_y) .. '" x2="' .. lx .. '" y2="' .. n(last_y) .. '" stroke="' .. ahead_col .. '" stroke-width="' .. lw
|
||||
.. '" stroke-dasharray="' .. px(2) .. ' ' .. px(4) .. '"/>')
|
||||
else
|
||||
table.insert(body, '<line x1="' .. lx .. '" y1="' .. n(ty) .. '" x2="' .. lx .. '" y2="' .. n(last_y) .. '" stroke="#bbbbbb" stroke-width="' .. lw .. '"/>')
|
||||
table.insert(body, '<line x1="' .. lx .. '" y1="' .. n(ty) .. '" x2="' .. lx .. '" y2="' .. n(last_y) .. '" stroke="' .. ahead_col .. '" stroke-width="' .. lw .. '"/>')
|
||||
end
|
||||
for k, i in ipairs(list) do
|
||||
local last = i == last_i
|
||||
local rr = last and r * 1.2 or r
|
||||
table.insert(body, '<circle cx="' .. lx .. '" cy="' .. n(cy(k - 1)) .. '" r="' .. n(rr) .. '" fill="' .. (k == 1 and "#000000" or "#ffffff")
|
||||
.. '" stroke="#000000" stroke-width="' .. n(math.max(1, px(1.5))) .. '"/>')
|
||||
if last then table.insert(body, '<circle cx="' .. lx .. '" cy="' .. n(cy(k - 1)) .. '" r="' .. n(rr * 0.42) .. '" fill="#000000"/>') end
|
||||
table.insert(body, '<circle cx="' .. lx .. '" cy="' .. n(cy(k - 1)) .. '" r="' .. n(rr) .. '" fill="' .. (k == 1 and line_col or "#ffffff")
|
||||
.. '" stroke="' .. line_col .. '" stroke-width="' .. n(math.max(1, px(1.5))) .. '"/>')
|
||||
if last then table.insert(body, '<circle cx="' .. lx .. '" cy="' .. n(cy(k - 1)) .. '" r="' .. n(rr * 0.42) .. '" fill="' .. line_col .. '"/>') end
|
||||
end
|
||||
local car_w, win = px(11), px(3.2)
|
||||
table.insert(body, '<rect x="' .. n(lx - car_w / 2) .. '" y="' .. n(ty - t_h / 2) .. '" width="' .. n(car_w) .. '" height="' .. n(t_h) .. '" rx="' .. n(px(3))
|
||||
@@ -902,10 +925,11 @@ local function timetable(w, avail, cs)
|
||||
end
|
||||
|
||||
function Decor.chapters(ctx, w, hh)
|
||||
local line, ahead = BoardColour.artHex(ctx, "#000000", "ink"), BoardColour.artHex(ctx, "#bbbbbb", "tint")
|
||||
local cs = ctx.d.chapters
|
||||
local pad = px(12)
|
||||
local iw, ih = w - 2 * pad, hh - 2 * pad
|
||||
local function train() return icon("train-front", px(17), { sw = 2.25 }) end
|
||||
local function train() return icon("train-front", px(17), BoardColour.icon(ctx, 2.25)) end
|
||||
if not cs then
|
||||
return emptyTile(w, hh, _("Next stop"), train, _("Open a book and its chapters line up here."), _("Open a book to start."))
|
||||
end
|
||||
@@ -920,7 +944,7 @@ function Decor.chapters(ctx, w, hh)
|
||||
if ih > px(150) and iw < ih * 1.9 then
|
||||
table.insert(variants, function()
|
||||
local foot = lh1(12) + px(10)
|
||||
local tt = timetable(iw, ih - lh1(13) - px(12) - foot, cs)
|
||||
local tt = timetable(iw, ih - lh1(13) - px(12) - foot, cs, line, ahead)
|
||||
if not tt then return skip("timetable needs three rows") end
|
||||
return padded(w, hh, {
|
||||
head(iw, cs.book.title or "", text(BoardData.pctLabel(cs.book) or "", { s = 12, col = INK.g4 })),
|
||||
@@ -935,7 +959,7 @@ function Decor.chapters(ctx, w, hh)
|
||||
for _i, v in ipairs({ { 16, true, true }, { 16, false, true }, { 15, false, false }, { 14, false, false } }) do
|
||||
table.insert(variants, function()
|
||||
if iw < px(220) then return skip("too narrow for the line") end
|
||||
local art = lineArt(iw, cs, { labels = v[2], max = iw > px(500) and 6 or 3 })
|
||||
local art = lineArt(iw, cs, { labels = v[2], max = iw > px(500) and 6 or 3, line = line, ahead = ahead })
|
||||
local foot = false
|
||||
if v[3] then
|
||||
local left = endRuns(12)
|
||||
@@ -961,7 +985,7 @@ function Decor.chapters(ctx, w, hh)
|
||||
text(when, { s = 20, b = true, w = iw }),
|
||||
with_name and text(cs.stops[cs.next].title, { s = 12, b = true, serif = true, w = iw, data = true }) or false,
|
||||
flex(0),
|
||||
lineArt(iw, cs, {}),
|
||||
lineArt(iw, cs, { line = line, ahead = ahead }),
|
||||
})
|
||||
end)
|
||||
end
|
||||
@@ -981,7 +1005,8 @@ local RARITY = {
|
||||
|
||||
-- The medal: rarity shows in the rim (plain, double, beaded, sunburst). A
|
||||
-- badge in progress wears a progress ring; an earned one hangs on ribbons.
|
||||
local function medal(D0, b, earned)
|
||||
-- rf: the rarity's hex shades on a colour screen, nil for grays.
|
||||
local function medal(D0, b, earned, rf)
|
||||
local cx = D0 / 2
|
||||
local ring_w = earned and 0 or math.max(3, px(5))
|
||||
local gap = earned and 0 or px(4)
|
||||
@@ -995,7 +1020,7 @@ local function medal(D0, b, earned)
|
||||
local y0, y1 = cy + R * 0.3, cy + R + ribbon_h
|
||||
local wr = R * 0.34
|
||||
return '<path d="M' .. n(x0 - wr / 2) .. ' ' .. n(y0) .. ' L' .. n(x0 + wr / 2) .. ' ' .. n(y0) .. ' L' .. n(x1 + wr / 2) .. ' ' .. n(y1)
|
||||
.. ' L' .. n(x1) .. ' ' .. n(y1 - wr * 0.45) .. ' L' .. n(x1 - wr / 2) .. ' ' .. n(y1) .. ' Z" fill="#555555" stroke="#000000" stroke-width="'
|
||||
.. ' L' .. n(x1) .. ' ' .. n(y1 - wr * 0.45) .. ' L' .. n(x1 - wr / 2) .. ' ' .. n(y1) .. ' Z" fill="' .. (rf and rf.ribbon or "#555555") .. '" stroke="#000000" stroke-width="'
|
||||
.. n(math.max(1, px(1.25))) .. '" stroke-linejoin="round"/>'
|
||||
end
|
||||
table.insert(body, tail(-1))
|
||||
@@ -1003,14 +1028,14 @@ local function medal(D0, b, earned)
|
||||
else
|
||||
local pr = R + gap + ring_w / 2
|
||||
local p = (b.threshold and b.threshold > 0 and b.progress) and clamp(b.progress / b.threshold, 0, 0.9999) or 0
|
||||
table.insert(body, '<circle cx="' .. n(cx) .. '" cy="' .. n(cy) .. '" r="' .. n(pr) .. '" fill="none" stroke="#dddddd" stroke-width="' .. ring_w .. '"/>')
|
||||
table.insert(body, '<circle cx="' .. n(cx) .. '" cy="' .. n(cy) .. '" r="' .. n(pr) .. '" fill="none" stroke="' .. (rf and rf.tint or "#dddddd") .. '" stroke-width="' .. ring_w .. '"/>')
|
||||
if p > 0.005 then
|
||||
local a = p * 2 * math.pi
|
||||
table.insert(body, '<path d="M' .. n(cx) .. ' ' .. n(cy - pr) .. ' A' .. n(pr) .. ' ' .. n(pr) .. ' 0 ' .. (p > 0.5 and 1 or 0) .. ' 1 '
|
||||
.. n(cx + pr * math.sin(a)) .. ' ' .. n(cy - pr * math.cos(a)) .. '" fill="none" stroke="#000000" stroke-width="' .. ring_w .. '" stroke-linecap="round"/>')
|
||||
.. n(cx + pr * math.sin(a)) .. ' ' .. n(cy - pr * math.cos(a)) .. '" fill="none" stroke="' .. (rf and rf.ink or "#000000") .. '" stroke-width="' .. ring_w .. '" stroke-linecap="round"/>')
|
||||
end
|
||||
end
|
||||
local face, ink = earned and "#000000" or "#ffffff", earned and "#ffffff" or "#000000"
|
||||
local face, ink = earned and (rf and rf.deep or "#000000") or "#ffffff", earned and "#ffffff" or "#000000"
|
||||
local sw = math.max(1, px(1.5))
|
||||
if b.rarity == "legendary" then
|
||||
local pts = {}
|
||||
@@ -1018,7 +1043,7 @@ local function medal(D0, b, earned)
|
||||
local a, rr = i * math.pi / 16, (i % 2 == 1) and R * 0.9 or R * 1.06
|
||||
table.insert(pts, (i == 0 and "M" or "L") .. n(cx + rr * math.sin(a)) .. " " .. n(cy - rr * math.cos(a)))
|
||||
end
|
||||
table.insert(body, '<path d="' .. table.concat(pts) .. 'Z" fill="#000000"/>')
|
||||
table.insert(body, '<path d="' .. table.concat(pts) .. 'Z" fill="' .. (rf and rf.ink or "#000000") .. '"/>')
|
||||
table.insert(body, '<circle cx="' .. n(cx) .. '" cy="' .. n(cy) .. '" r="' .. n(R * 0.84) .. '" fill="' .. face .. '" stroke="' .. (earned and "#ffffff" or "#000000")
|
||||
.. '" stroke-width="' .. n(sw) .. '"/>')
|
||||
else
|
||||
@@ -1043,9 +1068,15 @@ end
|
||||
|
||||
function Decor.badge(ctx, w, hh)
|
||||
local data = ctx.d.badges or { missing = true }
|
||||
-- A badge's rarity colours, the same in every scheme.
|
||||
local function rarity(r)
|
||||
if not BoardColour.on(ctx) then return nil end
|
||||
local f = BoardColour.rarity(r)
|
||||
return { ink = BoardColour.hex(f.ink), deep = BoardColour.hex(f.deep), tint = BoardColour.hex(f.tint), ribbon = BoardColour.hex(BoardColour.rarity("ribbon").ink) }
|
||||
end
|
||||
local pad = px(12)
|
||||
local iw, ih = w - 2 * pad, hh - 2 * pad
|
||||
local function award() return icon("award", px(17), { sw = 2.25 }) end
|
||||
local function award() return icon("award", px(17), BoardColour.icon(ctx, 2.25)) end
|
||||
if data.missing then
|
||||
return emptyTile(w, hh, _("Next badge"), award, _("Badges show up here with a newer BookOrbit server."), _("Needs a newer server."))
|
||||
end
|
||||
@@ -1080,7 +1111,7 @@ function Decor.badge(ctx, w, hh)
|
||||
head(iw, label, text(T(_("%1 of %2"), data.earned, data.total), { s = 12, col = INK.g4 })),
|
||||
space(nil, px(12)),
|
||||
hstack({ w = iw, h = room, gap = px(14) }, {
|
||||
medal(dia, b, false),
|
||||
medal(dia, b, false, rarity(b.rarity)),
|
||||
vstack({ w = tw }, {
|
||||
v[2] and text((RARITY[b.rarity] or RARITY.common)(), { s = 12, col = INK.g4, w = tw }) or false,
|
||||
text(b.name, { s = 16, b = true, w = tw, lines = 2, data = true }),
|
||||
@@ -1097,7 +1128,7 @@ function Decor.badge(ctx, w, hh)
|
||||
table.insert(items, BoardUI.rect(iw, math.max(1, px(0.75)), INK.gc))
|
||||
table.insert(items, space(nil, px(10)))
|
||||
table.insert(items, hstack({ w = iw, h = px(36), gap = px(10) }, {
|
||||
medal(px(36), latest, true),
|
||||
medal(px(36), latest, true, rarity(latest.rarity)),
|
||||
vstack({ w = iw - px(46) }, {
|
||||
text(latest.name, { s = 13, b = true, w = iw - px(46), data = true }),
|
||||
text(latest.when or "", { s = 12, col = INK.g4, w = iw - px(46) }),
|
||||
@@ -1122,7 +1153,7 @@ function Decor.badge(ctx, w, hh)
|
||||
local name_s = BoardUI.fitSize(b.name, { s = v[1], b = true }, tw, 12)
|
||||
return padded(w, hh, {
|
||||
hstack({ w = iw, h = ih, gap = px(14) }, {
|
||||
medal(dia, b, earned),
|
||||
medal(dia, b, earned, rarity(b.rarity)),
|
||||
vstack({ w = tw }, {
|
||||
v[4] and text(label, { s = 12, col = INK.g4, w = tw }) or false,
|
||||
text(b.name, { s = name_s, b = true, w = tw, data = true }),
|
||||
@@ -1145,7 +1176,7 @@ function Decor.badge(ctx, w, hh)
|
||||
local name_s = BoardUI.fitSize(b.name, { s = 12, b = true }, iw, 10)
|
||||
return padded(w, hh, {
|
||||
flex(0),
|
||||
medal(dia, b, earned),
|
||||
medal(dia, b, earned, rarity(b.rarity)),
|
||||
with_name and space(nil, px(6)) or false,
|
||||
with_name and text(b.name, { s = name_s, b = true, w = iw, align = "center", data = true }) or false,
|
||||
with_name and hstack({ w = iw }, { flex(0), progressRuns(12), flex(0) }) or false,
|
||||
@@ -1214,12 +1245,12 @@ local function dustArt(cw, ch, seed)
|
||||
return svg(round(cw), round(ch), table.concat(body))
|
||||
end
|
||||
|
||||
local function dustyCover(book, cw, ch)
|
||||
local function dustyCover(book, cw, ch, edge)
|
||||
local cov = BoardUI.cover(book, cw, ch, {
|
||||
path = book.cover_path,
|
||||
loading = book.cover_state == "loading",
|
||||
border = math.max(1, px(1)),
|
||||
border_col = INK.gb,
|
||||
border_col = edge or INK.gb,
|
||||
-- A film of dust: the cover a shade paler.
|
||||
lighten = 0.22,
|
||||
})
|
||||
@@ -1277,7 +1308,7 @@ function Decor.dust(ctx, w, hh)
|
||||
head(iw, _("Gathering dust"), text(T(_.ngettext("%1 book", "%1 books", dd.count), dd.count), { s = 12, col = INK.g4 })),
|
||||
space(nil, px(10)),
|
||||
hstack({ w = iw, gap = px(14), valign = "top" }, {
|
||||
dustyCover(book, cw, ch),
|
||||
dustyCover(book, cw, ch, BoardColour.coverEdge(ctx, INK.gb)),
|
||||
vstack({ w = tw }, {
|
||||
text(book.title or "", { s = 16, b = true, serif = true, lines = 3, w = tw, data = true }),
|
||||
text(book.authors and book.authors[1] or "", { s = 13, col = INK.g4, w = tw, data = true }),
|
||||
@@ -1304,7 +1335,7 @@ function Decor.dust(ctx, w, hh)
|
||||
if tw < px(150) then return skip("no room for words") end
|
||||
return padded(w, hh, {
|
||||
hstack({ w = iw, h = ih, gap = px(12) }, {
|
||||
dustyCover(book, cw, ch),
|
||||
dustyCover(book, cw, ch, BoardColour.coverEdge(ctx, INK.gb)),
|
||||
vstack({ w = tw, h = ih }, {
|
||||
v[4] and text(_("Gathering dust"), { s = 12, col = INK.g4, w = tw }) or false,
|
||||
text(book.title or "", { s = 14, b = true, serif = true, lines = v[1], w = tw, data = true }),
|
||||
@@ -1325,7 +1356,7 @@ function Decor.dust(ctx, w, hh)
|
||||
if ch < px(50) and with_caption then return skip("cover too small") end
|
||||
ch, cw = math.max(1, ch), math.max(1, cw)
|
||||
return padded(w, hh, {
|
||||
dustyCover(book, cw, ch),
|
||||
dustyCover(book, cw, ch, BoardColour.coverEdge(ctx, INK.gb)),
|
||||
with_caption and space(nil, px(6)) or false,
|
||||
with_caption and text(caption, { s = 12, b = true, w = iw, align = "center" }) or false,
|
||||
}, "center")
|
||||
|
||||
@@ -456,6 +456,12 @@ function BoardEdit:boardSetTiles(tiles)
|
||||
if self:dashboardMode() then self:updateItems(nil, true) end
|
||||
end
|
||||
|
||||
function BoardEdit:boardSetColour(colour)
|
||||
self:persistSetting(BoardStore.SETTING_KEY, BoardStore.withColour(self.settings, colour))
|
||||
require("bookorbit_board_ui").clearSnapshots()
|
||||
if self:dashboardMode() then self:updateItems(nil, true) end
|
||||
end
|
||||
|
||||
function BoardEdit.install(Catalog)
|
||||
for name, fn in pairs(BoardEdit) do
|
||||
if name ~= "install" and type(fn) == "function" and name:match("^board") then
|
||||
|
||||
@@ -15,18 +15,21 @@ layout; until then the starter pages are computed fresh, so new widgets and
|
||||
device differences (a frontlight or not) apply without a migration.
|
||||
|
||||
Stored under catalog_board, keyed by column count plus "l" in landscape:
|
||||
{ version = 1, preset = "reader", tiles = "outline", started = true,
|
||||
{ version = 1, preset = "reader", tiles = "outline", colour = "accents", started = true,
|
||||
custom = { ["4"] = { pages = { { items = {...} }, ... } }, ["4l"] = ... } }
|
||||
]]
|
||||
|
||||
local _ = require("gettext")
|
||||
|
||||
local BoardColour = require("bookorbit_board_colour")
|
||||
|
||||
local BoardStore = {}
|
||||
|
||||
BoardStore.SETTING_KEY = "catalog_board"
|
||||
BoardStore.VERSION = 1
|
||||
BoardStore.PRESETS = { "reader", "stats", "library" }
|
||||
BoardStore.TILE_STYLES = { "outline", "soft", "ink" }
|
||||
BoardStore.COLOURS = BoardColour.SCHEMES
|
||||
BoardStore.PAGE_COUNT = 4
|
||||
BoardStore.PAGE_IDS = { "home", "insights", "library", "decor" }
|
||||
|
||||
@@ -163,12 +166,13 @@ end
|
||||
local function readState(settings)
|
||||
local stored = settings and settings[BoardStore.SETTING_KEY]
|
||||
if type(stored) ~= "table" or stored.version ~= BoardStore.VERSION then
|
||||
return { version = BoardStore.VERSION, preset = "reader", tiles = "outline", started = false, custom = {} }
|
||||
return { version = BoardStore.VERSION, preset = "reader", tiles = "outline", colour = BoardColour.DEFAULT, started = false, custom = {} }
|
||||
end
|
||||
local state = {
|
||||
version = BoardStore.VERSION,
|
||||
preset = stored.preset,
|
||||
tiles = stored.tiles,
|
||||
colour = BoardColour.normalize(stored.colour),
|
||||
started = stored.started == true,
|
||||
custom = type(stored.custom) == "table" and stored.custom or {},
|
||||
}
|
||||
@@ -253,4 +257,10 @@ function BoardStore.withTiles(settings, tiles)
|
||||
return state
|
||||
end
|
||||
|
||||
function BoardStore.withColour(settings, colour)
|
||||
local state = readState(settings)
|
||||
state.colour = BoardColour.normalize(colour)
|
||||
return state
|
||||
end
|
||||
|
||||
return BoardStore
|
||||
|
||||
@@ -42,6 +42,35 @@ local BoardUI = {}
|
||||
BoardUI.INK = INK
|
||||
BoardUI.px = px
|
||||
|
||||
-- KOReader's plain painters turn an RGB colour into its gray; RGB colours
|
||||
-- (the board's colour schemes) go through the RGB32 painters instead.
|
||||
local Blitbuffer = package.loaded["ffi/blitbuffer"] or select(2, pcall(require, "ffi/blitbuffer"))
|
||||
if type(Blitbuffer) ~= "table" then Blitbuffer = {} end
|
||||
|
||||
-- A truthiness test, never == nil: LuaJIT hands a colour's comparison with
|
||||
-- nil to its __eq, which indexes the other side.
|
||||
local function isRGB(c)
|
||||
if not c or not Blitbuffer.isColor8 then return false end
|
||||
return not Blitbuffer.isColor8(c)
|
||||
end
|
||||
BoardUI.isRGB = isRGB
|
||||
|
||||
local function paintBorder(bb, x, y, w, h, bw, c, r)
|
||||
if isRGB(c) and bb.paintBorderRGB32 then
|
||||
bb:paintBorderRGB32(x, y, w, h, bw, c, r)
|
||||
else
|
||||
bb:paintBorder(x, y, w, h, bw, c, r)
|
||||
end
|
||||
end
|
||||
|
||||
local function paintRoundedRect(bb, x, y, w, h, c, r)
|
||||
if isRGB(c) and bb.paintRoundedRectRGB32 then
|
||||
bb:paintRoundedRectRGB32(x, y, w, h, c, r)
|
||||
else
|
||||
bb:paintRoundedRect(x, y, w, h, c, r)
|
||||
end
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------- fit checks
|
||||
local fit_stack = {}
|
||||
|
||||
@@ -128,6 +157,41 @@ local function faceKind(o)
|
||||
return o.i and "sans_italic" or "sans"
|
||||
end
|
||||
|
||||
-- One line of text in an RGB colour. TextWidget paints glyphs in gray only,
|
||||
-- so the line is set white on black once, which is its coverage mask, and
|
||||
-- blitted in the colour. (Blitbuffer:invert only flips a flag that the C
|
||||
-- blitter ignores when both buffers carry it, as a night-mode screen does.)
|
||||
local ColourLine = Widget:extend{ line = nil, color = nil }
|
||||
|
||||
function ColourLine:getSize()
|
||||
return self.line:getSize()
|
||||
end
|
||||
|
||||
function ColourLine:isTruncated()
|
||||
return self.line:isTruncated()
|
||||
end
|
||||
|
||||
function ColourLine:paintTo(bb, x, y)
|
||||
local size = self.line:getSize()
|
||||
self.dimen = Geom:new{ x = x, y = y, w = size.w, h = size.h }
|
||||
if not self.mask then
|
||||
local canvas = Blitbuffer.new(size.w, size.h, Blitbuffer.TYPE_BB8)
|
||||
canvas:fill(Blitbuffer.COLOR_BLACK)
|
||||
self.line:paintTo(canvas, 0, 0)
|
||||
self.mask = canvas
|
||||
end
|
||||
if bb.colorblitFromRGB32 then
|
||||
bb:colorblitFromRGB32(self.mask, x, y, 0, 0, size.w, size.h, self.color)
|
||||
else
|
||||
bb:colorblitFrom(self.mask, x, y, 0, 0, size.w, size.h, self.color)
|
||||
end
|
||||
end
|
||||
|
||||
function ColourLine:free()
|
||||
if self.mask then self.mask:free(); self.mask = nil end
|
||||
if self.line and self.line.free then self.line:free() end
|
||||
end
|
||||
|
||||
-- A single line, or o.lines lines when o.lines > 1. o: s (nominal size),
|
||||
-- b, i, serif, col, w (box width), align, lines, data.
|
||||
function BoardUI.text(str, o)
|
||||
@@ -160,15 +224,17 @@ function BoardUI.text(str, o)
|
||||
if tb.line_with_ellipsis and not o.data then problem("clamped", str) end
|
||||
return tb
|
||||
end
|
||||
local rgb = isRGB(o.col) and Blitbuffer.TYPE_BB8 ~= nil
|
||||
local tw = TextWidget:new{
|
||||
text = str,
|
||||
padding = 0,
|
||||
face = face,
|
||||
bold = o.b == true,
|
||||
fgcolor = o.col or INK.black,
|
||||
fgcolor = rgb and INK.white or (o.col or INK.black),
|
||||
max_width = o.w,
|
||||
}
|
||||
if o.w and tw:isTruncated() and not o.data then problem("ellipsis", str .. " w=" .. o.w .. " need=" .. tw:getSize().w) end
|
||||
if rgb then tw = ColourLine:new{ line = tw, color = o.col } end
|
||||
if not o.w then return tw end
|
||||
local dimen = Geom:new{ w = o.w, h = tw:getSize().h }
|
||||
if o.align == "center" then return CenterContainer:new{ dimen = dimen, tw } end
|
||||
@@ -433,10 +499,31 @@ function BoardUI.spread(w, left, right, h)
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------------- frames
|
||||
-- An RGB border painted over a frame of the same size.
|
||||
local ColourRing = Widget:extend{ bw = 1, color = nil, radius = 0 }
|
||||
|
||||
function ColourRing:getSize()
|
||||
return self.dimen
|
||||
end
|
||||
|
||||
function ColourRing:paintTo(bb, x, y)
|
||||
self.dimen.x, self.dimen.y = x, y
|
||||
paintBorder(bb, x, y, self.dimen.w, self.dimen.h, self.bw, self.color, self.radius)
|
||||
end
|
||||
|
||||
-- A fixed box with an optional fill, radius and inner border.
|
||||
function BoardUI.box(o, child)
|
||||
local border = o.border or 0
|
||||
local inner_w, inner_h = o.w - 2 * border, o.h - 2 * border
|
||||
if border > 0 and isRGB(o.border_col) then
|
||||
local plain = {}
|
||||
for k, v in pairs(o) do plain[k] = v end
|
||||
plain.border_col = INK.white
|
||||
return BoardUI.overlay(o.w, o.h, {
|
||||
{ widget = BoardUI.box(plain, child) },
|
||||
{ widget = ColourRing:new{ dimen = Geom:new{ w = o.w, h = o.h }, bw = border, color = o.border_col, radius = o.radius or 0 } },
|
||||
})
|
||||
end
|
||||
return FrameContainer:new{
|
||||
margin = 0,
|
||||
padding = 0,
|
||||
@@ -683,7 +770,7 @@ function BoardCover:paintTo(bb, x, y)
|
||||
bb:blitFrom(img.bb, px_, py_, 0, 0, img.w, img.h)
|
||||
if self.dim then bb:lightenRect(x, y, w, h, 0.55) end
|
||||
if Screen.night_mode then bb:invertRect(px_, py_, img.w, img.h) end
|
||||
if self.border > 0 then bb:paintBorder(px_, py_, img.w, img.h, self.border, self.border_col or INK.gb) end
|
||||
if self.border > 0 then paintBorder(bb, px_, py_, img.w, img.h, self.border, self.border_col or INK.gb) end
|
||||
return
|
||||
end
|
||||
local dx, dy, dw, dh
|
||||
@@ -708,7 +795,7 @@ function BoardCover:paintTo(bb, x, y)
|
||||
bb:invertRect(dx, dy, dw, dh)
|
||||
end
|
||||
if self.border > 0 then
|
||||
bb:paintBorder(dx, dy, dw, dh, self.border, self.border_col or INK.gb)
|
||||
paintBorder(bb, dx, dy, dw, dh, self.border, self.border_col or INK.gb)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -769,7 +856,7 @@ function BoardUI.cover(book, w, h, o)
|
||||
-- too small for one line stays an empty frame.
|
||||
local lines = math.min(4, math.floor((h - 2 * frame_b - 2 * pad) / BoardMetrics.lh(s)))
|
||||
if o.loading ~= true and w > px(48) and lines >= 1 then
|
||||
inner = { BoardUI.flex(0), BoardUI.text(title, { s = s, b = true, serif = true, lines = lines, w = tw, align = "center", data = true }), BoardUI.flex(0) }
|
||||
inner = { BoardUI.flex(0), BoardUI.text(title, { s = s, b = true, serif = true, lines = lines, w = tw, align = "center", data = true, bg = INK.ge }), BoardUI.flex(0) }
|
||||
end
|
||||
return BoardUI.box({ w = w, h = h, border = frame_b, border_col = INK.black, bg = INK.ge },
|
||||
BoardUI.vstack({ w = w - 2 * frame_b, h = h - 2 * frame_b, pad = pad, align = "center" }, inner))
|
||||
@@ -798,10 +885,12 @@ function CellGrid:paintTo(bb, x, y)
|
||||
bb:paintRoundedRect(x + cell.x - ring, y + cell.y - ring, s + 2 * ring, s + 2 * ring, INK.black, r + ring)
|
||||
bb:paintRoundedRect(x + cell.x - gap, y + cell.y - gap, s + 2 * gap, s + 2 * gap, INK.white, r + gap)
|
||||
end
|
||||
if cell.border then
|
||||
if cell.border and isRGB(cell.col) and bb.paintBorderRGB32 then
|
||||
bb:paintBorderRGB32(x + cell.x, y + cell.y, s, s, cell.border, cell.col, r, true)
|
||||
elseif cell.border then
|
||||
bb:paintBorder(x + cell.x, y + cell.y, s, s, cell.border, cell.col, r, true)
|
||||
else
|
||||
bb:paintRoundedRect(x + cell.x, y + cell.y, s, s, cell.col, r)
|
||||
paintRoundedRect(bb, x + cell.x, y + cell.y, s, s, cell.col, r)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,6 +12,7 @@ local datetime = require("datetime")
|
||||
local T = require("ffi/util").template
|
||||
local _ = require("gettext")
|
||||
|
||||
local BoardColour = require("bookorbit_board_colour")
|
||||
local BoardData = require("bookorbit_board_data")
|
||||
local BoardStore = require("bookorbit_board_store")
|
||||
local BoardMetrics = require("bookorbit_board_metrics")
|
||||
@@ -85,7 +86,7 @@ local function cover(ctx, book, w, h, o)
|
||||
path = book and book.cover_path,
|
||||
loading = book and book.cover_state == "loading",
|
||||
border = o.border or thin(),
|
||||
border_col = o.border_col or INK.gb,
|
||||
border_col = o.border_col or BoardColour.coverEdge(ctx, INK.gb),
|
||||
dim = o.dim,
|
||||
})
|
||||
end
|
||||
@@ -132,7 +133,7 @@ W.hero = function(ctx, w, hh)
|
||||
return hstack({ w = w, h = hh }, {
|
||||
space(pad, 1),
|
||||
vstack({ w = tw }, {
|
||||
text(_("Start reading"), { s = 11, b = true, col = INK.gb, w = tw }),
|
||||
text(_("Start reading"), { s = 11, b = true, col = BoardColour.onInk(ctx, INK.gb, "hero"), w = tw }),
|
||||
space(nil, px(3)),
|
||||
text(label, { s = 16, b = true, col = INK.white, lines = 2, w = tw }),
|
||||
}),
|
||||
@@ -159,7 +160,7 @@ W.hero = function(ctx, w, hh)
|
||||
local shuffle = BoardUI.box({ w = BoardUI.runs({ _("Shuffle") }, { s = 14 }):getSize().w + px(16) + px(13) + px(16) + px(8), h = btn_h, radius = math.floor(btn_h / 2), bg = INK.white },
|
||||
hstack({ h = btn_h, gap = px(8) }, { space(px(13), 1), icon("shuffle", px(16), { sw = 2.25 }), text(_("Shuffle"), { s = 14, b = true }) }))
|
||||
return vstack({ w = w, h = hh, pad = pad }, {
|
||||
text(_("Start reading"), { s = 11, b = true, col = INK.gb, w = iw }),
|
||||
text(_("Start reading"), { s = 11, b = true, col = BoardColour.onInk(ctx, INK.gb, "hero"), w = iw }),
|
||||
space(nil, px(3)),
|
||||
text(_("Pick a book from your library to begin"), { s = 16, b = true, col = INK.white, lines = 2, w = iw }),
|
||||
flex(px(10)),
|
||||
@@ -195,9 +196,9 @@ W.hero = function(ctx, w, hh)
|
||||
local function meta(mv)
|
||||
local m = { { t = BoardData.pctLabel(book) or "0%", b = true, col = INK.white } }
|
||||
if mv == 2 and book.minutes_left then
|
||||
table.insert(m, { t = " · " .. T(_("%1 left"), BoardData.formatDuration(book.minutes_left)), col = INK.gc })
|
||||
table.insert(m, { t = " · " .. T(_("%1 left"), BoardData.formatDuration(book.minutes_left)), col = BoardColour.onInk(ctx, INK.gc, "hero") })
|
||||
elseif mv == 1 and book.minutes_left then
|
||||
table.insert(m, { t = " · " .. T(_("%1 left"), BoardData.formatShortDuration(book.minutes_left)), col = INK.gc })
|
||||
table.insert(m, { t = " · " .. T(_("%1 left"), BoardData.formatShortDuration(book.minutes_left)), col = BoardColour.onInk(ctx, INK.gc, "hero") })
|
||||
end
|
||||
return m
|
||||
end
|
||||
@@ -216,11 +217,11 @@ W.hero = function(ctx, w, hh)
|
||||
space(px(14), 1),
|
||||
vstack({ w = tw2, h = ch }, {
|
||||
flex(0),
|
||||
lab and text(lab, { s = 11, b = true, col = INK.gb, w = tw2 }) or false,
|
||||
lab and text(lab, { s = 11, b = true, col = BoardColour.onInk(ctx, INK.gb, "hero"), w = tw2 }) or false,
|
||||
lab and space(nil, px(3)) or false,
|
||||
text(book.title, { s = tf.s, b = true, col = INK.white, lines = tf.lines, w = tw2, data = true }),
|
||||
space(nil, px(7)),
|
||||
hstack({ w = tw2, gap = px(10) }, { bar(math.max(px(20), tw2 - mw - px(10)), px(5), pct, { track = INK.g5, fill = INK.white, r = px(2.5) }), m }),
|
||||
hstack({ w = tw2, gap = px(10) }, { bar(math.max(px(20), tw2 - mw - px(10)), px(5), pct, { track = BoardColour.inkTrack(ctx, INK.g5, "hero"), fill = INK.white, r = px(2.5) }), m }),
|
||||
flex(0),
|
||||
}),
|
||||
space(px(14), 1),
|
||||
@@ -252,13 +253,13 @@ W.hero = function(ctx, w, hh)
|
||||
end
|
||||
local function pager()
|
||||
if total <= 1 then return nil end
|
||||
return hstack({ gap = px(2) }, { text(T(_("%1 / %2"), 1, total), { s = 12, col = INK.gb }), icon("chevron-right", px(15), { col = "gb", sw = 2.5 }) })
|
||||
return hstack({ gap = px(2) }, { text(T(_("%1 / %2"), 1, total), { s = 12, col = BoardColour.onInk(ctx, INK.gb, "hero") }), icon("chevron-right", px(15), { col = BoardColour.onInkHex(ctx, "gb", "hero"), sw = 2.5 }) })
|
||||
end
|
||||
local function meta(full)
|
||||
local m = { { t = tostring(math.floor(pct + 0.5)) .. "%", b = true, col = INK.white } }
|
||||
if book.minutes_left then
|
||||
local dur = full and BoardData.formatDuration(book.minutes_left) or BoardData.formatShortDuration(book.minutes_left)
|
||||
table.insert(m, { t = " · " .. T(_("%1 left"), dur), col = INK.gc })
|
||||
table.insert(m, { t = " · " .. T(_("%1 left"), dur), col = BoardColour.onInk(ctx, INK.gc, "hero") })
|
||||
end
|
||||
return m
|
||||
end
|
||||
@@ -276,7 +277,7 @@ W.hero = function(ctx, w, hh)
|
||||
local fixed = lh1(12) + label_gap + author_gap + lh1(15) + progress_gap + px(6) + meta_gap + lh1(13) + action_gap + px(40)
|
||||
local tf = BoardUI.fitTitle(book.title, { b = true }, tw, { 26, 23, 20, 18, 16 }, 3, cover_h - fixed)
|
||||
local actions = opt.ghost and hstack({ gap = px(8) }, { btn(opt.short), ghost() }) or btn(opt.short)
|
||||
local label = text(_("Continue reading"), { s = 12, b = true, col = INK.gb })
|
||||
local label = text(_("Continue reading"), { s = 12, b = true, col = BoardColour.onInk(ctx, INK.gb, "hero") })
|
||||
return hstack({ w = w, h = hh, valign = "top" }, {
|
||||
space(pad, 1),
|
||||
-- A cover narrower than the column's height is centred in it,
|
||||
@@ -288,9 +289,9 @@ W.hero = function(ctx, w, hh)
|
||||
space(nil, label_gap),
|
||||
text(book.title, { s = tf.s, b = true, col = INK.white, lines = tf.lines, w = tw, data = true }),
|
||||
space(nil, author_gap),
|
||||
text(firstAuthor(book) or "", { s = 15, col = INK.gd, w = tw, data = true }),
|
||||
text(firstAuthor(book) or "", { s = 15, col = BoardColour.onInk(ctx, INK.gd, "hero"), w = tw, data = true }),
|
||||
flex(progress_gap),
|
||||
bar(tw, px(6), pct, { track = INK.g5, fill = INK.white, r = px(3) }),
|
||||
bar(tw, px(6), pct, { track = BoardColour.inkTrack(ctx, INK.g5, "hero"), fill = INK.white, r = px(3) }),
|
||||
space(nil, meta_gap),
|
||||
runs(meta(opt.full_meta), { s = 13, w = tw }),
|
||||
space(nil, action_gap),
|
||||
@@ -319,17 +320,17 @@ W.hero = function(ctx, w, hh)
|
||||
vstack({ w = cover_w, h = hh, pad = { pad, 0, pad, 0 } }, { flex(0), cover(ctx, book, cover_w, math.min(cover_h, math.floor(cover_w * 1.5 + 0.5)), { border = math.max(1, px(1)), border_col = INK.g4 }), flex(0) }),
|
||||
space(gutter, 1),
|
||||
vstack({ w = tw, h = hh, pad = { pad, 0, pad, 0 }, strict_padding = true }, {
|
||||
text(_("Continue reading"), { s = 12, b = true, col = INK.gb, w = tw }),
|
||||
text(_("Continue reading"), { s = 12, b = true, col = BoardColour.onInk(ctx, INK.gb, "hero"), w = tw }),
|
||||
space(nil, px(6)),
|
||||
text(book.title, { s = tf.s, b = true, col = INK.white, lines = tf.lines, w = tw, data = true }),
|
||||
opt.author and space(nil, px(4)) or false,
|
||||
opt.author and text(firstAuthor(book) or "", { s = 13, col = INK.gd, w = tw, data = true }) or false,
|
||||
opt.author and text(firstAuthor(book) or "", { s = 13, col = BoardColour.onInk(ctx, INK.gd, "hero"), w = tw, data = true }) or false,
|
||||
flex(px(8)),
|
||||
hstack({ w = tw, gap = px(10) }, {
|
||||
vstack({ w = bar_w }, {
|
||||
text(tostring(math.floor(pct + 0.5)) .. "%", { s = 13, b = true, col = INK.white, w = bar_w }),
|
||||
space(nil, px(5)),
|
||||
bar(bar_w, px(6), pct, { track = INK.g5, fill = INK.white, r = px(3) }),
|
||||
bar(bar_w, px(6), pct, { track = BoardColour.inkTrack(ctx, INK.g5, "hero"), fill = INK.white, r = px(3) }),
|
||||
}),
|
||||
BoardUI.hit(roundButton(ctx, btn_d, play_icon, book.on_device, true, not book.on_device and not d.online), { kind = "book", book = book, resume = true, list = { label = _("Continue reading"), books = d.continue_reading } }),
|
||||
}),
|
||||
@@ -345,7 +346,7 @@ W.hero = function(ctx, w, hh)
|
||||
local cover_w = math.floor(cover_h * 2 / 3 + 0.5)
|
||||
local tw = iw - cover_w - px(12)
|
||||
local tf = BoardUI.fitTitle(book.title, { b = true }, tw, { 20, 18, 16, 15 }, 3, cover_h - (opt.author and (lh1(13) + px(4)) or 0))
|
||||
local label = text(_("Continue reading"), { s = 12, b = true, col = INK.gb })
|
||||
local label = text(_("Continue reading"), { s = 12, b = true, col = BoardColour.onInk(ctx, INK.gb, "hero") })
|
||||
return vstack({ w = w, h = hh, pad = pad, strict_padding = true }, {
|
||||
opt.pager and BoardUI.spread(iw, label, pager()) or label,
|
||||
space(nil, px(8)),
|
||||
@@ -354,11 +355,11 @@ W.hero = function(ctx, w, hh)
|
||||
vstack({ w = tw }, {
|
||||
text(book.title, { s = tf.s, b = true, col = INK.white, lines = tf.lines, w = tw, data = true }),
|
||||
opt.author and space(nil, px(4)) or false,
|
||||
opt.author and text(firstAuthor(book) or "", { s = 13, col = INK.gd, w = tw, data = true }) or false,
|
||||
opt.author and text(firstAuthor(book) or "", { s = 13, col = BoardColour.onInk(ctx, INK.gd, "hero"), w = tw, data = true }) or false,
|
||||
}),
|
||||
}),
|
||||
flex(px(8)),
|
||||
bar(iw, px(6), pct, { track = INK.g5, fill = INK.white, r = px(3) }),
|
||||
bar(iw, px(6), pct, { track = BoardColour.inkTrack(ctx, INK.g5, "hero"), fill = INK.white, r = px(3) }),
|
||||
space(nil, px(8)),
|
||||
runs(meta(opt.full_meta), { s = 13, w = iw }),
|
||||
space(nil, px(12)),
|
||||
@@ -374,7 +375,7 @@ W.hero = function(ctx, w, hh)
|
||||
local bar_w = iw - btn_d - px(10)
|
||||
local tf = BoardUI.fitTitle(book.title, { b = true }, iw, { 18, 16, 15 }, 2, ih - lh1(12) - px(6) - px(8) - btn_d)
|
||||
return vstack({ w = w, h = hh, pad = pad, strict_padding = true }, {
|
||||
text(_("Continue reading"), { s = 12, b = true, col = INK.gb, w = iw }),
|
||||
text(_("Continue reading"), { s = 12, b = true, col = BoardColour.onInk(ctx, INK.gb, "hero"), w = iw }),
|
||||
space(nil, px(6)),
|
||||
text(book.title, { s = tf.s, b = true, col = INK.white, lines = tf.lines, w = iw, data = true }),
|
||||
flex(px(8)),
|
||||
@@ -382,7 +383,7 @@ W.hero = function(ctx, w, hh)
|
||||
vstack({ w = bar_w }, {
|
||||
text(tostring(math.floor(pct + 0.5)) .. "%", { s = 13, b = true, col = INK.white, w = bar_w }),
|
||||
space(nil, px(5)),
|
||||
bar(bar_w, px(6), pct, { track = INK.g5, fill = INK.white, r = px(3) }),
|
||||
bar(bar_w, px(6), pct, { track = BoardColour.inkTrack(ctx, INK.g5, "hero"), fill = INK.white, r = px(3) }),
|
||||
}),
|
||||
BoardUI.hit(roundButton(ctx, btn_d, play_icon, book.on_device, true, not book.on_device and not d.online), { kind = "book", book = book, resume = true, list = { label = _("Continue reading"), books = d.continue_reading } }),
|
||||
}),
|
||||
@@ -424,9 +425,9 @@ W.streak = function(ctx, w, hh)
|
||||
local dot_gap = wide and px(7) or math.max(px(3), math.floor((iw - 7 * px(9)) / 6))
|
||||
return pick({
|
||||
function()
|
||||
local dot_row = dots(d.streak_week, dot_size, dot_gap)
|
||||
local dot_row = dots(d.streak_week, dot_size, dot_gap, { col = BoardColour.ink(ctx, INK.black) })
|
||||
return vstack({ w = w, h = hh, pad = pad }, {
|
||||
head(iw, _("Streak"), icon("flame", px(17), { sw = 2.25 })),
|
||||
head(iw, _("Streak"), icon("flame", px(17), BoardColour.icon(ctx, 2.25))),
|
||||
flex(0),
|
||||
count(iw),
|
||||
flex(0),
|
||||
@@ -436,14 +437,14 @@ W.streak = function(ctx, w, hh)
|
||||
-- A short row: the week beside the count.
|
||||
function()
|
||||
return vstack({ w = w, h = hh, pad = pad }, {
|
||||
head(iw, _("Streak"), icon("flame", px(17), { sw = 2.25 })),
|
||||
head(iw, _("Streak"), icon("flame", px(17), BoardColour.icon(ctx, 2.25))),
|
||||
flex(0),
|
||||
BoardUI.spread(iw, count(nil), dots(d.streak_week, dot_size, dot_gap)),
|
||||
BoardUI.spread(iw, count(nil), dots(d.streak_week, dot_size, dot_gap, { col = BoardColour.ink(ctx, INK.black) })),
|
||||
})
|
||||
end,
|
||||
function()
|
||||
return vstack({ w = w, h = hh, pad = pad }, {
|
||||
head(iw, _("Streak"), icon("flame", px(17), { sw = 2.25 })),
|
||||
head(iw, _("Streak"), icon("flame", px(17), BoardColour.icon(ctx, 2.25))),
|
||||
flex(0),
|
||||
count(iw),
|
||||
})
|
||||
@@ -464,7 +465,7 @@ W.week = function(ctx, w, hh)
|
||||
local bars = {}
|
||||
for i, v in ipairs(d.week) do
|
||||
local h = v > 0 and math.max(px(3), math.floor((chart_h - px(2)) * v / max + 0.5)) or px(2)
|
||||
local col = v > 0 and (i == 7 and INK.black or INK.g8) or INK.gc
|
||||
local col = v > 0 and (i == 7 and BoardColour.ink(ctx, INK.black) or BoardColour.mid(ctx, INK.g8)) or BoardColour.track(ctx, INK.gc)
|
||||
table.insert(bars, BoardUI.rect(bar_w, h, col))
|
||||
end
|
||||
return vstack({ w = iw }, {
|
||||
@@ -530,7 +531,7 @@ W.today = function(ctx, w, hh)
|
||||
return pick({
|
||||
function()
|
||||
return vstack({ w = w, h = hh, pad = pad }, {
|
||||
head(iw, _("Today"), icon("clock", px(17), { sw = 2.25 })),
|
||||
head(iw, _("Today"), icon("clock", px(17), BoardColour.icon(ctx, 2.25))),
|
||||
flex(0),
|
||||
value(),
|
||||
flex(0),
|
||||
@@ -539,7 +540,7 @@ W.today = function(ctx, w, hh)
|
||||
end,
|
||||
function()
|
||||
return vstack({ w = w, h = hh, pad = pad }, {
|
||||
head(iw, _("Today"), icon("clock", px(17), { sw = 2.25 })),
|
||||
head(iw, _("Today"), icon("clock", px(17), BoardColour.icon(ctx, 2.25))),
|
||||
flex(0),
|
||||
value(),
|
||||
})
|
||||
@@ -559,7 +560,7 @@ W.goalpace = function(ctx, w, hh)
|
||||
local function empty(str)
|
||||
return function()
|
||||
return vstack({ w = w, h = hh, pad = pad }, {
|
||||
head(iw, T(_("%1 goal"), g.year), icon("target", px(16), { sw = 2.25 })),
|
||||
head(iw, T(_("%1 goal"), g.year), icon("target", px(16), BoardColour.icon(ctx, 2.25))),
|
||||
space(nil, px(6)),
|
||||
hint(str, iw, hh - 2 * pad - lh1(13) - px(6)),
|
||||
})
|
||||
@@ -583,7 +584,7 @@ W.goalpace = function(ctx, w, hh)
|
||||
statuses = { T(_("%1 behind pace"), behind), T(_("%1 behind"), behind) }
|
||||
end
|
||||
local behind = not met and done > 0 and done < expected
|
||||
local trophy = icon(met and "trophy" or "target", px(16), { sw = 2.25 })
|
||||
local trophy = icon(met and "trophy" or "target", px(16), BoardColour.icon(ctx, 2.25))
|
||||
local function goalBar()
|
||||
local scale = math.max(goal, projected, done, 1)
|
||||
local goal_x = math.floor(iw * goal / scale + 0.5)
|
||||
@@ -592,7 +593,7 @@ W.goalpace = function(ctx, w, hh)
|
||||
and BoardUI.box({ w = tick_w + 2 * math.max(1, px(1)), h = px(14) + 2 * math.max(1, px(1)), border = math.max(1, px(1)), bg = INK.white })
|
||||
or rect(tick_w, px(14), INK.black)
|
||||
return BoardUI.overlay(iw, px(14), {
|
||||
{ widget = bar(iw, px(6), math.floor(100 * done / scale + 0.5), { track = INK.gd, r = px(3) }), x = 0, y = px(4) },
|
||||
{ widget = bar(iw, px(6), math.floor(100 * done / scale + 0.5), { track = BoardColour.track(ctx, INK.gd), fill = BoardColour.ink(ctx, INK.black), r = px(3) }), x = 0, y = px(4) },
|
||||
{ widget = tick, x = math.min(iw - px(2), goal_x) - (met and math.max(1, px(1)) or 0), y = met and -math.max(1, px(1)) or 0 },
|
||||
})
|
||||
end
|
||||
@@ -669,13 +670,14 @@ W.heatmap = function(ctx, w, hh)
|
||||
if v > 0 then days_read = days_read + 1 end
|
||||
if v > best then best, best_i = v, i end
|
||||
end
|
||||
local ramp = BoardColour.ramp(ctx, { INK.ge, INK.gb, INK.g8, INK.g4, INK.black })
|
||||
local function level(v)
|
||||
if v == nil then return nil end
|
||||
if v <= 0 then return INK.ge end
|
||||
if v < 30 then return INK.gb end
|
||||
if v < 60 then return INK.g8 end
|
||||
if v < 120 then return INK.g4 end
|
||||
return INK.black
|
||||
if v <= 0 then return ramp[1] end
|
||||
if v < 30 then return ramp[2] end
|
||||
if v < 60 then return ramp[3] end
|
||||
if v < 120 then return ramp[4] end
|
||||
return ramp[5]
|
||||
end
|
||||
local grid_w = weeks * cell + (weeks - 1) * g
|
||||
local radius = math.max(1, px(1.5))
|
||||
@@ -816,7 +818,7 @@ W.calendar = function(ctx, w, hh)
|
||||
local x, y = (i % per_row) * (cell + gap), math.floor(i / per_row) * (cell + gap)
|
||||
local is_read = read[day] == true
|
||||
if is_read then
|
||||
table.insert(cells, { x = x, y = y, col = day == today and INK.black or INK.g6 })
|
||||
table.insert(cells, { x = x, y = y, col = day == today and BoardColour.deep(ctx, INK.black) or BoardColour.ink(ctx, INK.g6) })
|
||||
else
|
||||
table.insert(cells, { x = x, y = y, col = day > today and INK.gd or INK.gb, border = outline })
|
||||
end
|
||||
@@ -878,11 +880,11 @@ W.also = function(ctx, w, hh)
|
||||
})
|
||||
table.insert(rows, sep or false)
|
||||
table.insert(rows, BoardUI.hit(hstack({ w = iw, h = row_inner_h, gap = px(10) }, {
|
||||
cover(ctx, b, cover_w, cover_h, { border = thin(), border_col = INK.gb, dim = offlineDim(ctx, b) }),
|
||||
cover(ctx, b, cover_w, cover_h, { border = thin(), dim = offlineDim(ctx, b) }),
|
||||
vstack({ w = tw }, {
|
||||
text(b.title, { s = 14, b = true, w = tw, data = true }),
|
||||
space(nil, px(5)),
|
||||
hstack({ w = tw, gap = px(8) }, { bar(tw - right_w - px(8), px(4), pct, { track = INK.gc, r = px(2) }), right }),
|
||||
hstack({ w = tw, gap = px(8) }, { bar(tw - right_w - px(8), px(4), pct, { track = BoardColour.track(ctx, INK.gc), fill = BoardColour.ink(ctx, INK.black), r = px(2) }), right }),
|
||||
}),
|
||||
}), { kind = "book", book = b, resume = ctx.tap_resumes, list = { label = _("Continue reading"), books = d.continue_reading } }))
|
||||
end
|
||||
@@ -907,7 +909,7 @@ W.highlight = function(ctx, w, hh)
|
||||
return pick({
|
||||
function()
|
||||
return vstack({ w = w, h = hh, pad = pad }, {
|
||||
icon("quote", px(20), { fill = true, sw = 1 }),
|
||||
icon("quote", px(20), { fill = true, sw = 1, col = BoardColour.artHex(ctx, "#000000", "deep") }),
|
||||
space(nil, px(8)),
|
||||
hint(str, iw, hh - 2 * pad - px(20) - px(8), o),
|
||||
})
|
||||
@@ -925,7 +927,7 @@ W.highlight = function(ctx, w, hh)
|
||||
return text(hl.text, { s = qs, i = true, serif = true, lines = lines > 1 and lines or nil, w = iw3, col = INK.g1, data = true })
|
||||
end
|
||||
local label = function()
|
||||
return hstack({ w = iw3, h = lh1(12), gap = px(6) }, { icon("quote", px(14), { fill = true, sw = 1 }), text(_("Highlight of the day"), { s = 12, b = true, col = INK.g4 }), flex(0) })
|
||||
return hstack({ w = iw3, h = lh1(12), gap = px(6) }, { icon("quote", px(14), { fill = true, sw = 1, col = BoardColour.artHex(ctx, "#000000", "deep") }), text(_("Highlight of the day"), { s = 12, b = true, col = INK.g4 }), flex(0) })
|
||||
end
|
||||
return pick({
|
||||
function()
|
||||
@@ -1005,7 +1007,7 @@ W.shelf = function(ctx, w, hh, props, item)
|
||||
local title = finished and T(_("Finished in %1"), d.goal.year) or BoardWidgets.shelfTitle(props)
|
||||
local right
|
||||
if props.shuffle then
|
||||
right = BoardUI.hit(hstack({ w = px(36), h = lh1(13) }, { flex(0), icon("shuffle", px(17), { sw = 2.25, col = d.online and "black" or "ga" }) }),
|
||||
right = BoardUI.hit(hstack({ w = px(36), h = lh1(13) }, { flex(0), icon("shuffle", px(17), { sw = BoardColour.icon(ctx, 2.25).sw, col = d.online and BoardColour.artHex(ctx, "#000000") or "ga" }) }),
|
||||
d.online and { kind = "shuffle" } or { kind = "noop" })
|
||||
elseif finished then
|
||||
right = hstack({ gap = px(2) }, { text(tostring(d.goal.done or 0), { s = 13, b = true }), icon("chevron-right", px(15), { sw = 2.5 }) })
|
||||
@@ -1035,7 +1037,7 @@ W.shelf = function(ctx, w, hh, props, item)
|
||||
for i = 1, math.min(n, #books) do
|
||||
local b = books[i]
|
||||
table.insert(cards, BoardUI.hit(vstack({ w = cover_w }, {
|
||||
cover(ctx, b, cover_w, cover_h, { border = thin(), border_col = INK.gb, dim = offlineDim(ctx, b) }),
|
||||
cover(ctx, b, cover_w, cover_h, { border = thin(), dim = offlineDim(ctx, b) }),
|
||||
space(nil, px(6)),
|
||||
text(b.title, { s = title_size, b = true, w = cover_w, data = true }),
|
||||
}), { kind = "detail", book_id = b.id, list = { label = title, books = books } }))
|
||||
@@ -1080,8 +1082,8 @@ local function fan(ctx, books, cover_h)
|
||||
local layers = {}
|
||||
for i = #list, 1, -1 do
|
||||
local x = (#list - i) * step
|
||||
table.insert(layers, { widget = rect(cw + 2 * ring, cover_h + 2 * ring, INK.white), x = x - ring, y = -ring })
|
||||
table.insert(layers, { widget = cover(ctx, list[i], cw, cover_h, { border = thin(), border_col = INK.g9 }), x = x, y = 0 })
|
||||
table.insert(layers, { widget = rect(cw + 2 * ring, cover_h + 2 * ring, BoardColour.halo(ctx, INK.white)), x = x - ring, y = -ring })
|
||||
table.insert(layers, { widget = cover(ctx, list[i], cw, cover_h, { border = thin(), border_col = BoardColour.coverEdge(ctx, INK.g9) }), x = x, y = 0 })
|
||||
end
|
||||
return BoardUI.overlay(width, cover_h, layers)
|
||||
end
|
||||
@@ -1117,7 +1119,7 @@ W.link = function(ctx, w, hh, props)
|
||||
need = text(label, { s = size, b = true }):getSize().w
|
||||
end
|
||||
return hstack({ w = iw, h = lh1(13), gap = px(5) }, {
|
||||
with_icon and icon(spec[1], px(15), { sw = 2.25 }) or false,
|
||||
with_icon and icon(spec[1], px(15), BoardColour.icon(ctx, 2.25)) or false,
|
||||
text(label, { s = size, b = true, w = lab_w }),
|
||||
chevron or false,
|
||||
})
|
||||
@@ -1141,7 +1143,7 @@ W.link = function(ctx, w, hh, props)
|
||||
local label2 = text(T(_("%1 GB free"), string.format("%.1f", storage.free_gb):gsub("%.0$", "")), { s = 11, col = INK.g4 })
|
||||
local used = math.floor(100 * (storage.total_gb - storage.free_gb) / storage.total_gb + 0.5)
|
||||
table.insert(items, space(nil, px(5)))
|
||||
table.insert(items, hstack({ w = iw, gap = px(8) }, { bar(iw - label2:getSize().w - px(8), px(6), used, { track = INK.gd, r = px(3) }), label2 }))
|
||||
table.insert(items, hstack({ w = iw, gap = px(8) }, { bar(iw - label2:getSize().w - px(8), px(6), used, { track = BoardColour.track(ctx, INK.gd), fill = BoardColour.ink(ctx, INK.black), r = px(3) }), label2 }))
|
||||
end
|
||||
return vstack({ w = w, h = hh, pad = pad }, items)
|
||||
end,
|
||||
@@ -1253,7 +1255,7 @@ W.clock = function(ctx, w, hh)
|
||||
text(d.clock, { s = val_s, b = true, w = iw, col = INK.white }),
|
||||
flex(0),
|
||||
text(d.weekday, { s = 13, b = true, w = iw, col = INK.white }),
|
||||
text(d.day_month, { s = 13, w = iw, col = INK.gc }),
|
||||
text(d.day_month, { s = 13, w = iw, col = BoardColour.onInk(ctx, INK.gc) }),
|
||||
})
|
||||
end,
|
||||
-- A short row: the time over the date.
|
||||
@@ -1281,11 +1283,11 @@ W.sync = function(ctx, w, hh)
|
||||
for _i, o in ipairs(opts) do
|
||||
table.insert(variants, function()
|
||||
return vstack({ w = w, h = hh, pad = pad }, {
|
||||
head(iw, title, icon(on and "cloud-check" or "cloud-off", px(17), { sw = 2.25 })),
|
||||
head(iw, title, icon(on and "cloud-check" or "cloud-off", px(17), BoardColour.icon(ctx, 2.25))),
|
||||
flex(0),
|
||||
text(o[1], { s = o[2], b = true, w = iw }),
|
||||
space(nil, px(6)),
|
||||
hstack({ gap = px(5) }, { icon(on and "refresh-cw" or "wifi", px(14), { sw = 2.5 }), text(on and _("Sync now") or _("Connect"), { s = 12, b = true }) }),
|
||||
hstack({ gap = px(5) }, { icon(on and "refresh-cw" or "wifi", px(14), BoardColour.icon(ctx, 2.5)), text(on and _("Sync now") or _("Connect"), { s = 12, b = true }) }),
|
||||
})
|
||||
end)
|
||||
end
|
||||
@@ -1293,7 +1295,7 @@ W.sync = function(ctx, w, hh)
|
||||
for _i, o in ipairs(opts) do
|
||||
table.insert(variants, function()
|
||||
return vstack({ w = w, h = hh, pad = pad }, {
|
||||
head(iw, title, icon(on and "cloud-check" or "cloud-off", px(17), { sw = 2.25 })),
|
||||
head(iw, title, icon(on and "cloud-check" or "cloud-off", px(17), BoardColour.icon(ctx, 2.25))),
|
||||
flex(0),
|
||||
text(o[1], { s = o[2], b = true, w = iw }),
|
||||
})
|
||||
@@ -1327,7 +1329,7 @@ W.hours = function(ctx, w, hh)
|
||||
local iw = w - 2 * pad
|
||||
local hours = d.hours or {}
|
||||
local part = dayPart(hours)
|
||||
local function sunMoon() return icon("sun-moon", px(17), { sw = 2.25 }) end
|
||||
local function sunMoon() return icon("sun-moon", px(17), BoardColour.icon(ctx, 2.25)) end
|
||||
if not part then
|
||||
return vstack({ w = w, h = hh, pad = pad }, {
|
||||
head(iw, _("When you read"), sunMoon()),
|
||||
@@ -1351,7 +1353,7 @@ W.hours = function(ctx, w, hh)
|
||||
local v = hours[i] or 0
|
||||
if v > 0 then
|
||||
local h = math.max(px(2), math.floor((chart_h - px(2)) * v / max + 0.5))
|
||||
table.insert(bars, BoardUI.rect(bar_w, h, i - 1 == peak and INK.black or INK.g8))
|
||||
table.insert(bars, BoardUI.rect(bar_w, h, i - 1 == peak and BoardColour.ink(ctx, INK.black) or BoardColour.mid(ctx, INK.g8)))
|
||||
else
|
||||
table.insert(bars, space(bar_w, 0))
|
||||
end
|
||||
@@ -1419,7 +1421,7 @@ W.records = function(ctx, w, hh)
|
||||
local r = d.records
|
||||
local pad = px(12)
|
||||
local iw = w - 2 * pad
|
||||
local function award() return icon("award", px(17), { sw = 2.25 }) end
|
||||
local function award() return icon("award", px(17), BoardColour.icon(ctx, 2.25)) end
|
||||
if not r then
|
||||
return vstack({ w = w, h = hh, pad = pad }, {
|
||||
head(iw, _("Reading records"), award()),
|
||||
@@ -1503,7 +1505,7 @@ W.toggles = function(ctx, w, hh)
|
||||
table.insert(items, { "refresh-cw", _("Sync"), false, "sync" })
|
||||
local function circleBtn(size, it)
|
||||
local on = it[3]
|
||||
return BoardUI.hit(BoardUI.circle(size, { bg = on and INK.black or INK.white, border = on and 0 or math.max(1, px(1.5)) },
|
||||
return BoardUI.hit(BoardUI.circle(size, { bg = on and BoardColour.ink(ctx, INK.black) or INK.white, border = on and 0 or math.max(1, px(1.5)) },
|
||||
icon(it[1], math.floor(size * 0.46 + 0.5), { col = on and "white" or "black", sw = 2.25 })), { kind = it[4] })
|
||||
end
|
||||
if small then
|
||||
@@ -1541,7 +1543,7 @@ W.light = function(ctx, w, hh)
|
||||
local iw, ih = w - 2 * pad, hh - 2 * pad
|
||||
if not lt then
|
||||
return vstack({ w = w, h = hh, pad = pad }, {
|
||||
head(iw, _("Light"), icon("sun", px(16), { sw = 2.25 })),
|
||||
head(iw, _("Light"), icon("sun", px(16), BoardColour.icon(ctx, 2.25))),
|
||||
space(nil, px(6)),
|
||||
hint(_("This device has no frontlight."), iw, hh - 2 * pad - lh1(13) - px(6), { sizes = { 12, 11 } }),
|
||||
})
|
||||
@@ -1549,7 +1551,7 @@ W.light = function(ctx, w, hh)
|
||||
local function compact(bd, s)
|
||||
return function()
|
||||
return vstack({ w = w, h = hh, pad = pad }, {
|
||||
head(iw, _("Light"), icon("sun", px(16), { sw = 2.25 })),
|
||||
head(iw, _("Light"), icon("sun", px(16), BoardColour.icon(ctx, 2.25))),
|
||||
flex(0),
|
||||
hstack({ w = iw }, {
|
||||
BoardUI.hit(stepButton(bd, "minus"), { kind = "light", what = "brightness", delta = -5 }),
|
||||
@@ -1565,13 +1567,13 @@ W.light = function(ctx, w, hh)
|
||||
local avail = ih - lh1(13) - px(6)
|
||||
local rh = math.min(px(38), math.floor((avail - (rows_n - 1) * px(5)) / rows_n))
|
||||
local bd = math.min(px(32), rh)
|
||||
local function stepper(name, val, what)
|
||||
local function stepper(name, val, what, fam)
|
||||
local vw = px(36)
|
||||
local bw = iw - px(18) - 2 * bd - vw - 4 * px(8)
|
||||
return hstack({ w = iw, h = rh, gap = px(8) }, {
|
||||
icon(name, px(18), { sw = 2.25 }),
|
||||
icon(name, px(18), BoardColour.icon(ctx, 2.25, fam)),
|
||||
BoardUI.hit(stepButton(bd, "minus"), { kind = "light", what = what, delta = -5 }),
|
||||
bar(math.max(px(16), bw), px(8), val, { track = INK.gd, r = px(4) }),
|
||||
bar(math.max(px(16), bw), px(8), val, { track = BoardColour.track(ctx, INK.gd, fam), fill = BoardColour.ink(ctx, INK.black, fam), r = px(4) }),
|
||||
BoardUI.hit(stepButton(bd, "plus"), { kind = "light", what = what, delta = 5 }),
|
||||
text(tostring(val), { s = 13, b = true, w = vw, align = "right" }),
|
||||
})
|
||||
@@ -1580,9 +1582,9 @@ W.light = function(ctx, w, hh)
|
||||
return vstack({ w = w, h = hh, pad = pad }, {
|
||||
head(iw, _("Light"), with_label and text(label, { s = 12, col = INK.g4 }) or nil),
|
||||
flex(0),
|
||||
stepper("sun", lt.brightness, "brightness"),
|
||||
stepper("sun", lt.brightness, "brightness", "gold"),
|
||||
rows_n == 2 and space(nil, px(5)) or false,
|
||||
rows_n == 2 and stepper("thermometer-sun", lt.warmth, "warmth") or false,
|
||||
rows_n == 2 and stepper("thermometer-sun", lt.warmth, "warmth", "orange") or false,
|
||||
})
|
||||
end
|
||||
end
|
||||
@@ -1602,7 +1604,7 @@ W.forecast = function(ctx, w, hh)
|
||||
for _i, long in ipairs({ true, false }) do
|
||||
table.insert(variants, function()
|
||||
return vstack({ w = w, h = hh, pad = pad }, {
|
||||
head(iw, long and _("Finish forecast") or _("Finish"), icon("hourglass", px(16), { sw = 2.25 })),
|
||||
head(iw, long and _("Finish forecast") or _("Finish"), icon("hourglass", px(16), BoardColour.icon(ctx, 2.25))),
|
||||
space(nil, px(6)),
|
||||
text(long and _("Read a little on this device and a finish date shows up here.") or _("Read a little"), { s = 13, col = INK.g4, lines = 3, w = iw }),
|
||||
})
|
||||
@@ -1619,7 +1621,7 @@ W.forecast = function(ctx, w, hh)
|
||||
local is_end, is_today = i == n, i == 1
|
||||
local t = d.now + (i - 1) * 86400
|
||||
table.insert(cells, vstack({ w = cw, align = "center" }, {
|
||||
rect(cw, px(8), is_end and INK.black or (is_today and INK.g6 or INK.gc), px(4)),
|
||||
rect(cw, px(8), is_end and BoardColour.ink(ctx, INK.black) or (is_today and BoardColour.mid(ctx, INK.g6) or BoardColour.track(ctx, INK.gc)), px(4)),
|
||||
space(nil, px(3)),
|
||||
text(BoardData.shortWeekday(t):sub(1, 1), { s = 11, b = is_end or is_today, col = (is_end or is_today) and INK.black or INK.g4, w = cw, align = "center" }),
|
||||
}))
|
||||
@@ -1665,7 +1667,7 @@ W.forecast = function(ctx, w, hh)
|
||||
local cover_w = math.floor(cover_h * 2 / 3 + 0.5)
|
||||
local tw = iw - cover_w - px(12)
|
||||
return vstack({ w = w, h = hh, pad = pad }, {
|
||||
head(iw, cfg.label, icon("hourglass", px(16), { sw = 2.25 })),
|
||||
head(iw, cfg.label, icon("hourglass", px(16), BoardColour.icon(ctx, 2.25))),
|
||||
space(nil, px(8)),
|
||||
hstack({ gap = px(12), valign = "bottom" }, {
|
||||
cover(ctx, f.book, cover_w, cover_h),
|
||||
@@ -1678,7 +1680,7 @@ W.forecast = function(ctx, w, hh)
|
||||
})
|
||||
end
|
||||
return vstack({ w = w, h = hh, pad = pad }, {
|
||||
head(iw, cfg.label, icon("hourglass", px(16), { sw = 2.25 })),
|
||||
head(iw, cfg.label, icon("hourglass", px(16), BoardColour.icon(ctx, 2.25))),
|
||||
flex(0),
|
||||
dayEl(cfg.day, iw),
|
||||
cfg.strip and space(nil, px(8)) or false, cfg.strip and strip(iw) or false,
|
||||
@@ -1712,7 +1714,7 @@ W.word = function(ctx, w, hh)
|
||||
local wd = ctx.d.word
|
||||
local pad = px(12)
|
||||
local iw = w - 2 * pad
|
||||
local word_icon = icon("whole-word", px(17), { sw = 2.25 })
|
||||
local word_icon = icon("whole-word", px(17), BoardColour.icon(ctx, 2.25))
|
||||
if not wd then
|
||||
local variants = {}
|
||||
for _i, long in ipairs({ true, false }) do
|
||||
@@ -1733,7 +1735,7 @@ W.word = function(ctx, w, hh)
|
||||
local items = {
|
||||
head(iw, _("Word of the day"), word_icon),
|
||||
space(nil, px(cfg.big and 10 or 4)),
|
||||
text(wd.word, { s = cfg.ws, b = true, serif = true, w = iw, data = true }),
|
||||
text(wd.word, { s = cfg.ws, b = true, serif = true, w = iw, data = true, col = BoardColour.ink(ctx, INK.black) }),
|
||||
}
|
||||
if cfg.ctx_lines > 0 then
|
||||
table.insert(items, space(nil, px(4)))
|
||||
@@ -1768,7 +1770,7 @@ W.yearago = function(ctx, w, hh)
|
||||
local y = ctx.d.year_ago
|
||||
local pad = px(12)
|
||||
local iw, ih = w - 2 * pad, hh - 2 * pad
|
||||
local cal = icon("calendar-clock", px(17), { sw = 2.25 })
|
||||
local cal = icon("calendar-clock", px(17), BoardColour.icon(ctx, 2.25))
|
||||
if not y then
|
||||
local variants = {}
|
||||
for _i, long in ipairs({ true, false }) do
|
||||
@@ -1862,7 +1864,8 @@ end
|
||||
-- the outline never costs the content any room.
|
||||
function BoardWidgets.tile(ctx, item, w, h)
|
||||
local fn = W[item.type]
|
||||
local ch = BoardWidgets.chrome(ctx.tiles, item.chrome)
|
||||
ctx.tile_key = BoardColour.keyFor(item.type, item.props)
|
||||
local ch = BoardColour.chrome(ctx, item.chrome, ctx.tile_key, BoardWidgets.chrome(ctx.tiles, item.chrome), math.max(1, px(1)), math.max(1, px(1.5)))
|
||||
local content
|
||||
if fn then
|
||||
BoardUI.debug_widget = item.type .. " " .. w .. "x" .. h
|
||||
|
||||
@@ -24,6 +24,7 @@ local logger = require("logger")
|
||||
local T = require("ffi/util").template
|
||||
local _ = require("gettext")
|
||||
|
||||
local BoardColour = require("bookorbit_board_colour")
|
||||
local BoardData = require("bookorbit_board_data")
|
||||
local BoardEdit = require("bookorbit_board_edit")
|
||||
local BoardLayout = require("bookorbit_board_layout")
|
||||
@@ -367,7 +368,7 @@ function CatalogBoard:boardPreview(ctx, entry, box_w, box_h, g)
|
||||
local item = { type = entry.type, cw = cw, rh = rh, chrome = entry.chrome or "line", props = entry.props }
|
||||
local key = "preview:" .. entry.type .. ":" .. tostring(entry.props and (entry.props.source or entry.props.link or entry.props.animal) or "") .. ":" .. pw .. "x" .. ph .. ":" .. sw
|
||||
local shot = BoardUI.snapshot(key, pw, ph, sw, sh, function()
|
||||
return BoardWidgets.tile({ d = ctx.d, tiles = "outline", menu = self }, item, pw, ph)
|
||||
return BoardWidgets.tile({ d = ctx.d, tiles = "outline", colour = ctx.colour, menu = self }, item, pw, ph)
|
||||
end)
|
||||
return BoardUI.box({ w = box_w, h = box_h, radius = px(12), bg = INK.ge }, hstack({ w = box_w, h = box_h }, { flex(0), shot, flex(0) }))
|
||||
end
|
||||
@@ -734,7 +735,7 @@ function CatalogBoard:updateDashboardItems(select_number, no_recalculate_dimen)
|
||||
local page = self:boardPage()
|
||||
local d = BoardData.build(self, context, os.time())
|
||||
local state = BoardStore.read(self.settings)
|
||||
local ctx = { d = d, tiles = state.tiles, menu = self, tap_resumes = self.settings.catalog_dashboard_tap_resumes ~= false }
|
||||
local ctx = { d = d, tiles = state.tiles, colour = state.colour, menu = self, tap_resumes = self.settings.catalog_dashboard_tap_resumes ~= false }
|
||||
self._board_ctx = ctx
|
||||
self._board_geometry = g
|
||||
local t_data = PROFILE and os.clock()
|
||||
@@ -782,11 +783,18 @@ function CatalogBoard:updateDashboardItems(select_number, no_recalculate_dimen)
|
||||
self:boardRefreshPets()
|
||||
end
|
||||
|
||||
-- The pets are colour pictures. A colour screen shows them in full colour only
|
||||
-- through a dithered refresh (on Kaleido, its colour waveform), and the
|
||||
-- board's own "ui" refresh is not one, so each pet tile gets its own.
|
||||
-- A colour screen shows colour only through a dithered refresh (on Kaleido,
|
||||
-- its colour waveform), and the board's own "ui" refresh is not one. With the
|
||||
-- Colour setting on, the whole board gets one; with it off, only the pets,
|
||||
-- which are colour pictures, get their own.
|
||||
function CatalogBoard:boardRefreshPets()
|
||||
if not Screen:isColorEnabled() then return end
|
||||
if BoardColour.on(self._board_ctx) then
|
||||
UIManager:setDirty(nil, function()
|
||||
if self.dimen and self.dimen.x then return "partial", self.dimen, true end
|
||||
end)
|
||||
return
|
||||
end
|
||||
for _i, entry in ipairs(self._board_tiles or {}) do
|
||||
if entry.item.type == "buddy" then
|
||||
local tile = entry.tile
|
||||
@@ -798,9 +806,16 @@ function CatalogBoard:boardRefreshPets()
|
||||
end
|
||||
|
||||
-- Repaints one tile in place: rebuilds its content and refreshes only its
|
||||
-- rectangle, so a stepper or a hero page does not flash the whole board.
|
||||
-- rectangle, so a stepper or a hero page does not flash the whole board. A
|
||||
-- coloured board's tiles keep their colours through the dithered refresh.
|
||||
function CatalogBoard:boardRefreshTile(entry, mode, dither)
|
||||
if not entry or not self._board_ctx then return end
|
||||
-- The digital clock is black and white unless tiles are tinted, so its
|
||||
-- minute stays on the fast refresh.
|
||||
local plain_clock = entry.item.type == "clock" and not BoardColour.tinted(self._board_ctx)
|
||||
if BoardColour.on(self._board_ctx) and not plain_clock and (mode == nil or mode == "ui" or mode == "fast") then
|
||||
mode, dither = "partial", true
|
||||
end
|
||||
self._board_ctx.d = BoardData.build(self, self.current_context or {}, os.time())
|
||||
local fresh = self:boardBuildTile(self._board_ctx, entry.item, entry.w, entry.h)
|
||||
entry.tile[1] = fresh[1]
|
||||
|
||||
@@ -564,6 +564,15 @@ function MainMenu:applyBoardTiles(catalog, tiles)
|
||||
end
|
||||
end
|
||||
|
||||
function MainMenu:applyBoardColour(catalog, colour)
|
||||
if catalog and catalog.boardSetColour then
|
||||
catalog:boardSetColour(colour)
|
||||
else
|
||||
self.settings[BoardStore.SETTING_KEY] = BoardStore.withColour(self.settings, colour)
|
||||
G_reader_settings:flush()
|
||||
end
|
||||
end
|
||||
|
||||
function MainMenu:chooseDownloadFolder(catalog, on_done)
|
||||
if catalog and catalog.chooseDownloadDir then
|
||||
catalog:chooseDownloadDir(on_done)
|
||||
|
||||
@@ -33,6 +33,7 @@ local STRATEGY = { SILENT = 2, PROMPT = 1, DISABLE = 3 }
|
||||
local STRATEGY_ORDER = { STRATEGY.SILENT, STRATEGY.PROMPT, STRATEGY.DISABLE }
|
||||
local PRESETS = { "reader", "stats", "library" }
|
||||
local TILES = { "outline", "soft", "ink" }
|
||||
local COLOURS = BoardStore.COLOURS
|
||||
|
||||
local function scaleLabel(value)
|
||||
return string.format("%.2f", value):gsub("0$", "") .. "x"
|
||||
@@ -297,19 +298,54 @@ local function homeBody(page, w, embedded)
|
||||
})
|
||||
local preset_labels = { _("Reader"), _("Stats"), _("Library") }
|
||||
local tile_labels = { _("Outline"), _("Soft"), _("Ink") }
|
||||
-- Colour only on a screen that shows it.
|
||||
local colour_labels = Device.screen:isColorEnabled() and { _("Accents"), _("Tinted"), _("Off") } or nil
|
||||
local function colourRow(rw, tight)
|
||||
local row = segRow(rw, _("Colour"), nil, colour_labels, indexOf(COLOURS, state.colour, 1),
|
||||
function(index) return { kind = "colour", value = COLOURS[index] } end, tight)
|
||||
row._no_icon = true
|
||||
return row
|
||||
end
|
||||
-- Colour sits beside Tile style when half a row holds both controls'
|
||||
-- labels, so the page keeps the height it has on a black-and-white screen.
|
||||
local function pairsWithTiles(half)
|
||||
local seg = math.floor((half - 2 * math.max(1, px(1.5))) / 3) - px(8)
|
||||
for _i, list in ipairs({ tile_labels, colour_labels }) do
|
||||
for _j, label in ipairs(list) do
|
||||
local t = text(label, { s = 14, b = true })
|
||||
local wide = t:getSize().w > seg
|
||||
t:free()
|
||||
if wide then return false end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
local scale_labels = {}
|
||||
for index, scale in ipairs(BoardMetrics.UI_SCALES) do
|
||||
scale_labels[index] = scaleLabel(scale)
|
||||
end
|
||||
local a = segRow(iw, _("Starting layout"), _("Replaces your pages with a starter set"), preset_labels, indexOf(PRESETS, state.preset, 1),
|
||||
function(index) return { kind = "preset", value = PRESETS[index] } end)
|
||||
local b = segRow(iw, _("Tile style"), nil, tile_labels, indexOf(TILES, state.tiles, 1),
|
||||
local look_gap = px(16)
|
||||
local look_half = math.floor((iw - look_gap) / 2)
|
||||
local pair_colour = colour_labels ~= nil and not page.compact and pairsWithTiles(look_half)
|
||||
local b = segRow(pair_colour and look_half or iw, _("Tile style"), nil, tile_labels, indexOf(TILES, state.tiles, 1),
|
||||
function(index) return { kind = "tiles", value = TILES[index] } end)
|
||||
local c = segRow(iw, _("Interface size"), _("Text and controls across BookOrbit"), scale_labels,
|
||||
indexOf(BoardMetrics.UI_SCALES, BoardMetrics.normalizeUserScale(settings.ui_scale), 1),
|
||||
function(index) return { kind = "ui-scale", value = BoardMetrics.UI_SCALES[index] } end)
|
||||
a._no_icon, b._no_icon, c._no_icon = true, true, true
|
||||
local look = MenuUI.group(w, { a, b, c }, { inset = 0 })
|
||||
local look
|
||||
if pair_colour then
|
||||
local pair = hstack({ w = iw, gap = look_gap, valign = "top" }, { b, colourRow(iw - look_half - look_gap) })
|
||||
pair._no_icon = true
|
||||
look = MenuUI.group(w, { a, pair, c }, { inset = 0 })
|
||||
else
|
||||
look = MenuUI.group(w, { a, b, c }, { inset = 0 })
|
||||
end
|
||||
-- Too narrow to share Tile style's row: Colour is a card of its own after
|
||||
-- the first, which a two-column page can set beside Look.
|
||||
local colour_card = colour_labels and not pair_colour and MenuUI.group(w, { colourRow(iw, page.compact ~= nil) }, { inset = 0 }) or nil
|
||||
-- On a short, wide screen whose columns cannot hold the full card
|
||||
-- (page.compact, set by build): without the notes, and at level 1 the
|
||||
-- layout and tile style sharing a row when each half has room.
|
||||
@@ -341,6 +377,8 @@ local function homeBody(page, w, embedded)
|
||||
embedded and text(_("Home"), { s = 18, b = true, w = w }) or false,
|
||||
embedded and space(nil, px(12)) or false,
|
||||
top,
|
||||
colour_card and space(nil, px(16)) or false,
|
||||
colour_card or false,
|
||||
space(nil, px(16)),
|
||||
MenuUI.section(w, _("Look"), look),
|
||||
space(nil, px(16)),
|
||||
@@ -818,6 +856,9 @@ local function onAction(page, action)
|
||||
elseif kind == "tiles" then
|
||||
plugin:applyBoardTiles(catalog, action.value)
|
||||
page:refresh()
|
||||
elseif kind == "colour" then
|
||||
plugin:applyBoardColour(catalog, action.value)
|
||||
page:refresh()
|
||||
elseif kind == "ui-scale" then
|
||||
if settings.ui_scale == action.value then return true end
|
||||
local scale = BoardMetrics.setUserScale(action.value)
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
package.path = "koreader-plugin/bookorbit.koplugin/?.lua;" .. package.path
|
||||
|
||||
local BLACK = { grey = 0 }
|
||||
package.loaded["ffi/blitbuffer"] = {
|
||||
COLOR_BLACK = BLACK,
|
||||
ColorRGB32 = function(r, g, b, a) return { r = r, g = g, b = b, a = a } end,
|
||||
isColor8 = function(c) return c.grey ~= nil end,
|
||||
}
|
||||
package.loaded["gettext"] = function(value) return value end
|
||||
local pixels_of = {}
|
||||
package.loaded["ui/renderimage"] = {
|
||||
renderImageFile = function(_self, path)
|
||||
local list = pixels_of[path]
|
||||
if not list then return nil end
|
||||
return {
|
||||
getWidth = function() return #list end,
|
||||
getHeight = function() return 1 end,
|
||||
getPixel = function(_bb, x)
|
||||
local p = list[x + 1]
|
||||
return { getColorRGB32 = function() return { r = p[1], g = p[2], b = p[3] } end }
|
||||
end,
|
||||
free = function() end,
|
||||
}
|
||||
end,
|
||||
}
|
||||
|
||||
local Colour = require("bookorbit_board_colour")
|
||||
local Layout = require("bookorbit_board_layout")
|
||||
local Store = require("bookorbit_board_store")
|
||||
|
||||
local function hex(c) return string.format("#%02x%02x%02x", c.r, c.g, c.b) end
|
||||
local function rgb(s) return tonumber(s:sub(2, 3), 16), tonumber(s:sub(4, 5), 16), tonumber(s:sub(6, 7), 16) end
|
||||
local function chroma(s)
|
||||
local r, g, b = rgb(s)
|
||||
return math.max(r, g, b) - math.min(r, g, b)
|
||||
end
|
||||
local function luma(s)
|
||||
local r, g, b = rgb(s)
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||
end
|
||||
|
||||
-- Colour math.
|
||||
assert(Colour.mix("#000000", "#ffffff", 0.5) == "#808080", "mix halves")
|
||||
assert(Colour.boost("#777777", 1.4) == "#777777", "boost leaves a gray alone")
|
||||
assert(chroma(Colour.boost("#2b61d6", 1.4)) > chroma("#2b61d6"), "boost pushes chroma out")
|
||||
assert(math.abs(luma(Colour.boost("#2b61d6", 1.4)) - luma("#2b61d6")) < 3, "boost keeps luma")
|
||||
assert(Colour.forNight("#808080") == "#808080", "night leaves a gray alone")
|
||||
do
|
||||
-- Inverting what is painted for night shows the colour's own hue.
|
||||
local painted = Colour.forNight("#de4127")
|
||||
local r, g, b = rgb(painted)
|
||||
local hn = Colour.rgbToHls((255 - r) / 255, (255 - g) / 255, (255 - b) / 255)
|
||||
local r0, g0, b0 = rgb("#de4127")
|
||||
local h0, l0 = Colour.rgbToHls(r0 / 255, g0 / 255, b0 / 255)
|
||||
local _, lp = Colour.rgbToHls(r / 255, g / 255, b / 255)
|
||||
assert(math.abs(hn - h0) < 0.01, "night shows the same hue")
|
||||
assert(math.abs(lp - l0) < 0.01, "night paints at the same lightness")
|
||||
end
|
||||
|
||||
-- Cover families.
|
||||
local function fill(n, p) local t = {} for i = 1, n do t[i] = p end return t end
|
||||
assert(Colour.familyFromPixels(fill(100, { 128, 128, 128 })) == nil, "a gray jacket has no colour")
|
||||
assert(Colour.familyFromPixels(fill(100, { 250, 250, 250 })) == nil, "a white jacket has no colour")
|
||||
do
|
||||
local mostly_gray = fill(97, { 30, 30, 30 })
|
||||
for _i = 1, 3 do table.insert(mostly_gray, { 40, 90, 200 }) end
|
||||
assert(Colour.familyFromPixels(mostly_gray) == nil, "a few coloured pixels are not enough")
|
||||
local blue = fill(60, { 40, 90, 200 })
|
||||
for _i = 1, 40 do table.insert(blue, { 20, 20, 20 }) end
|
||||
local fam = Colour.familyFromPixels(blue)
|
||||
assert(fam, "a blue jacket has a family")
|
||||
assert(luma(fam.deep) < luma(fam.ink) and luma(fam.ink) < luma(fam.tint) and luma(fam.tint) < luma(fam.wash), "deep, ink, tint, wash get lighter")
|
||||
local r, g, b = rgb(fam.ink)
|
||||
assert(b > r and b > g, "the family keeps the jacket's hue")
|
||||
-- Deep carries white text.
|
||||
assert(luma(fam.deep) < 80, "deep is dark enough for white text")
|
||||
end
|
||||
pixels_of["/covers/ocean.jpg"] = fill(50, { 30, 120, 160 })
|
||||
pixels_of["/covers/plain.jpg"] = fill(50, { 200, 200, 200 })
|
||||
assert(Colour.coverFamily("/covers/ocean.jpg"), "a cover file gives its family")
|
||||
pixels_of["/covers/ocean.jpg"] = nil
|
||||
assert(Colour.coverFamily("/covers/ocean.jpg"), "a cover's family is kept")
|
||||
assert(Colour.coverFamily("/covers/plain.jpg") == nil and Colour.coverFamily("/covers/plain.jpg") == nil, "no family, asked twice")
|
||||
assert(Colour.coverFamily(nil) == nil, "no path, no family")
|
||||
|
||||
-- What reaches the screen.
|
||||
local GREY = { grey = 136 }
|
||||
local function ctx(scheme, extra)
|
||||
local c = { colour = scheme, tiles = "outline", d = { continue_reading = { { cover_path = "/covers/ocean.jpg" } } } }
|
||||
for k, v in pairs(extra or {}) do c[k] = v end
|
||||
return c
|
||||
end
|
||||
local LCD, EINK = { colour = true, eink = false, night = false }, { colour = true, eink = true, night = false }
|
||||
local MONO = { colour = false, eink = true, night = false }
|
||||
|
||||
Colour._env = MONO
|
||||
for _, scheme in ipairs(Colour.SCHEMES) do
|
||||
local c = ctx(scheme, { tile_key = "streak" })
|
||||
assert(Colour.ink(c, GREY) == GREY and Colour.track(c, GREY) == GREY and Colour.mid(c, GREY) == GREY, scheme .. ": a black-and-white screen keeps its grays")
|
||||
assert(Colour.inkFill(c, "hero") == BLACK, scheme .. ": Continue reading stays black there")
|
||||
local base = { bg = GREY, border = 1, border_col = GREY }
|
||||
assert(Colour.chrome(c, "line", "highlight", base, 1, 2) == base, scheme .. ": tiles unchanged there")
|
||||
assert(Colour.icon(c, 2.25).col == nil and Colour.icon(c, 2.25).sw == 2.25, scheme .. ": icons unchanged there")
|
||||
end
|
||||
|
||||
Colour._env = LCD
|
||||
do
|
||||
local off = ctx("off", { tile_key = "streak" })
|
||||
assert(Colour.ink(off, GREY) == GREY, "Off keeps the grays on a colour screen")
|
||||
local acc = ctx("accents", { tile_key = "streak" })
|
||||
assert(hex(Colour.ink(acc, GREY)) == Colour.FAM.orange.ink, "Streak is orange")
|
||||
assert(hex(Colour.track(acc, GREY)) == Colour.FAM.orange.tint, "a bar runs on its own tint")
|
||||
assert(Colour.paint("#123456") == Colour.paint("#123456"), "one object per colour")
|
||||
assert(Colour.icon(acc, 2.25).sw == 2.25, "a phone keeps icon weights")
|
||||
-- Accents: tiles as they are, the highlight yellow, Continue reading in its cover.
|
||||
local base = { bg = GREY, border = 1, border_col = GREY, radius = 4 }
|
||||
assert(Colour.chrome(acc, "line", "streak", base, 1, 2) == base, "Accents keeps a tile's own chrome")
|
||||
local hl = Colour.chrome(acc, "fill", "highlight", base, 1, 2)
|
||||
assert(hex(hl.bg) == Colour.FAM.yellow.wash and hl.border == 0 and hl.radius == 4, "Highlight of the day is a yellow card")
|
||||
local lead = Colour.coverFamily("/covers/ocean.jpg")
|
||||
assert(hex(Colour.chrome(acc, "ink", "hero", base, 1, 2).bg) == lead.deep, "Continue reading takes its cover's deep shade")
|
||||
assert(Colour.chrome(acc, "ink", "clock", base, 1, 2).bg == BLACK, "the clock stays black in Accents")
|
||||
assert(Colour.chrome(acc, "ink", "moon", base, 1, 2).bg == BLACK, "the moon stays black in Accents")
|
||||
assert(Colour.onInk(acc, GREY, "clock") == GREY, "quiet text on black stays gray")
|
||||
assert(Colour.onInk(acc, GREY, "hero") ~= GREY, "quiet text on the cover's shade takes a pale shade")
|
||||
local plain = ctx("accents", { d = { continue_reading = { { cover_path = "/covers/plain.jpg" } } } })
|
||||
assert(Colour.inkFill(plain, "hero") == BLACK, "a jacket without colour keeps the black tile")
|
||||
-- Tinted: the wash, with the tile style's edge in the widget's colour.
|
||||
local outline = ctx("tinted", { tile_key = "week" })
|
||||
local ch = Colour.chrome(outline, "line", "week", base, 1, 2)
|
||||
assert(hex(ch.bg) == Colour.FAM.green.wash and ch.border == 1 and hex(ch.border_col) == Colour.mix(Colour.FAM.green.wash, Colour.FAM.green.ink, 0.4), "Outline: wash and a hairline")
|
||||
local soft = Colour.chrome(ctx("tinted", { tiles = "soft" }), "line", "week", base, 1, 2)
|
||||
assert(hex(soft.bg) == Colour.FAM.green.wash and soft.border == 0, "Soft: the wash alone")
|
||||
local ink = Colour.chrome(ctx("tinted", { tiles = "ink" }), "line", "week", base, 1, 2)
|
||||
assert(ink.border == 2 and hex(ink.border_col) == Colour.FAM.green.deep, "Ink: a firm deep edge")
|
||||
assert(hex(Colour.chrome(outline, "ink", "clock", base, 1, 2).bg) == Colour.FAM.blue.deep, "the clock goes deep blue when tinted")
|
||||
assert(hex(Colour.track(outline, GREY)) == "#ffffff", "bars run on white in a washed tile")
|
||||
assert(hex(Colour.halo(outline, GREY)) == Colour.FAM.green.wash, "fanned covers sit in the wash")
|
||||
assert(Colour.halo(acc, GREY) == GREY, "and keep their white halo on white tiles")
|
||||
assert(Colour.chrome(outline, "open", "week", base, 1, 2) == base, "open tiles stay open")
|
||||
end
|
||||
|
||||
Colour._env = EINK
|
||||
do
|
||||
local acc = ctx("accents", { tile_key = "streak" })
|
||||
local ink = hex(Colour.ink(acc, GREY))
|
||||
assert(ink == Colour.boost(Colour.FAM.orange.ink, 1.4), "a colour e-reader gets the boosted shade")
|
||||
assert(Colour.icon(acc, 2.25).sw > 2.25, "and heavier icons")
|
||||
end
|
||||
|
||||
Colour._env = { colour = true, eink = false, night = true }
|
||||
do
|
||||
local acc = ctx("accents", { tile_key = "streak" })
|
||||
assert(hex(Colour.ink(acc, GREY)) == Colour.forNight(Colour.FAM.orange.ink), "night mode paints the turned hue")
|
||||
assert(Colour.icon(acc, 2.25).col == Colour.forNight(Colour.FAM.orange.ink), "icons too")
|
||||
end
|
||||
Colour._env = nil
|
||||
|
||||
-- The setting.
|
||||
assert(Store.read({}).colour == "accents", "Accents by default")
|
||||
assert(Store.read({ catalog_board = { version = 1, colour = "loud" } }).colour == "accents", "an unknown value reads as Accents")
|
||||
assert(Store.withColour({}, "tinted").colour == "tinted", "Tinted is kept")
|
||||
assert(Store.withColour({ catalog_board = { version = 1, tiles = "ink", colour = "tinted" } }, "off").tiles == "ink", "changing colour keeps the tile style")
|
||||
assert(Colour.normalize(nil) == "accents" and Colour.normalize("off") == "off", "normalize")
|
||||
|
||||
-- No two touching tiles on a starter page share a colour or a look-alike
|
||||
-- wash, on any screen; a dark ink tile may touch a pale one of its family.
|
||||
local FIXED = { highlight = true, hero = true, chapters = true, clock = true, analog = true, moon = true }
|
||||
local ALIKE = { gold = "yellow", yellow = "gold", rose = "red", red = "rose", teal = "green", green = "teal" }
|
||||
local function clash(a, b) return a == b or ALIKE[a] == b end
|
||||
for _, cols in ipairs({ 2, 4, 6 }) do
|
||||
for _, landscape in ipairs({ false, true }) do
|
||||
for _, has_light in ipairs({ true, false }) do
|
||||
for _, page in ipairs(Store.starterPages(cols, "reader", has_light, landscape)) do
|
||||
local packed = Layout.pack(page.base, cols)
|
||||
local at = {}
|
||||
for _, it in ipairs(packed.placed) do
|
||||
for y = it.y, it.y + it.rh - 1 do
|
||||
for x = it.x, it.x + it.cw - 1 do
|
||||
at[y .. ":" .. x] = it
|
||||
end
|
||||
end
|
||||
end
|
||||
local function fam(it)
|
||||
local key = Colour.keyFor(it.type, it.props)
|
||||
return Colour.HUE[key]
|
||||
end
|
||||
for _, it in ipairs(packed.placed) do
|
||||
for y = it.y, it.y + it.rh - 1 do
|
||||
for _, other in ipairs({ at[y .. ":" .. (it.x + it.cw)] }) do
|
||||
if other and other ~= it and not FIXED[it.type] and not FIXED[other.type] then
|
||||
assert(not clash(fam(it), fam(other)), string.format("%d cols%s %s: %s beside %s", cols, landscape and " landscape" or "", page.id, it.type, other.type))
|
||||
end
|
||||
end
|
||||
end
|
||||
for x = it.x, it.x + it.cw - 1 do
|
||||
local other = at[(it.y + it.rh) .. ":" .. x]
|
||||
if other and other ~= it and not FIXED[it.type] and not FIXED[other.type] then
|
||||
assert(not clash(fam(it), fam(other)), string.format("%d cols%s %s: %s above %s", cols, landscape and " landscape" or "", page.id, it.type, other.type))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
print("bookorbit_board_colour_test: ok")
|
||||
Reference in New Issue
Block a user