From ce180875527780fe1d8cdf75d4372ceec727d8a4 Mon Sep 17 00:00:00 2001 From: John Factotum <50942278+johnfactotum@users.noreply.github.com> Date: Mon, 9 Oct 2023 13:32:26 +0800 Subject: [PATCH] Load custom themes from config dir --- src/book-viewer.js | 98 +---------------------------------- src/library.js | 28 ++++------ src/themes.js | 124 +++++++++++++++++++++++++++++++++++++++++++++ src/utils.js | 13 +++++ 4 files changed, 147 insertions(+), 116 deletions(-) create mode 100644 src/themes.js diff --git a/src/book-viewer.js b/src/book-viewer.js index 95718541..1ebc618c 100644 --- a/src/book-viewer.js +++ b/src/book-viewer.js @@ -23,6 +23,7 @@ import { SelectionPopover } from './selection-tools.js' import { ImageViewer } from './image-viewer.js' import { formatAuthors, makeBookInfoWindow } from './book-info.js' import { getURIStore, getBookList } from './library.js' +import { themes, invertTheme } from './themes.js' // for use in the WebView const uiText = { @@ -31,103 +32,6 @@ const uiText = { goToFootnote: _('Go to Footnote'), } -const themes = [ - { - name: 'default', label: _('Default'), - light: { fg: '#000000', bg: '#ffffff', link: '#0066cc' }, - dark: { fg: '#e0e0e0', bg: '#222222', link: '#88ccee' }, - }, - { - name: 'gray', label: _('Gray'), - light: { fg: '#222222', bg: '#e0e0e0', link: '#4488cc' }, - dark: { fg: '#c6c6c6', bg: '#444444', link: '#88ccee' }, - }, - { - name: 'sepia', label: _('Sepia'), - light: { fg: '#5b4636', bg: '#f1e8d0', link: '#008b8b' }, - dark: { fg: '#ffd595', bg: '#342e25', link: '#48d1cc' }, - }, - { - name: 'grass', label: _('Grass'), - light: { fg: '#232c16', bg: '#d7dbbd', link: '#177b4d' }, - dark: { fg: '#d8deba', bg: '#333627', link: '#a6d608' }, - }, - { - name: 'cherry', label: _('Cherry'), - light: { fg: '#4e1609', bg: '#f0d1d5', link: '#de3838' }, - dark: { fg: '#e5c4c8', bg: '#462f32', link: '#ff646e' }, - }, - { - name: 'sky', label: _('Sky'), - light: { fg: '#262d48', bg: '#cedef5', link: '#2d53e5' }, - dark: { fg: '#babee1', bg: '#282e47', link: '#ff646e' }, - }, - { - name: 'solarized', label: _('Solarized'), - light: { fg: '#586e75', bg: '#fdf6e3', link: '#268bd2' }, - dark: { fg: '#93a1a1', bg: '#002b36', link: '#268bd2' }, - }, - { - name: 'gruvbox', label: _('Gruvbox'), - light: { fg: '#3c3836', bg: '#fbf1c7', link: '#076678' }, - dark: { fg: '#ebdbb2', bg: '#282828', link: '#83a598' }, - }, - { - name: 'nord', label: _('Nord'), - light: { fg: '#2e3440', bg: '#eceff4', link: '#5e81ac' }, - dark: { fg: '#d8dee9', bg: '#2e3440', link: '#88c0d0' }, - }, -] - -const themeCssProvider = new Gtk.CssProvider() -themeCssProvider.load_from_data(` - .theme-container .card { - padding: 9px; - } -` + themes.map(theme => { - const id = `theme-${GLib.uuid_string_random()}` - theme.id = id - // NOTE: .92 matches Libadwaita's sidebar color when the bg is white - // in dark mode it should use 1.41 to match Libadwaita - // but it seems better to tone it down as many dark themes are quite bright - return ` - .${id} { - color: ${theme.light.fg}; - background: ${theme.light.bg}; - } - .is-dark .${id} { - color: ${theme.dark.fg}; - background: ${theme.dark.bg}; - } - .sidebar-${id}:not(.background) { - color: ${theme.light.fg}; - background: shade(${theme.light.bg}, .92); - } - .is-dark .sidebar-${id}:not(.background) { - color: ${theme.dark.fg}; - background: shade(${theme.dark.bg}, 1.2); - } - .${id} highlight { - background: ${theme.light.link}; - } - .is-dark .${id} highlight { - background: ${theme.dark.link}; - } - .${id} popover highlight, .is-dark .${id} popover highlight { - background: @accent_bg_color; - } - ` -}).join(''), -1) -Gtk.StyleContext.add_provider_for_display( - Gdk.Display.get_default(), - themeCssProvider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) - -const invertTheme = ({ light, dark }) => ({ light, dark, inverted: { - fg: utils.invertColor(dark.fg), - link: utils.invertColor(dark.link), -} }) - const userStylesheet = utils.readFile(Gio.File.new_for_path( pkg.configpath('user-stylesheet.css'))) diff --git a/src/library.js b/src/library.js index 388c1ec0..f2cfe940 100644 --- a/src/library.js +++ b/src/library.js @@ -14,24 +14,14 @@ import { formatAuthors, makeBookInfoWindow } from './book-info.js' const showCovers = utils.settings('library')?.get_boolean('show-covers') ?? true -const listDir = function* (path) { - const dir = Gio.File.new_for_path(path) - if (!GLib.file_test(path, GLib.FileTest.IS_DIR)) return null - const children = dir.enumerate_children('standard::name,time::modified', - Gio.FileQueryInfoFlags.NONE, null) - let info - while ((info = children.next_file(null)) != null) { - try { - const name = info.get_name() - if (!/\.json$/.test(name)) continue - const child = dir.get_child(name) - yield { - file: child, - modified: new Date(info.get_attribute_uint64('time::modified') * 1000), - } - } catch (e) { - continue - } +const listBooks = function* (path) { + const ls = utils.listDir(path, 'standard::name,time::modified') + for (const { file, name, info } of ls) try { + if (!/\.json$/.test(name)) continue + const modified = new Date(info.get_attribute_uint64('time::modified') * 1000) + yield { file, modified } + } catch (e) { + console.error(e) } } @@ -57,7 +47,7 @@ const BookList = GObject.registerClass({ GTypeName: 'FoliateBookList', }, class extends Gio.ListStore { #uriStore = getURIStore() - #files = Array.from(listDir(pkg.datadir) ?? []) + #files = Array.from(listBooks(pkg.datadir) ?? []) .sort((a, b) => b.modified - a.modified) .map(x => x.file) #iter = this.#files.values() diff --git a/src/themes.js b/src/themes.js new file mode 100644 index 00000000..555cef7b --- /dev/null +++ b/src/themes.js @@ -0,0 +1,124 @@ +import Gtk from 'gi://Gtk' +import GLib from 'gi://GLib' +import Gdk from 'gi://Gdk' +import { gettext as _ } from 'gettext' +import * as utils from './utils.js' + +export const themes = [ + { + name: 'default', label: _('Default'), + light: { fg: '#000000', bg: '#ffffff', link: '#0066cc' }, + dark: { fg: '#e0e0e0', bg: '#222222', link: '#77bbee' }, + }, + { + name: 'gray', label: _('Gray'), + light: { fg: '#222222', bg: '#e0e0e0', link: '#4488cc' }, + dark: { fg: '#c6c6c6', bg: '#444444', link: '#88ccee' }, + }, + { + name: 'sepia', label: _('Sepia'), + light: { fg: '#5b4636', bg: '#f1e8d0', link: '#008b8b' }, + dark: { fg: '#ffd595', bg: '#342e25', link: '#48d1cc' }, + }, + { + name: 'grass', label: _('Grass'), + light: { fg: '#232c16', bg: '#d7dbbd', link: '#177b4d' }, + dark: { fg: '#d8deba', bg: '#333627', link: '#a6d608' }, + }, + { + name: 'cherry', label: _('Cherry'), + light: { fg: '#4e1609', bg: '#f0d1d5', link: '#de3838' }, + dark: { fg: '#e5c4c8', bg: '#462f32', link: '#ff646e' }, + }, + { + name: 'sky', label: _('Sky'), + light: { fg: '#262d48', bg: '#cedef5', link: '#2d53e5' }, + dark: { fg: '#babee1', bg: '#282e47', link: '#ff646e' }, + }, + { + name: 'solarized', label: _('Solarized'), + light: { fg: '#586e75', bg: '#fdf6e3', link: '#268bd2' }, + dark: { fg: '#93a1a1', bg: '#002b36', link: '#268bd2' }, + }, + { + name: 'gruvbox', label: _('Gruvbox'), + light: { fg: '#3c3836', bg: '#fbf1c7', link: '#076678' }, + dark: { fg: '#ebdbb2', bg: '#282828', link: '#83a598' }, + }, + { + name: 'nord', label: _('Nord'), + light: { fg: '#2e3440', bg: '#eceff4', link: '#5e81ac' }, + dark: { fg: '#d8dee9', bg: '#2e3440', link: '#88c0d0' }, + }, +] + +for (const { file, name } of utils.listDir(pkg.configpath('themes'))) try { + if (!/\.json$/.test(name)) continue + const name_ = name.replace(/\.json$/) + const theme = utils.readJSONFile(file) + themes.push({ + name: name_, + label: theme.label ?? name_, + light: { + fg: theme.light.fg, + bg: theme.light.bg, + link: theme.light.link, + }, + dark: { + fg: theme.dark.fg, + bg: theme.dark.bg, + link: theme.dark.link, + }, + }) +} catch (e) { + console.error(e) +} + +const themeCssProvider = new Gtk.CssProvider() +themeCssProvider.load_from_data(` + .theme-container .card { + padding: 9px; + } +` + themes.map(theme => { + const id = `theme-${GLib.uuid_string_random()}` + theme.id = id + // NOTE: .92 matches Libadwaita's sidebar color when the bg is white + // in dark mode it should use 1.41 to match Libadwaita + // but it seems better to tone it down as many dark themes are quite bright + return ` + .${id} { + color: ${theme.light.fg}; + background: ${theme.light.bg}; + } + .is-dark .${id} { + color: ${theme.dark.fg}; + background: ${theme.dark.bg}; + } + .sidebar-${id}:not(.background) { + color: ${theme.light.fg}; + background: shade(${theme.light.bg}, .92); + } + .is-dark .sidebar-${id}:not(.background) { + color: ${theme.dark.fg}; + background: shade(${theme.dark.bg}, 1.2); + } + .${id} highlight { + background: ${theme.light.link}; + } + .is-dark .${id} highlight { + background: ${theme.dark.link}; + } + .${id} popover highlight, .is-dark .${id} popover highlight { + background: @accent_bg_color; + } + ` +}).join(''), -1) +Gtk.StyleContext.add_provider_for_display( + Gdk.Display.get_default(), + themeCssProvider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + +export const invertTheme = ({ light, dark }) => ({ light, dark, inverted: { + fg: utils.invertColor(dark.fg), + link: utils.invertColor(dark.link), +} }) diff --git a/src/utils.js b/src/utils.js index a82b4de1..a4cf1062 100644 --- a/src/utils.js +++ b/src/utils.js @@ -38,6 +38,19 @@ export const debounce = (f, wait, immediate) => { } } +export const listDir = function* (path, attributes = 'standard::name') { + const dir = Gio.File.new_for_path(path) + if (!GLib.file_test(path, GLib.FileTest.IS_DIR)) return + const children = dir.enumerate_children(attributes, Gio.FileQueryInfoFlags.NONE, null) + let info + while ((info = children.next_file(null)) != null) try { + const name = info.get_name() + yield { file: dir.get_child(name), name, info } + } catch (e) { + console.error(e) + } +} + const decoder = new TextDecoder() export const readFile = (file, defaultValue = '') => { try {