├── COPYING ├── README.md ├── Screenshot from 2024-04-04 19-45-22.png ├── data ├── de.hummdudel.Libellus.appdata.xml.in ├── de.hummdudel.Libellus.desktop.in ├── de.hummdudel.Libellus.gschema.xml ├── icons │ ├── blur-symbolic.svg │ ├── hicolor │ │ ├── scalable │ │ │ ├── actions │ │ │ │ ├── funnel-outline-symbolic.svg │ │ │ │ ├── funnel-symbolic.svg │ │ │ │ ├── library-symbolic.svg │ │ │ │ ├── star-large-symbolic.svg │ │ │ │ └── user-trash-symbolic.svg │ │ │ └── apps │ │ │ │ └── de.hummdudel.Libellus.svg │ │ └── symbolic │ │ │ └── apps │ │ │ └── de.hummdudel.Libellus-symbolic.svg │ ├── meson.build │ └── scroll-on-a-roll-symbolic.svg └── meson.build ├── de.hummdudel.Libellus.json ├── meson.build ├── po ├── LINGUAS ├── POTFILES ├── meson.build └── pt_BR.po └── src ├── api.js ├── api ├── 5e-SRD-Ability-Scores.json ├── 5e-SRD-Alignments.json ├── 5e-SRD-Backgrounds.json ├── 5e-SRD-Classes.json ├── 5e-SRD-Conditions.json ├── 5e-SRD-Damage-Types.json ├── 5e-SRD-Equipment-Categories.json ├── 5e-SRD-Equipment.json ├── 5e-SRD-Feats.json ├── 5e-SRD-Features.json ├── 5e-SRD-Languages.json ├── 5e-SRD-Levels.json ├── 5e-SRD-Magic-Items.json ├── 5e-SRD-Magic-Schools.json ├── 5e-SRD-Monsters.json ├── 5e-SRD-Proficiencies.json ├── 5e-SRD-Races.json ├── 5e-SRD-Rule-Sections.json ├── 5e-SRD-Rules.json ├── 5e-SRD-Skills.json ├── 5e-SRD-Spells.json ├── 5e-SRD-Subclasses.json ├── 5e-SRD-Subraces.json ├── 5e-SRD-Traits.json └── 5e-SRD-Weapon-Properties.json ├── character_sheet.js ├── data_builder.js ├── dbus.js ├── de.hummdudel.Libellus.data.gresource.xml ├── de.hummdudel.Libellus.in ├── de.hummdudel.Libellus.src.gresource.xml ├── de.hummdudel.Libellus.xml ├── dnd.js ├── filter.js ├── filter_dialog.ui ├── filter_page.ui ├── filter_row.ui ├── gtk └── help-overlay.ui ├── main.js ├── meson.build ├── modules.js ├── results.js ├── source.js ├── source_dialog.ui ├── source_row.ui ├── window.js └── window.ui /README.md: -------------------------------------------------------------------------------- 1 | # Libellus 2 | 3 | ![Screenshot from 2024-04-04 19-45-22](https://github.com/qwertzuiopy/Libellus/assets/89102209/5b19aa2b-1231-435a-9a94-918889a97311) 4 | 5 | Get it on Flathub 6 | 7 | A simple DnD content viewer app. 8 | It uses https://www.dnd5eapi.co/ as a database and simply displays the information. 9 | But because making an http request every time is to slow a local copy of the database is shiped instead (Images are still downloaded though). 10 | 11 | The easiest way to test the app is to clone this repository and open it in Gnome Builder. 12 | Alternatively you can run "meson build && ninja -C build && sudo ninja -C build install" to install the app locally. 13 | 14 | Other sources than the Player's Handbook can also be added / viewed in the app, please see https://github.com/qwertzuiopy/LibellusSources. 15 | 16 | Dependencies are Gtk4 and Libadwaita. 17 | 18 | NOTE: This project is not finished, there will be bugs. 19 | If you have any suggestions or found any bugs, feel free to open an issue or a merge request, help is greatly appreciated! 20 | -------------------------------------------------------------------------------- /Screenshot from 2024-04-04 19-45-22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qwertzuiopy/Libellus/4c579e37f6ca033b0d5901dce38ae21460403de5/Screenshot from 2024-04-04 19-45-22.png -------------------------------------------------------------------------------- /data/de.hummdudel.Libellus.appdata.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | de.hummdudel.Libellus 4 | CC0-1.0 5 | GPL-3.0-or-later 6 | 7 | mild 8 | mild 9 | mild 10 | 11 | Libellus 12 | View DnD information in style 13 | 14 | Luna 15 | 16 | 17 |

Libellus lets you browse and read DnD 5th edition information like a wiki / lexicon. The basic navigation is tab-based, so multiple pages can be viewed at the same time. There is a basic search / filter system. The data is stored locally, images are downloaded.

18 |

19 | Libellus currently supports Classes, Races, Spells, Magic Items, Equipment, and Monsters 20 |

21 |
22 | https://libellus.hummdudel.de 23 | https://github.com/qwertzuiopy/Libellus/issues 24 | de.hummdudel.Libellus.desktop 25 | 26 | #c061cb 27 | #813d9c 28 | 29 | 30 | 31 | https://libellus.hummdudel.de/Images/libellus.png 32 | Info page for the Aboleth monster 33 | 34 | 35 | https://libellus.hummdudel.de/Images/screenshot-details.png 36 | Detailed class info page 37 | 38 | 39 | https://libellus.hummdudel.de/Images/screenshot-search.png 40 | Search view with a "Spells" filter 41 | 42 | 43 | https://libellus.hummdudel.de/Images/screenshot-tabs.png 44 | The overview showing multiple tabs with images 45 | 46 | 47 | https://libellus.hummdudel.de/Images/screenshot-addaptive.png 48 | An info page in a much smaller window 49 | 50 | 51 | 52 | keyboard 53 | pointing 54 | touch 55 | 56 | 57 | 300 58 | 59 | 60 | dnd 61 | wiki 62 | 63 | 64 | 65 | 66 |

Fix a potential crash on startup due to an old save file version.

67 |
68 |
69 | 70 | 71 |

Add initial support for multiple sources.

72 |

Update Sdk to version 47 (accent colors!).

73 |

Fix the Sorcerers details page.

74 |
75 |
76 | 77 | 78 |

Fix the brand colors to have more contrast, make the main list sizing more consistent and allow copying of descriptions.

79 |
80 |
81 | 82 | 83 |

Fix the ordering of Class Features + Fix long stat labels to wrap on small screens

84 |
85 |
86 | 87 | 88 |

Deduplicate the adaptivity code and fix Stat Row widgets to also be adaptive + update appdata

89 |
90 |
91 | 92 | 93 |

Initial release

94 |
95 |
96 |
97 |
98 | 99 | -------------------------------------------------------------------------------- /data/de.hummdudel.Libellus.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Libellus 3 | Exec=de.hummdudel.Libellus 4 | Icon=de.hummdudel.Libellus 5 | Terminal=false 6 | Type=Application 7 | Categories=Utility 8 | StartupNotify=true 9 | -------------------------------------------------------------------------------- /data/de.hummdudel.Libellus.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | star-large-symbolic.svg 5 | user-trash-symbolic.svg 6 | library-symbolic.svg 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /data/icons/blur-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/funnel-outline-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/funnel-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/library-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/star-large-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/user-trash-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/apps/de.hummdudel.Libellus.svg: -------------------------------------------------------------------------------- 1 | 2 | 195 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/apps/de.hummdudel.Libellus-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/icons/meson.build: -------------------------------------------------------------------------------- 1 | application_id = 'de.hummdudel.Libellus' 2 | 3 | scalable_dir = join_paths('hicolor', 'scalable', 'apps') 4 | install_data( 5 | join_paths(scalable_dir, ('@0@.svg').format(application_id)), 6 | install_dir: join_paths(get_option('datadir'), 'icons', scalable_dir) 7 | ) 8 | 9 | symbolic_dir = join_paths('hicolor', 'symbolic', 'apps') 10 | install_data( 11 | join_paths(symbolic_dir, ('@0@-symbolic.svg').format(application_id)), 12 | install_dir: join_paths(get_option('datadir'), 'icons', symbolic_dir) 13 | ) 14 | 15 | action_dir = join_paths('hicolor', 'scalable', 'actions') 16 | action_icons = [ 17 | # each icon must be registered here 18 | join_paths(action_dir, 'star-large-symbolic.svg'), 19 | join_paths(action_dir, 'funnel-outline-symbolic.svg'), 20 | join_paths(action_dir, 'funnel-symbolic.svg'), 21 | join_paths(action_dir, 'library-symbolic.svg'), 22 | ] 23 | install_data( 24 | action_icons, 25 | install_dir: join_paths(get_option('datadir'), 'icons', action_dir) 26 | ) 27 | -------------------------------------------------------------------------------- /data/icons/scroll-on-a-roll-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/meson.build: -------------------------------------------------------------------------------- 1 | desktop_file = i18n.merge_file( 2 | input: 'de.hummdudel.Libellus.desktop.in', 3 | output: 'de.hummdudel.Libellus.desktop', 4 | type: 'desktop', 5 | po_dir: '../po', 6 | install: true, 7 | install_dir: join_paths(get_option('datadir'), 'applications') 8 | ) 9 | 10 | desktop_utils = find_program('desktop-file-validate', required: false) 11 | if desktop_utils.found() 12 | test('Validate desktop file', desktop_utils, args: [desktop_file]) 13 | endif 14 | 15 | appstream_file = i18n.merge_file( 16 | input: 'de.hummdudel.Libellus.appdata.xml.in', 17 | output: 'de.hummdudel.Libellus.appdata.xml', 18 | po_dir: '../po', 19 | install: true, 20 | install_dir: join_paths(get_option('datadir'), 'appdata') 21 | ) 22 | 23 | appstream_util = find_program('appstream-util', required: false) 24 | if appstream_util.found() 25 | test('Validate appstream file', appstream_util, args: ['validate', appstream_file]) 26 | endif 27 | 28 | install_data('de.hummdudel.Libellus.gschema.xml', 29 | install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas') 30 | ) 31 | 32 | compile_schemas = find_program('glib-compile-schemas', required: false) 33 | if compile_schemas.found() 34 | test('Validate schema file', 35 | compile_schemas, 36 | args: ['--dry-run', meson.current_source_dir()]) 37 | endif 38 | 39 | subdir('icons') 40 | -------------------------------------------------------------------------------- /de.hummdudel.Libellus.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id" : "de.hummdudel.Libellus", 3 | "runtime" : "org.gnome.Platform", 4 | "runtime-version" : "master", 5 | "sdk" : "org.gnome.Sdk", 6 | "command" : "de.hummdudel.Libellus", 7 | "finish-args" : [ 8 | "--share=network", 9 | "--share=ipc", 10 | "--socket=fallback-x11", 11 | "--device=dri", 12 | "--socket=wayland" 13 | ], 14 | "cleanup" : [ 15 | "/include", 16 | "/lib/pkgconfig", 17 | "/man", 18 | "/share/doc", 19 | "/share/gtk-doc", 20 | "/share/man", 21 | "/share/pkgconfig", 22 | "*.la", 23 | "*.a" 24 | ], 25 | "modules" : [ 26 | { 27 | "name" : "Libellus", 28 | "builddir" : true, 29 | "buildsystem" : "meson", 30 | "sources" : [ 31 | { 32 | "type" : "archive", 33 | "sha256": "0f698489426da31c055f2248129397b89cb6a7e5ca44327801d232375a945348", 34 | "url" : "https://github.com/qwertzuiopy/Libellus/archive/refs/tags/v1.1.1.tar.gz" 35 | } 36 | ] 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('libellus', 2 | version: '1.1.1', 3 | meson_version: '>= 0.62.0', 4 | default_options: [ 'warning_level=2', 'werror=false', ], 5 | ) 6 | 7 | i18n = import('i18n') 8 | gnome = import('gnome') 9 | 10 | 11 | 12 | subdir('data') 13 | subdir('src') 14 | subdir('po') 15 | 16 | gnome.post_install( 17 | glib_compile_schemas: true, 18 | gtk_update_icon_cache: true, 19 | update_desktop_database: true, 20 | ) 21 | -------------------------------------------------------------------------------- /po/LINGUAS: -------------------------------------------------------------------------------- 1 | pt_BR 2 | -------------------------------------------------------------------------------- /po/POTFILES: -------------------------------------------------------------------------------- 1 | data/de.hummdudel.Libellus.desktop.in 2 | data/de.hummdudel.Libellus.appdata.xml.in 3 | data/de.hummdudel.Libellus.gschema.xml 4 | src/main.js 5 | src/window.js 6 | src/window.ui 7 | src/api.js 8 | src/modules.js 9 | src/dnd.js 10 | src/results.js 11 | src/character_sheet.js 12 | src/dbus.js 13 | -------------------------------------------------------------------------------- /po/meson.build: -------------------------------------------------------------------------------- 1 | i18n.gettext('libellus', preset: 'glib') 2 | -------------------------------------------------------------------------------- /po/pt_BR.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the libellus package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: libellus\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2025-01-31 02:05-0300\n" 12 | "PO-Revision-Date: 2025-01-31 02:11-0300\n" 13 | "Last-Translator: john peter \n" 14 | "Language-Team: \n" 15 | "Language: pt_BR\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "X-Generator: Poedit 3.5\n" 20 | 21 | #: data/de.hummdudel.Libellus.desktop.in:2 22 | #: data/de.hummdudel.Libellus.appdata.xml.in:11 23 | msgid "Libellus" 24 | msgstr "Petição" 25 | 26 | #: data/de.hummdudel.Libellus.appdata.xml.in:12 27 | msgid "View DnD information in style" 28 | msgstr "Veja as informações do DND em estilo" 29 | 30 | #: data/de.hummdudel.Libellus.appdata.xml.in:14 31 | msgid "Luna" 32 | msgstr "Principal" 33 | 34 | #: data/de.hummdudel.Libellus.appdata.xml.in:17 35 | msgid "" 36 | "Libellus lets you browse and read DnD 5th edition information like a wiki / " 37 | "lexicon. The basic navigation is tab-based, so multiple pages can be viewed " 38 | "at the same time. There is a basic search / filter system. The data is " 39 | "stored locally, images are downloaded." 40 | msgstr "" 41 | "Libellus permite navegar e ler informações da 5ª edição da DND, como um " 42 | "wiki / léxico. A navegação básica é baseada em guias, para que várias " 43 | "páginas possam ser visualizadas ao mesmo tempo. Existe um sistema básico de " 44 | "pesquisa / filtro. Os dados são armazenados localmente, as imagens são " 45 | "baixadas." 46 | 47 | #: data/de.hummdudel.Libellus.appdata.xml.in:18 48 | msgid "" 49 | "Libellus currently supports Classes, Races, Spells, Magic Items, Equipment, " 50 | "and Monsters" 51 | msgstr "" 52 | "Atualmente, Libellus suporta aulas, corridas, feitiços, itens mágicos, " 53 | "equipamentos e monstros" 54 | 55 | #: data/de.hummdudel.Libellus.appdata.xml.in:32 56 | msgid "Info page for the Aboleth monster" 57 | msgstr "Página de informações para o ABOLETH Monster" 58 | 59 | #: data/de.hummdudel.Libellus.appdata.xml.in:36 60 | msgid "Detailed class info page" 61 | msgstr "Página de informações detalhadas da classe" 62 | 63 | #: data/de.hummdudel.Libellus.appdata.xml.in:40 64 | msgid "Search view with a \"Spells\" filter" 65 | msgstr "Visualização de pesquisa com um filtro \"feitiços\"" 66 | 67 | #: data/de.hummdudel.Libellus.appdata.xml.in:44 68 | msgid "The overview showing multiple tabs with images" 69 | msgstr "A visão geral mostrando várias guias com imagens" 70 | 71 | #: data/de.hummdudel.Libellus.appdata.xml.in:48 72 | msgid "An info page in a much smaller window" 73 | msgstr "Uma página de informações em uma janela muito menor" 74 | 75 | #: data/de.hummdudel.Libellus.appdata.xml.in:61 76 | msgid "wiki" 77 | msgstr "Wiki" 78 | 79 | #: data/de.hummdudel.Libellus.appdata.xml.in:66 80 | msgid "Add initial support for multiple sources." 81 | msgstr "Adicione suporte inicial a várias fontes." 82 | 83 | #: data/de.hummdudel.Libellus.appdata.xml.in:67 84 | msgid "Update Sdk to version 47 (accent colors!)." 85 | msgstr "Atualize o SDK para a versão 47 (cores de destaque!)." 86 | 87 | #: data/de.hummdudel.Libellus.appdata.xml.in:68 88 | msgid "Fix the Sorcerers details page." 89 | msgstr "Corrija a página Detalhes dos Feiticeiros." 90 | 91 | #: data/de.hummdudel.Libellus.appdata.xml.in:73 92 | msgid "" 93 | "Fix the brand colors to have more contrast, make the main list sizing more " 94 | "consistent and allow copying of descriptions." 95 | msgstr "" 96 | "Corrija as cores da marca para ter mais contraste, tornar o dimensionamento " 97 | "da lista principal mais consistente e permitir a cópia das descrições." 98 | 99 | #: data/de.hummdudel.Libellus.appdata.xml.in:78 100 | msgid "" 101 | "Fix the ordering of Class Features + Fix long stat labels to wrap on small " 102 | "screens" 103 | msgstr "" 104 | "Corrija a ordem dos recursos da classe + Fix Long Stat Rótulos para envolver " 105 | "em telas pequenas" 106 | 107 | #: data/de.hummdudel.Libellus.appdata.xml.in:83 108 | msgid "" 109 | "Deduplicate the adaptivity code and fix Stat Row widgets to also be adaptive " 110 | "+ update appdata" 111 | msgstr "" 112 | "Desduplique o código de adaptividade e corrija os widgets de linha " 113 | "estatística para também ser adaptativo + atualização AppData" 114 | 115 | #: data/de.hummdudel.Libellus.appdata.xml.in:88 116 | msgid "Initial release" 117 | msgstr "Liberação inicial" 118 | -------------------------------------------------------------------------------- /src/api/5e-SRD-Ability-Scores.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "index": "str", 4 | "name": "STR", 5 | "full_name": "Strength", 6 | "desc": [ 7 | "Strength measures bodily power, athletic training, and the extent to which you can exert raw physical force.", 8 | "A Strength check can model any attempt to lift, push, pull, or break something, to force your body through a space, or to otherwise apply brute force to a situation. The Athletics skill reflects aptitude in certain kinds of Strength checks." 9 | ], 10 | "skills": [ 11 | { 12 | "name": "Athletics", 13 | "index": "athletics", 14 | "url": "/api/skills/athletics" 15 | } 16 | ], 17 | "url": "/api/ability-scores/str" 18 | }, 19 | { 20 | "index": "dex", 21 | "name": "DEX", 22 | "full_name": "Dexterity", 23 | "desc": [ 24 | "Dexterity measures agility, reflexes, and balance.", 25 | "A Dexterity check can model any attempt to move nimbly, quickly, or quietly, or to keep from falling on tricky footing. The Acrobatics, Sleight of Hand, and Stealth skills reflect aptitude in certain kinds of Dexterity checks." 26 | ], 27 | "skills": [ 28 | { 29 | "name": "Acrobatics", 30 | "index": "acrobatics", 31 | "url": "/api/skills/acrobatics" 32 | }, 33 | { 34 | "name": "Sleight of Hand", 35 | "index": "sleight-of-hand", 36 | "url": "/api/skills/sleight-of-hand" 37 | }, 38 | { 39 | "name": "Stealth", 40 | "index": "stealth", 41 | "url": "/api/skills/stealth" 42 | } 43 | ], 44 | "url": "/api/ability-scores/dex" 45 | }, 46 | { 47 | "index": "con", 48 | "name": "CON", 49 | "full_name": "Constitution", 50 | "desc": [ 51 | "Constitution measures health, stamina, and vital force.", 52 | "Constitution checks are uncommon, and no skills apply to Constitution checks, because the endurance this ability represents is largely passive rather than involving a specific effort on the part of a character or monster." 53 | ], 54 | "skills": [], 55 | "url": "/api/ability-scores/con" 56 | }, 57 | { 58 | "index": "int", 59 | "name": "INT", 60 | "full_name": "Intelligence", 61 | "desc": [ 62 | "Intelligence measures mental acuity, accuracy of recall, and the ability to reason.", 63 | "An Intelligence check comes into play when you need to draw on logic, education, memory, or deductive reasoning. The Arcana, History, Investigation, Nature, and Religion skills reflect aptitude in certain kinds of Intelligence checks." 64 | ], 65 | "skills": [ 66 | { 67 | "name": "Arcana", 68 | "index": "arcana", 69 | "url": "/api/skills/arcana" 70 | }, 71 | { 72 | "name": "History", 73 | "index": "history", 74 | "url": "/api/skills/history" 75 | }, 76 | { 77 | "name": "Investigation", 78 | "index": "investigation", 79 | "url": "/api/skills/investigation" 80 | }, 81 | { 82 | "name": "Nature", 83 | "index": "nature", 84 | "url": "/api/skills/nature" 85 | }, 86 | { 87 | "name": "Religion", 88 | "index": "religion", 89 | "url": "/api/skills/religion" 90 | } 91 | ], 92 | "url": "/api/ability-scores/int" 93 | }, 94 | { 95 | "index": "wis", 96 | "name": "WIS", 97 | "full_name": "Wisdom", 98 | "desc": [ 99 | "Wisdom reflects how attuned you are to the world around you and represents perceptiveness and intuition.", 100 | "A Wisdom check might reflect an effort to read body language, understand someone's feelings, notice things about the environment, or care for an injured person. The Animal Handling, Insight, Medicine, Perception, and Survival skills reflect aptitude in certain kinds of Wisdom checks." 101 | ], 102 | "skills": [ 103 | { 104 | "name": "Animal Handling", 105 | "index": "animal-handling", 106 | "url": "/api/skills/animal-handling" 107 | }, 108 | { 109 | "name": "Insight", 110 | "index": "insight", 111 | "url": "/api/skills/insight" 112 | }, 113 | { 114 | "name": "Medicine", 115 | "index": "medicine", 116 | "url": "/api/skills/medicine" 117 | }, 118 | { 119 | "name": "Perception", 120 | "index": "perception", 121 | "url": "/api/skills/perception" 122 | }, 123 | { 124 | "name": "Survival", 125 | "index": "survival", 126 | "url": "/api/skills/survival" 127 | } 128 | ], 129 | "url": "/api/ability-scores/wis" 130 | }, 131 | { 132 | "index": "cha", 133 | "name": "CHA", 134 | "full_name": "Charisma", 135 | "desc": [ 136 | "Charisma measures your ability to interact effectively with others. It includes such factors as confidence and eloquence, and it can represent a charming or commanding personality.", 137 | "A Charisma check might arise when you try to influence or entertain others, when you try to make an impression or tell a convincing lie, or when you are navigating a tricky social situation. The Deception, Intimidation, Performance, and Persuasion skills reflect aptitude in certain kinds of Charisma checks." 138 | ], 139 | "skills": [ 140 | { 141 | "name": "Deception", 142 | "index": "deception", 143 | "url": "/api/skills/deception" 144 | }, 145 | { 146 | "name": "Intimidation", 147 | "index": "intimidation", 148 | "url": "/api/skills/intimidation" 149 | }, 150 | { 151 | "name": "Performance", 152 | "index": "performance", 153 | "url": "/api/skills/performance" 154 | }, 155 | { 156 | "name": "Persuasion", 157 | "index": "persuasion", 158 | "url": "/api/skills/persuasion" 159 | } 160 | ], 161 | "url": "/api/ability-scores/cha" 162 | } 163 | ] 164 | -------------------------------------------------------------------------------- /src/api/5e-SRD-Alignments.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "index": "lawful-good", 4 | "name": "Lawful Good", 5 | "abbreviation": "LG", 6 | "desc": "Lawful good (LG) creatures can be counted on to do the right thing as expected by society. Gold dragons, paladins, and most dwarves are lawful good.", 7 | "url": "/api/alignments/lawful-good" 8 | }, 9 | { 10 | "index": "neutral-good", 11 | "name": "Neutral Good", 12 | "abbreviation": "NG", 13 | "desc": "Neutral good (NG) folk do the best they can to help others according to their needs. Many celestials, some cloud giants, and most gnomes are neutral good.", 14 | "url": "/api/alignments/neutral-good" 15 | }, 16 | { 17 | "index": "chaotic-good", 18 | "name": "Chaotic Good", 19 | "abbreviation": "CG", 20 | "desc": "Chaotic good (CG) creatures act as their conscience directs, with little regard for what others expect. Copper dragons, many elves, and unicorns are chaotic good.", 21 | "url": "/api/alignments/chaotic-good" 22 | }, 23 | { 24 | "index": "lawful-neutral", 25 | "name": "Lawful Neutral", 26 | "abbreviation": "LN", 27 | "desc": "Lawful neutral (LN) individuals act in accordance with law, tradition, or personal codes. Many monks and some wizards are lawful neutral.", 28 | "url": "/api/alignments/lawful-neutral" 29 | }, 30 | { 31 | "index": "neutral", 32 | "name": "Neutral", 33 | "abbreviation": "N", 34 | "desc": "Neutral (N) is the alignment of those who prefer to steer clear of moral questions and don't take sides, doing what seems best at the time. Lizardfolk, most druids, and many humans are neutral.", 35 | "url": "/api/alignments/neutral" 36 | }, 37 | { 38 | "index": "chaotic-neutral", 39 | "name": "Chaotic Neutral", 40 | "abbreviation": "CN", 41 | "desc": "Chaotic neutral (CN) creatures follow their whims, holding their personal freedom above all else. Many barbarians and rogues, and some bards, are chaotic neutral.", 42 | "url": "/api/alignments/chaotic-neutral" 43 | }, 44 | { 45 | "index": "lawful-evil", 46 | "name": "Lawful Evil", 47 | "abbreviation": "LE", 48 | "desc": "Lawful evil (LE) creatures methodically take what they want, within the limits of a code of tradition, loyalty, or order. Devils, blue dragons, and hobgoblins are lawful evil.", 49 | "url": "/api/alignments/lawful-evil" 50 | }, 51 | { 52 | "index": "neutral-evil", 53 | "name": "Neutral Evil", 54 | "abbreviation": "NE", 55 | "desc": "Neutral evil (NE) is the alignment of those who do whatever they can get away with, without compassion or qualms. Many drow, some cloud giants, and goblins are neutral evil.", 56 | "url": "/api/alignments/neutral-evil" 57 | }, 58 | { 59 | "index": "chaotic-evil", 60 | "name": "Chaotic Evil", 61 | "abbreviation": "CE", 62 | "desc": "Chaotic evil (CE) creatures act with arbitrary violence, spurred by their greed, hatred, or bloodlust. Demons, red dragons, and orcs are chaotic evil.", 63 | "url": "/api/alignments/chaotic-evil" 64 | } 65 | ] 66 | -------------------------------------------------------------------------------- /src/api/5e-SRD-Backgrounds.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "index": "acolyte", 4 | "name": "Acolyte", 5 | "starting_proficiencies": [ 6 | { 7 | "index": "skill-insight", 8 | "name": "Skill: Insight", 9 | "url": "/api/proficiencies/skill-insight" 10 | }, 11 | { 12 | "index": "skill-religion", 13 | "name": "Skill: Religion", 14 | "url": "/api/proficiencies/skill-religion" 15 | } 16 | ], 17 | "language_options": { 18 | "choose": 2, 19 | "type": "languages", 20 | "from": { 21 | "option_set_type": "resource_list", 22 | "resource_list_url": "/api/languages" 23 | } 24 | }, 25 | "starting_equipment": [ 26 | { 27 | "equipment": { 28 | "index": "clothes-common", 29 | "name": "Clothes, common", 30 | "url": "/api/equipment/clothes-common" 31 | }, 32 | "quantity": 1 33 | }, 34 | { 35 | "equipment": { 36 | "index": "pouch", 37 | "name": "Pouch", 38 | "url": "/api/equipment/pouch" 39 | }, 40 | "quantity": 1 41 | } 42 | ], 43 | "starting_equipment_options": [ 44 | { 45 | "choose": 1, 46 | "type": "equipment", 47 | "from": { 48 | "option_set_type": "equipment_category", 49 | "equipment_category": { 50 | "index": "holy-symbols", 51 | "name": "Holy Symbols", 52 | "url": "/api/equipment-categories/holy-symbols" 53 | } 54 | } 55 | } 56 | ], 57 | "feature": { 58 | "name": "Shelter of the Faithful", 59 | "desc": [ 60 | "As an acolyte, you command the respect of those who share your faith, and you can perform the religious ceremonies of your deity. You and your adventuring companions can expect to receive free healing and care at a temple, shrine, or other established presence of your faith, though you must provide any material components needed for spells. Those who share your religion will support you (but only you) at a modest lifestyle.", 61 | "You might also have ties to a specific temple dedicated to your chosen deity or pantheon, and you have a residence there. This could be the temple where you used to serve, if you remain on good terms with it, or a temple where you have found a new home. While near your temple, you can call upon the priests for assistance, provided the assistance you ask for is not hazardous and you remain in good standing with your temple." 62 | ] 63 | }, 64 | "personality_traits": { 65 | "choose": 2, 66 | "type": "personality_traits", 67 | "from": { 68 | "option_set_type": "options_array", 69 | "options": [ 70 | { 71 | "option_type": "string", 72 | "string": "I idolize a particular hero of my faith, and constantly refer to that person's deeds and example." 73 | }, 74 | { 75 | "option_type": "string", 76 | "string": "I can find common ground between the fiercest enemies, empathizing with them and always working toward peace." 77 | }, 78 | { 79 | "option_type": "string", 80 | "string": "I see omens in every event and action. The gods try to speak to us, we just need to listen." 81 | }, 82 | { 83 | "option_type": "string", 84 | "string": "Nothing can shake my optimistic attitude." 85 | }, 86 | { 87 | "option_type": "string", 88 | "string": "I quote (or misquote) sacred texts and proverbs in almost every situation." 89 | }, 90 | { 91 | "option_type": "string", 92 | "string": "I am tolerant (or intolerant) of other faiths and respect (or condemn) the worship of other gods." 93 | }, 94 | { 95 | "option_type": "string", 96 | "string": "I've enjoyed fine food, drink, and high society among my temple's elite. Rough living grates on me." 97 | }, 98 | { 99 | "option_type": "string", 100 | "string": "I've spent so long in the temple that I have little practical experience dealing with people in the outside world." 101 | } 102 | ] 103 | } 104 | }, 105 | "ideals": { 106 | "choose": 1, 107 | "type": "ideals", 108 | "from": { 109 | "option_set_type": "options_array", 110 | "options": [ 111 | { 112 | "option_type": "ideal", 113 | "desc": "Tradition. The ancient traditions of worship and sacrifice must be preserved and upheld.", 114 | "alignments": [ 115 | { 116 | "index": "lawful-good", 117 | "name": "Lawful Good", 118 | "url": "/api/alignments/lawful-good" 119 | }, 120 | { 121 | "index": "lawful-neutral", 122 | "name": "Lawful Neutral", 123 | "url": "/api/alignments/lawful-neutral" 124 | }, 125 | { 126 | "index": "lawful-evil", 127 | "name": "Lawful Evil", 128 | "url": "/api/alignments/lawful-evil" 129 | } 130 | ] 131 | }, 132 | { 133 | "option_type": "ideal", 134 | "desc": "Charity. I always try to help those in need, no matter what the personal cost.", 135 | "alignments": [ 136 | { 137 | "index": "lawful-good", 138 | "name": "Lawful Good", 139 | "url": "/api/alignments/lawful-good" 140 | }, 141 | { 142 | "index": "neutral-good", 143 | "name": "Neutral Good", 144 | "url": "/api/alignments/neutral-good" 145 | }, 146 | { 147 | "index": "chaotic-good", 148 | "name": "Chaotic Good", 149 | "url": "/api/alignments/chaotic-good" 150 | } 151 | ] 152 | }, 153 | { 154 | "option_type": "ideal", 155 | "desc": "Change. We must help bring about the changes the gods are constantly working in the world.", 156 | "alignments": [ 157 | { 158 | "index": "chaotic-good", 159 | "name": "Chaotic Good", 160 | "url": "/api/alignments/chaotic-good" 161 | }, 162 | { 163 | "index": "chaotic-neutral", 164 | "name": "Chaotic Neutral", 165 | "url": "/api/alignments/chaotic-neutral" 166 | }, 167 | { 168 | "index": "chaotic-evil", 169 | "name": "Chaotic Evil", 170 | "url": "/api/alignments/chaotic-evil" 171 | } 172 | ] 173 | }, 174 | { 175 | "option_type": "ideal", 176 | "desc": "Power. I hope to one day rise to the top of my faith's religious hierarchy.", 177 | "alignments": [ 178 | { 179 | "index": "lawful-good", 180 | "name": "Lawful Good", 181 | "url": "/api/alignments/lawful-good" 182 | }, 183 | { 184 | "index": "lawful-neutral", 185 | "name": "Lawful Neutral", 186 | "url": "/api/alignments/lawful-neutral" 187 | }, 188 | { 189 | "index": "lawful-evil", 190 | "name": "Lawful Evil", 191 | "url": "/api/alignments/lawful-evil" 192 | } 193 | ] 194 | }, 195 | { 196 | "option_type": "ideal", 197 | "desc": "Faith. I trust that my deity will guide my actions. I have faith that if I work hard, things will go well.", 198 | "alignments": [ 199 | { 200 | "index": "lawful-good", 201 | "name": "Lawful Good", 202 | "url": "/api/alignments/lawful-good" 203 | }, 204 | { 205 | "index": "lawful-neutral", 206 | "name": "Lawful Neutral", 207 | "url": "/api/alignments/lawful-neutral" 208 | }, 209 | { 210 | "index": "lawful-evil", 211 | "name": "Lawful Evil", 212 | "url": "/api/alignments/lawful-evil" 213 | } 214 | ] 215 | }, 216 | { 217 | "option_type": "ideal", 218 | "desc": "Aspiration. I seek to prove myself worthy of my god's favor by matching my actions against his or her teachings.", 219 | "alignments": [ 220 | { 221 | "index": "lawful-good", 222 | "name": "Lawful Good", 223 | "url": "/api/alignments/lawful-good" 224 | }, 225 | { 226 | "index": "neutral-good", 227 | "name": "Neutral Good", 228 | "url": "/api/alignments/neutral-good" 229 | }, 230 | { 231 | "index": "chaotic-good", 232 | "name": "Chaotic Good", 233 | "url": "/api/alignments/chaotic-good" 234 | }, 235 | { 236 | "index": "lawful-neutral", 237 | "name": "Lawful Neutral", 238 | "url": "/api/alignments/lawful-neutral" 239 | }, 240 | { 241 | "index": "neutral", 242 | "name": "Neutral", 243 | "url": "/api/alignments/neutral" 244 | }, 245 | { 246 | "index": "chaotic-neutral", 247 | "name": "Chaotic Neutral", 248 | "url": "/api/alignments/chaotic-neutral" 249 | }, 250 | { 251 | "index": "lawful-evil", 252 | "name": "Lawful Evil", 253 | "url": "/api/alignments/lawful-evil" 254 | }, 255 | { 256 | "index": "neutral-evil", 257 | "name": "Neutral Evil", 258 | "url": "/api/alignments/neutral-evil" 259 | }, 260 | { 261 | "index": "chaotic-evil", 262 | "name": "Chaotic Evil", 263 | "url": "/api/alignments/chaotic-evil" 264 | } 265 | ] 266 | } 267 | ] 268 | } 269 | }, 270 | "bonds": { 271 | "choose": 1, 272 | "type": "bonds", 273 | "from": { 274 | "option_set_type": "options_array", 275 | "options": [ 276 | { 277 | "option_type": "string", 278 | "string": "I would die to recover an ancient relic of my faith that was lost long ago." 279 | }, 280 | { 281 | "option_type": "string", 282 | "string": "I will someday get revenge on the corrupt temple hierarchy who branded me a heretic." 283 | }, 284 | { 285 | "option_type": "string", 286 | "string": "I owe my life to the priest who took me in when my parents died." 287 | }, 288 | { 289 | "option_type": "string", 290 | "string": "Everything I do is for the common people." 291 | }, 292 | { 293 | "option_type": "string", 294 | "string": "I will do anything to protect the temple where I served." 295 | }, 296 | { 297 | "option_type": "string", 298 | "string": "I seek to preserve a sacred text that my enemies consider heretical and seek to destroy." 299 | } 300 | ] 301 | } 302 | }, 303 | "flaws": { 304 | "choose": 1, 305 | "type": "flaws", 306 | "from": { 307 | "option_set_type": "options_array", 308 | "options": [ 309 | { 310 | "option_type": "string", 311 | "string": "I judge others harshly, and myself even more severely." 312 | }, 313 | { 314 | "option_type": "string", 315 | "string": "I put too much trust in those who wield power within my temple's hierarchy." 316 | }, 317 | { 318 | "option_type": "string", 319 | "string": "My piety sometimes leads me to blindly trust those that profess faith in my god." 320 | }, 321 | { 322 | "option_type": "string", 323 | "string": "I am inflexible in my thinking." 324 | }, 325 | { 326 | "option_type": "string", 327 | "string": "I am suspicious of strangers and expect the worst of them." 328 | }, 329 | { 330 | "option_type": "string", 331 | "string": "Once I pick a goal, I become obsessed with it to the detriment of everything else in my life." 332 | } 333 | ] 334 | } 335 | }, 336 | "url": "/api/backgrounds/acolyte" 337 | } 338 | ] 339 | -------------------------------------------------------------------------------- /src/api/5e-SRD-Conditions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "index": "blinded", 4 | "name": "Blinded", 5 | "desc": [ 6 | "- A blinded creature can't see and automatically fails any ability check that requires sight.", 7 | "- Attack rolls against the creature have advantage, and the creature's attack rolls have disadvantage." 8 | ], 9 | "url": "/api/conditions/blinded" 10 | }, 11 | { 12 | "index": "charmed", 13 | "name": "Charmed", 14 | "desc": [ 15 | "- A charmed creature can't attack the charmer or target the charmer with harmful abilities or magical effects.", 16 | "- The charmer has advantage on any ability check to interact socially with the creature." 17 | ], 18 | "url": "/api/conditions/charmed" 19 | }, 20 | { 21 | "index": "deafened", 22 | "name": "Deafened", 23 | "desc": [ 24 | "- A deafened creature can't hear and automatically fails any ability check that requires hearing." 25 | ], 26 | "url": "/api/conditions/deafened" 27 | }, 28 | { 29 | "index": "frightened", 30 | "name": "Frightened", 31 | "desc": [ 32 | "- A frightened creature has disadvantage on ability checks and attack rolls while the source of its fear is within line of sight.", 33 | "- The creature can't willingly move closer to the source of its fear." 34 | ], 35 | "url": "/api/conditions/frightened" 36 | }, 37 | { 38 | "index": "grappled", 39 | "name": "Grappled", 40 | "desc": [ 41 | "- A grappled creature's speed becomes 0, and it can't benefit from any bonus to its speed.", 42 | "- The condition ends if the grappler is incapacitated (see the condition).", 43 | "- The condition also ends if an effect removes the grappled creature from the reach of the grappler or grappling effect, such as when a creature is hurled away by the thunderwave spell." 44 | ], 45 | "url": "/api/conditions/grappled" 46 | }, 47 | { 48 | "index": "incapacitated", 49 | "name": "Incapacitated", 50 | "desc": ["- An incapacitated creature can't take actions or reactions."], 51 | "url": "/api/conditions/incapacitated" 52 | }, 53 | { 54 | "index": "invisible", 55 | "name": "Invisible", 56 | "desc": [ 57 | "- An invisible creature is impossible to see without the aid of magic or a special sense. For the purpose of hiding, the creature is heavily obscured. The creature's location can be detected by any noise it makes or any tracks it leaves.", 58 | "- Attack rolls against the creature have disadvantage, and the creature's attack rolls have advantage." 59 | ], 60 | "url": "/api/conditions/invisible" 61 | }, 62 | { 63 | "index": "paralyzed", 64 | "name": "Paralyzed", 65 | "desc": [ 66 | "- A paralyzed creature is incapacitated (see the condition) and can't move or speak.", 67 | "- The creature automatically fails Strength and Dexterity saving throws.", 68 | "- Attack rolls against the creature have advantage.", 69 | "- Any attack that hits the creature is a critical hit if the attacker is within 5 feet of the creature." 70 | ], 71 | "url": "/api/conditions/paralyzed" 72 | }, 73 | { 74 | "index": "petrified", 75 | "name": "Petrified", 76 | "desc": [ 77 | "- A petrified creature is transformed, along with any nonmagical object it is wearing or carrying, into a solid inanimate substance (usually stone). Its weight increases by a factor of ten, and it ceases aging.", 78 | "- The creature is incapacitated (see the condition), can't move or speak, and is unaware of its surroundings.", 79 | "- Attack rolls against the creature have advantage.", 80 | "- The creature automatically fails Strength and Dexterity saving throws.", 81 | "- The creature has resistance to all damage.", 82 | "- The creature is immune to poison and disease, although a poison or disease already in its system is suspended, not neutralized." 83 | ], 84 | "url": "/api/conditions/petrified" 85 | }, 86 | { 87 | "index": "poisoned", 88 | "name": "Poisoned", 89 | "desc": [ 90 | "- A poisoned creature has disadvantage on attack rolls and ability checks." 91 | ], 92 | "url": "/api/conditions/poisoned" 93 | }, 94 | { 95 | "index": "prone", 96 | "name": "Prone", 97 | "desc": [ 98 | "- A prone creature's only movement option is to crawl, unless it stands up and thereby ends the condition.", 99 | "- The creature has disadvantage on attack rolls.", 100 | "- An attack roll against the creature has advantage if the attacker is within 5 feet of the creature. Otherwise, the attack roll has disadvantage." 101 | ], 102 | "url": "/api/conditions/prone" 103 | }, 104 | { 105 | "index": "restrained", 106 | "name": "Restrained", 107 | "desc": [ 108 | "- A restrained creature's speed becomes 0, and it can't benefit from any bonus to its speed.", 109 | "- Attack rolls against the creature have advantage, and the creature's attack rolls have disadvantage.", 110 | "- The creature has disadvantage on Dexterity saving throws." 111 | ], 112 | "url": "/api/conditions/restrained" 113 | }, 114 | { 115 | "index": "stunned", 116 | "name": "Stunned", 117 | "desc": [ 118 | "- A stunned creature is incapacitated (see the condition), can't move, and can speak only falteringly.", 119 | "- The creature automatically fails Strength and Dexterity saving throws.", 120 | "- Attack rolls against the creature have advantage." 121 | ], 122 | "url": "/api/conditions/stunned" 123 | }, 124 | { 125 | "index": "unconscious", 126 | "name": "Unconscious", 127 | "desc": [ 128 | "- An unconscious creature is incapacitated (see the condition), can't move or speak, and is unaware of its surroundings.", 129 | "- The creature drops whatever it's holding and falls prone.", 130 | "- The creature automatically fails Strength and Dexterity saving throws.", 131 | "- Attack rolls against the creature have advantage.", 132 | "- Any attack that hits the creature is a critical hit if the attacker is within 5 feet of the creature." 133 | ], 134 | "url": "/api/conditions/unconscious" 135 | }, 136 | { 137 | "index": "exhaustion", 138 | "name": "Exhaustion", 139 | "desc": [ 140 | "Some special abilities and environmental hazards, such as starvation and the long-term effects of freezing or scorching temperatures, can lead to a special condition called exhaustion. Exhaustion is measured in six levels. An effect can give a creature one or more levels of exhaustion, as specified in the effect's description.", 141 | "1 - Disadvantage on ability checks", 142 | "2 - Speed halved", 143 | "3 - Disadvantage on attack rolls and saving throws", 144 | "4 - Hit point maximum halved", 145 | "5 - Speed reduced to 0", 146 | "6 - Death", 147 | "If an already exhausted creature suffers another effect that causes exhaustion, its current level of exhaustion increases by the amount specified in the effect's description.", 148 | "A creature suffers the effect of its current level of exhaustion as well as all lower levels. For example, a creature suffering level 2 exhaustion has its speed halved and has disadvantage on ability checks.", 149 | "An effect that removes exhaustion reduces its level as specified in the effect's description, with all exhaustion effects ending if a creature's exhaustion level is reduced below 1.", 150 | "Finishing a long rest reduces a creature's exhaustion level by 1, provided that the creature has also ingested some food and drink." 151 | ], 152 | "url": "/api/conditions/exhaustion" 153 | } 154 | ] 155 | -------------------------------------------------------------------------------- /src/api/5e-SRD-Damage-Types.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "index": "acid", 4 | "name": "Acid", 5 | "desc": [ 6 | "The corrosive spray of a black dragon's breath and the dissolving enzymes secreted by a black pudding deal acid damage." 7 | ], 8 | "url": "/api/damage-types/acid" 9 | }, 10 | { 11 | "index": "bludgeoning", 12 | "name": "Bludgeoning", 13 | "desc": [ 14 | "Blunt force attacks, falling, constriction, and the like deal bludgeoning damage." 15 | ], 16 | "url": "/api/damage-types/bludgeoning" 17 | }, 18 | { 19 | "index": "cold", 20 | "name": "Cold", 21 | "desc": [ 22 | "The infernal chill radiating from an ice devil's spear and the frigid blast of a white dragon's breath deal cold damage." 23 | ], 24 | "url": "/api/damage-types/cold" 25 | }, 26 | { 27 | "index": "fire", 28 | "name": "Fire", 29 | "desc": [ 30 | "Red dragons breathe fire, and many spells conjure flames to deal fire damage." 31 | ], 32 | "url": "/api/damage-types/fire" 33 | }, 34 | { 35 | "index": "force", 36 | "name": "Force", 37 | "desc": [ 38 | "Force is pure magical energy focused into a damaging form. Most effects that deal force damage are spells, including magic missile and spiritual weapon." 39 | ], 40 | "url": "/api/damage-types/force" 41 | }, 42 | { 43 | "index": "lightning", 44 | "name": "Lightning", 45 | "desc": [ 46 | "A lightning bolt spell and a blue dragon's breath deal lightning damage." 47 | ], 48 | "url": "/api/damage-types/lightning" 49 | }, 50 | { 51 | "index": "necrotic", 52 | "name": "Necrotic", 53 | "desc": [ 54 | "Necrotic damage, dealt by certain undead and a spell such as chill touch, withers matter and even the soul." 55 | ], 56 | "url": "/api/damage-types/necrotic" 57 | }, 58 | { 59 | "index": "piercing", 60 | "name": "Piercing", 61 | "desc": [ 62 | "Puncturing and impaling attacks, including spears and monsters' bites, deal piercing damage." 63 | ], 64 | "url": "/api/damage-types/piercing" 65 | }, 66 | { 67 | "index": "poison", 68 | "name": "Poison", 69 | "desc": [ 70 | "Venomous stings and the toxic gas of a green dragon's breath deal poison damage." 71 | ], 72 | "url": "/api/damage-types/poison" 73 | }, 74 | { 75 | "index": "psychic", 76 | "name": "Psychic", 77 | "desc": ["Mental abilities such as a psionic blast deal psychic damage."], 78 | "url": "/api/damage-types/psychic" 79 | }, 80 | { 81 | "index": "radiant", 82 | "name": "Radiant", 83 | "desc": [ 84 | "Radiant damage, dealt by a cleric's flame strike spell or an angel's smiting weapon, sears the flesh like fire and overloads the spirit with power." 85 | ], 86 | "url": "/api/damage-types/radiant" 87 | }, 88 | { 89 | "index": "slashing", 90 | "name": "Slashing", 91 | "desc": ["Swords, axes, and monsters' claws deal slashing damage."], 92 | "url": "/api/damage-types/slashing" 93 | }, 94 | { 95 | "index": "thunder", 96 | "name": "Thunder", 97 | "desc": [ 98 | "A concussive burst of sound, such as the effect of the thunderwave spell, deals thunder damage." 99 | ], 100 | "url": "/api/damage-types/thunder" 101 | } 102 | ] 103 | -------------------------------------------------------------------------------- /src/api/5e-SRD-Feats.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "index": "grappler", 4 | "name": "Grappler", 5 | "prerequisites": [ 6 | { 7 | "ability_score": { 8 | "index": "str", 9 | "name": "STR", 10 | "url": "/api/ability-scores/str" 11 | }, 12 | "minimum_score": 13 13 | } 14 | ], 15 | "desc": [ 16 | "You’ve developed the Skills necessary to hold your own in close--quarters Grappling. You gain the following benefits:", 17 | "- You have advantage on Attack Rolls against a creature you are Grappling.", 18 | "- You can use your action to try to pin a creature Grappled by you. To do so, make another grapple check. If you succeed, you and the creature are both Restrained until the grapple ends." 19 | ], 20 | "url": "/api/feats/grappler" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /src/api/5e-SRD-Languages.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "index": "common", 4 | "name": "Common", 5 | "type": "Standard", 6 | "typical_speakers": ["Humans"], 7 | "script": "Common", 8 | "url": "/api/languages/common" 9 | }, 10 | { 11 | "index": "dwarvish", 12 | "name": "Dwarvish", 13 | "desc": "Dwarvish is full of hard consonants and guttural sounds.", 14 | "type": "Standard", 15 | "typical_speakers": ["Dwarves"], 16 | "script": "Dwarvish", 17 | "url": "/api/languages/dwarvish" 18 | }, 19 | { 20 | "index": "elvish", 21 | "name": "Elvish", 22 | "desc": "Elvish is fluid, with subtle intonations and intricate grammar. Elven literature is rich and varied, and their songs and poems are famous among other races. Many bards learn their language so they can add Elvish ballads to their repertoires.", 23 | "type": "Standard", 24 | "typical_speakers": ["Elves"], 25 | "script": "Elvish", 26 | "url": "/api/languages/elvish" 27 | }, 28 | { 29 | "index": "giant", 30 | "name": "Giant", 31 | "type": "Standard", 32 | "typical_speakers": ["Ogres", "Giants"], 33 | "script": "Dwarvish", 34 | "url": "/api/languages/giant" 35 | }, 36 | { 37 | "index": "gnomish", 38 | "name": "Gnomish", 39 | "desc": "The Gnomish language, which uses the Dwarvish script, is renowned for its technical treatises and its catalogs of knowledge about the natural world.", 40 | "type": "Standard", 41 | "typical_speakers": ["Gnomes"], 42 | "script": "Dwarvish", 43 | "url": "/api/languages/gnomish" 44 | }, 45 | { 46 | "index": "goblin", 47 | "name": "Goblin", 48 | "type": "Standard", 49 | "typical_speakers": ["Goblinoids"], 50 | "script": "Dwarvish", 51 | "url": "/api/languages/goblin" 52 | }, 53 | { 54 | "index": "halfling", 55 | "name": "Halfling", 56 | "desc": "The Halfling language isn't secret, but halflings are loath to share it with others. They write very little, so they don't have a rich body of literature. Their oral tradition, however, is very strong.", 57 | "type": "Standard", 58 | "typical_speakers": ["Halflings"], 59 | "script": "Common", 60 | "url": "/api/languages/halfling" 61 | }, 62 | { 63 | "index": "orc", 64 | "name": "Orc", 65 | "desc": "Orc is a harsh, grating language with hard consonants. It has no script of its own but is written in the Dwarvish script.", 66 | "type": "Standard", 67 | "typical_speakers": ["Orcs"], 68 | "script": "Dwarvish", 69 | "url": "/api/languages/orc" 70 | }, 71 | { 72 | "index": "abyssal", 73 | "name": "Abyssal", 74 | "type": "Exotic", 75 | "typical_speakers": ["Demons"], 76 | "script": "Infernal", 77 | "url": "/api/languages/abyssal" 78 | }, 79 | { 80 | "index": "celestial", 81 | "name": "Celestial", 82 | "type": "Exotic", 83 | "typical_speakers": ["Celestials"], 84 | "script": "Celestial", 85 | "url": "/api/languages/celestial" 86 | }, 87 | { 88 | "index": "draconic", 89 | "name": "Draconic", 90 | "desc": "Draconic is thought to be one of the oldest languages and is often used in the study of magic. The language sounds harsh to most other creatures and includes numerous hard consonants and sibilants.", 91 | "type": "Exotic", 92 | "typical_speakers": ["Dragons", "Dragonborn"], 93 | "script": "Draconic", 94 | "url": "/api/languages/draconic" 95 | }, 96 | { 97 | "index": "deep-speech", 98 | "name": "Deep Speech", 99 | "type": "Exotic", 100 | "typical_speakers": ["Aboleths", "Cloakers"], 101 | "url": "/api/languages/deep-speech" 102 | }, 103 | { 104 | "index": "infernal", 105 | "name": "Infernal", 106 | "type": "Exotic", 107 | "typical_speakers": ["Devils"], 108 | "script": "Infernal", 109 | "url": "/api/languages/infernal" 110 | }, 111 | { 112 | "index": "primordial", 113 | "name": "Primordial", 114 | "type": "Exotic", 115 | "typical_speakers": ["Elementals"], 116 | "script": "Dwarvish", 117 | "url": "/api/languages/primordial" 118 | }, 119 | { 120 | "index": "sylvan", 121 | "name": "Sylvan", 122 | "type": "Exotic", 123 | "typical_speakers": ["Fey creatures"], 124 | "script": "Elvish", 125 | "url": "/api/languages/sylvan" 126 | }, 127 | { 128 | "index": "undercommon", 129 | "name": "Undercommon", 130 | "type": "Exotic", 131 | "typical_speakers": ["Underdark traders"], 132 | "script": "Elvish", 133 | "url": "/api/languages/undercommon" 134 | } 135 | ] 136 | -------------------------------------------------------------------------------- /src/api/5e-SRD-Magic-Schools.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "index": "abjuration", 4 | "name": "Abjuration", 5 | "desc": "Abjuration spells are protective in nature, though some of them have aggressive uses. They create magical barriers, negate harmful effects, harm trespassers, or banish creatures to other planes of existence.", 6 | "url": "/api/magic-schools/abjuration" 7 | }, 8 | { 9 | "index": "conjuration", 10 | "name": "Conjuration", 11 | "desc": "Conjuration spells involve the transportation of objects and creatures from one location to another. Some spells summon creatures or objects to the caster's side, whereas others allow the caster to teleport to another location. Some conjurations create objects or effects out of nothing.", 12 | "url": "/api/magic-schools/conjuration" 13 | }, 14 | { 15 | "index": "divination", 16 | "name": "Divination", 17 | "desc": "Divination spells reveal information, whether in the form of secrets long forgotten, glimpses of the future, the locations of hidden things, the truth behind illusions, or visions of distant people or places.", 18 | "url": "/api/magic-schools/divination" 19 | }, 20 | { 21 | "index": "enchantment", 22 | "name": "Enchantment", 23 | "desc": "Enchantment spells affect the minds of others, influencing or controlling their behavior. Such spells can make enemies see the caster as a friend, force creatures to take a course of action, or even control another creature like a puppet.", 24 | "url": "/api/magic-schools/enchantment" 25 | }, 26 | { 27 | "index": "evocation", 28 | "name": "Evocation", 29 | "desc": "Evocation spells manipulate magical energy to produce a desired effect. Some call up blasts of fire or lightning. Others channel positive energy to heal wounds.", 30 | "url": "/api/magic-schools/evocation" 31 | }, 32 | { 33 | "index": "illusion", 34 | "name": "Illusion", 35 | "desc": "Illusion spells deceive the senses or minds of others. They cause people to see things that are not there, to miss things that are there, to hear phantom noises, or to remember things that never happened. Some illusions create phantom images that any creature can see, but the most insidious illusions plant an image directly in the mind of a creature.", 36 | "url": "/api/magic-schools/illusion" 37 | }, 38 | { 39 | "index": "necromancy", 40 | "name": "Necromancy", 41 | "desc": "Necromancy spells manipulate the energies of life and death. Such spells can grant an extra reserve of life force, drain the life energy from another creature, create the undead, or even bring the dead back to life.", 42 | "url": "/api/magic-schools/necromancy" 43 | }, 44 | { 45 | "index": "transmutation", 46 | "name": "Transmutation", 47 | "desc": "Transmutation spells change the properties of a creature, object, or environment. They might turn an enemy into a harmless creature, bolster the strength of an ally, make an object move at the caster's command, or enhance a creature's innate healing abilities to rapidly recover from injury.", 48 | "url": "/api/magic-schools/transmutation" 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /src/api/5e-SRD-Rules.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Combat", 4 | "index": "combat", 5 | "desc": "# Combat\n", 6 | "subsections": [ 7 | { 8 | "name": "The Order of Combat", 9 | "index": "the-order-of-combat", 10 | "url": "/api/rule-sections/the-order-of-combat" 11 | }, 12 | { 13 | "name": "Movement and Position", 14 | "index": "movement-and-position", 15 | "url": "/api/rule-sections/movement-and-position" 16 | }, 17 | { 18 | "name": "Actions in Combat", 19 | "index": "actions-in-combat", 20 | "url": "/api/rule-sections/actions-in-combat" 21 | }, 22 | { 23 | "name": "Making an Attack", 24 | "index": "making-an-attack", 25 | "url": "/api/rule-sections/making-an-attack" 26 | }, 27 | { 28 | "name": "Cover", 29 | "index": "cover", 30 | "url": "/api/rule-sections/cover" 31 | }, 32 | { 33 | "name": "Damage and Healing", 34 | "index": "damage-and-healing", 35 | "url": "/api/rule-sections/damage-and-healing" 36 | }, 37 | { 38 | "name": "Mounted Combat", 39 | "index": "mounted-combat", 40 | "url": "/api/rule-sections/mounted-combat" 41 | }, 42 | { 43 | "name": "Underwater Combat", 44 | "index": "underwater-combat", 45 | "url": "/api/rule-sections/underwater-combat" 46 | } 47 | ], 48 | "url": "/api/rules/combat" 49 | }, 50 | { 51 | "name": "Using Ability Scores", 52 | "index": "using-ability-scores", 53 | "desc": "# Using Ability Scores\n\nSix abilities provide a quick description of every creature's physical and mental characteristics:\n- **Strength**, measuring physical power\n- **Dexterity**, measuring agility\n- **Constitution**, measuring endurance\n- **Intelligence**, measuring reasoning and memory\n- **Wisdom**, measuring perception and insight\n- **Charisma**, measuring force of personality\n\nIs a character muscle-bound and insightful? Brilliant and charming? Nimble and hardy? Ability scores define these qualities-a creature's assets as well as weaknesses.\n\nThe three main rolls of the game-the ability check, the saving throw, and the attack roll-rely on the six ability scores. The book's introduction describes the basic rule behind these rolls: roll a d20, add an ability modifier derived from one of the six ability scores, and compare the total to a target number.\n\n**Ability Scores and Modifiers** Each of a creature's abilities has a score, a number that defines the magnitude of that ability. An ability score is not just a measure of innate capabilities, but also encompasses a creature's training and competence in activities related to that ability.\n\nA score of 10 or 11 is the normal human average, but adventurers and many monsters are a cut above average in most abilities. A score of 18 is the highest that a person usually reaches. Adventurers can have scores as high as 20, and monsters and divine beings can have scores as high as 30.\n\nEach ability also has a modifier, derived from the score and ranging from -5 (for an ability score of 1) to +10 (for a score of 30). The Ability Scores and Modifiers table notes the ability modifiers for the range of possible ability scores, from 1 to 30.\n", 54 | "subsections": [ 55 | { 56 | "name": "Ability Scores and Modifiers", 57 | "index": "ability-scores-and-modifiers", 58 | "url": "/api/rule-sections/ability-scores-and-modifiers" 59 | }, 60 | { 61 | "name": "Advantage and Disadvantage", 62 | "index": "advantage-and-disadvantage", 63 | "url": "/api/rule-sections/advantage-and-disadvantage" 64 | }, 65 | { 66 | "name": "Proficiency Bonus", 67 | "index": "proficiency-bonus", 68 | "url": "/api/rule-sections/proficiency-bonus" 69 | }, 70 | { 71 | "name": "Ability Checks", 72 | "index": "ability-checks", 73 | "url": "/api/rule-sections/ability-checks" 74 | }, 75 | { 76 | "name": "Using Each Ability", 77 | "index": "using-each-ability", 78 | "url": "/api/rule-sections/using-each-ability" 79 | }, 80 | { 81 | "name": "Saving Throws", 82 | "index": "saving-throws", 83 | "url": "/api/rule-sections/saving-throws" 84 | } 85 | ], 86 | "url": "/api/rules/using-ability-scores" 87 | }, 88 | { 89 | "name": "Adventuring", 90 | "index": "adventuring", 91 | "desc": "# Adventuring\n", 92 | "subsections": [ 93 | { 94 | "name": "Time", 95 | "index": "time", 96 | "url": "/api/rule-sections/time" 97 | }, 98 | { 99 | "name": "Movement", 100 | "index": "movement", 101 | "url": "/api/rule-sections/movement" 102 | }, 103 | { 104 | "name": "The Environment", 105 | "index": "the-environment", 106 | "url": "/api/rule-sections/the-environment" 107 | }, 108 | { 109 | "name": "Traps", 110 | "index": "traps", 111 | "url": "/api/rule-sections/traps" 112 | }, 113 | { 114 | "name": "Diseases", 115 | "index": "diseases", 116 | "url": "/api/rule-sections/diseases" 117 | }, 118 | { 119 | "name": "Madness", 120 | "index": "madness", 121 | "url": "/api/rule-sections/madness" 122 | }, 123 | { 124 | "name": "Resting", 125 | "index": "resting", 126 | "url": "/api/rule-sections/resting" 127 | }, 128 | { 129 | "name": "Between Adventures", 130 | "index": "between-adventures", 131 | "url": "/api/rule-sections/between-adventures" 132 | } 133 | ], 134 | "url": "/api/rules/adventuring" 135 | }, 136 | { 137 | "name": "Spellcasting", 138 | "index": "spellcasting", 139 | "desc": "# Spellcasting\n\nMagic permeates fantasy gaming worlds and often appears in the form of a spell.\n\nThis chapter provides the rules for casting spells. Different character classes have distinctive ways of learning and preparing their spells, and monsters use spells in unique ways. Regardless of its source, a spell follows the rules here.\n", 140 | "subsections": [ 141 | { 142 | "name": "What Is a Spell?", 143 | "index": "what-is-a-spell", 144 | "url": "/api/rule-sections/what-is-a-spell" 145 | }, 146 | { 147 | "name": "Casting a Spell", 148 | "index": "casting-a-spell", 149 | "url": "/api/rule-sections/casting-a-spell" 150 | } 151 | ], 152 | "url": "/api/rules/spellcasting" 153 | }, 154 | { 155 | "name": "Equipment", 156 | "index": "equipment", 157 | "desc": "# Equipment\n\nCommon coins come in several different denominations based on the relative worth of the metal from which they are made. The three most common coins are the gold piece (gp), the silver piece (sp), and the copper piece (cp).\n\nWith one gold piece, a character can buy a bedroll, 50 feet of good rope, or a goat. A skilled (but not exceptional) artisan can earn one gold piece a day. The old piece is the standard unit of measure for wealth, even if the coin itself is not commonly used. When merchants discuss deals that involve goods or services worth hundreds or thousands of gold pieces, the transactions don't usually involve the exchange of individual coins. Rather, the gold piece is a standard measure of value, and the actual exchange is in gold bars, letters of credit, or valuable goods.\n\nOne gold piece is worth ten silver pieces, the most prevalent coin among commoners. A silver piece buys a laborer's work for half a day, a flask of lamp oil, or a night's rest in a poor inn.\n\nOne silver piece is worth ten copper pieces, which are common among laborers and beggars. A single copper piece buys a candle, a torch, or a piece of chalk.\n\nIn addition, unusual coins made of other precious metals sometimes appear in treasure hoards. The electrum piece (ep) and the platinum piece (pp) originate from fallen empires and lost kingdoms, and they sometimes arouse suspicion and skepticism when used in transactions. An electrum piece is worth five silver pieces, and a platinum piece is worth ten gold pieces.\n\nA standard coin weighs about a third of an ounce, so fifty coins weigh a pound.\n", 158 | "subsections": [ 159 | { 160 | "name": "Standard Exchange Rates", 161 | "index": "standard-exchange-rates", 162 | "url": "/api/rule-sections/standard-exchange-rates" 163 | }, 164 | { 165 | "name": "Objects", 166 | "index": "objects", 167 | "url": "/api/rule-sections/objects" 168 | }, 169 | { 170 | "name": "Poisons", 171 | "index": "poisons", 172 | "url": "/api/rule-sections/poisons" 173 | }, 174 | { 175 | "name": "Attunement", 176 | "index": "attunement", 177 | "url": "/api/rule-sections/attunement" 178 | }, 179 | { 180 | "name": "Wearing and Wielding Items", 181 | "index": "wearing-and-wielding-items", 182 | "url": "/api/rule-sections/wearing-and-wielding-items" 183 | }, 184 | { 185 | "name": "Activating an Item", 186 | "index": "activating-an-item", 187 | "url": "/api/rule-sections/activating-an-item" 188 | }, 189 | { 190 | "name": "Sentient Magic Items", 191 | "index": "sentient-magic-items", 192 | "url": "/api/rule-sections/sentient-magic-items" 193 | } 194 | ], 195 | "url": "/api/rules/equipment" 196 | }, 197 | { 198 | "name": "Appendix", 199 | "index": "appendix", 200 | "desc": "# Appendix\n", 201 | "subsections": [ 202 | { 203 | "name": "Fantasy-Historical Pantheons", 204 | "index": "fantasy-historical-pantheons", 205 | "url": "/api/rule-sections/fantasy-historical-pantheons" 206 | }, 207 | { 208 | "name": "The Planes of Existence", 209 | "index": "the-planes-of-existence", 210 | "url": "/api/rule-sections/the-planes-of-existence" 211 | } 212 | ], 213 | "url": "/api/rules/appendix" 214 | } 215 | ] 216 | -------------------------------------------------------------------------------- /src/api/5e-SRD-Skills.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "index": "acrobatics", 4 | "name": "Acrobatics", 5 | "desc": [ 6 | "Your Dexterity (Acrobatics) check covers your attempt to stay on your feet in a tricky situation, such as when you're trying to run across a sheet of ice, balance on a tightrope, or stay upright on a rocking ship's deck. The GM might also call for a Dexterity (Acrobatics) check to see if you can perform acrobatic stunts, including dives, rolls, somersaults, and flips." 7 | ], 8 | "ability_score": { 9 | "index": "dex", 10 | "name": "DEX", 11 | "url": "/api/ability-scores/dex" 12 | }, 13 | "url": "/api/skills/acrobatics" 14 | }, 15 | { 16 | "index": "animal-handling", 17 | "name": "Animal Handling", 18 | "desc": [ 19 | "When there is any question whether you can calm down a domesticated animal, keep a mount from getting spooked, or intuit an animal's intentions, the GM might call for a Wisdom (Animal Handling) check. You also make a Wisdom (Animal Handling) check to control your mount when you attempt a risky maneuver." 20 | ], 21 | "ability_score": { 22 | "index": "wis", 23 | "name": "WIS", 24 | "url": "/api/ability-scores/wis" 25 | }, 26 | "url": "/api/skills/animal-handling" 27 | }, 28 | { 29 | "index": "arcana", 30 | "name": "Arcana", 31 | "desc": [ 32 | "Your Intelligence (Arcana) check measures your ability to recall lore about spells, magic items, eldritch symbols, magical traditions, the planes of existence, and the inhabitants of those planes." 33 | ], 34 | "ability_score": { 35 | "index": "int", 36 | "name": "INT", 37 | "url": "/api/ability-scores/int" 38 | }, 39 | "url": "/api/skills/arcana" 40 | }, 41 | { 42 | "index": "athletics", 43 | "name": "Athletics", 44 | "desc": [ 45 | "Your Strength (Athletics) check covers difficult situations you encounter while climbing, jumping, or swimming." 46 | ], 47 | "ability_score": { 48 | "index": "str", 49 | "name": "STR", 50 | "url": "/api/ability-scores/str" 51 | }, 52 | "url": "/api/skills/athletics" 53 | }, 54 | { 55 | "index": "deception", 56 | "name": "Deception", 57 | "desc": [ 58 | "Your Charisma (Deception) check determines whether you can convincingly hide the truth, either verbally or through your actions. This deception can encompass everything from misleading others through ambiguity to telling outright lies. Typical situations include trying to fast- talk a guard, con a merchant, earn money through gambling, pass yourself off in a disguise, dull someone's suspicions with false assurances, or maintain a straight face while telling a blatant lie." 59 | ], 60 | "ability_score": { 61 | "index": "cha", 62 | "name": "CHA", 63 | "url": "/api/ability-scores/cha" 64 | }, 65 | "url": "/api/skills/deception" 66 | }, 67 | { 68 | "index": "history", 69 | "name": "History", 70 | "desc": [ 71 | "Your Intelligence (History) check measures your ability to recall lore about historical events, legendary people, ancient kingdoms, past disputes, recent wars, and lost civilizations." 72 | ], 73 | "ability_score": { 74 | "index": "int", 75 | "name": "INT", 76 | "url": "/api/ability-scores/int" 77 | }, 78 | "url": "/api/skills/history" 79 | }, 80 | { 81 | "index": "insight", 82 | "name": "Insight", 83 | "desc": [ 84 | "Your Wisdom (Insight) check decides whether you can determine the true intentions of a creature, such as when searching out a lie or predicting someone's next move. Doing so involves gleaning clues from body language, speech habits, and changes in mannerisms." 85 | ], 86 | "ability_score": { 87 | "index": "wis", 88 | "name": "WIS", 89 | "url": "/api/ability-scores/wis" 90 | }, 91 | "url": "/api/skills/insight" 92 | }, 93 | { 94 | "index": "intimidation", 95 | "name": "Intimidation", 96 | "desc": [ 97 | "When you attempt to influence someone through overt threats, hostile actions, and physical violence, the GM might ask you to make a Charisma (Intimidation) check. Examples include trying to pry information out of a prisoner, convincing street thugs to back down from a confrontation, or using the edge of a broken bottle to convince a sneering vizier to reconsider a decision." 98 | ], 99 | "ability_score": { 100 | "index": "cha", 101 | "name": "CHA", 102 | "url": "/api/ability-scores/cha" 103 | }, 104 | "url": "/api/skills/intimidation" 105 | }, 106 | { 107 | "index": "investigation", 108 | "name": "Investigation", 109 | "desc": [ 110 | "When you look around for clues and make deductions based on those clues, you make an Intelligence (Investigation) check. You might deduce the location of a hidden object, discern from the appearance of a wound what kind of weapon dealt it, or determine the weakest point in a tunnel that could cause it to collapse. Poring through ancient scrolls in search of a hidden fragment of knowledge might also call for an Intelligence (Investigation) check." 111 | ], 112 | "ability_score": { 113 | "index": "int", 114 | "name": "INT", 115 | "url": "/api/ability-scores/int" 116 | }, 117 | "url": "/api/skills/investigation" 118 | }, 119 | { 120 | "index": "medicine", 121 | "name": "Medicine", 122 | "desc": [ 123 | "A Wisdom (Medicine) check lets you try to stabilize a dying companion or diagnose an illness." 124 | ], 125 | "ability_score": { 126 | "index": "wis", 127 | "name": "WIS", 128 | "url": "/api/ability-scores/wis" 129 | }, 130 | "url": "/api/skills/medicine" 131 | }, 132 | { 133 | "index": "nature", 134 | "name": "Nature", 135 | "desc": [ 136 | "Your Intelligence (Nature) check measures your ability to recall lore about terrain, plants and animals, the weather, and natural cycles." 137 | ], 138 | "ability_score": { 139 | "index": "int", 140 | "name": "INT", 141 | "url": "/api/ability-scores/int" 142 | }, 143 | "url": "/api/skills/nature" 144 | }, 145 | { 146 | "index": "perception", 147 | "name": "Perception", 148 | "desc": [ 149 | "Your Wisdom (Perception) check lets you spot, hear, or otherwise detect the presence of something. It measures your general awareness of your surroundings and the keenness of your senses. For example, you might try to hear a conversation through a closed door, eavesdrop under an open window, or hear monsters moving stealthily in the forest. Or you might try to spot things that are obscured or easy to miss, whether they are orcs lying in ambush on a road, thugs hiding in the shadows of an alley, or candlelight under a closed secret door." 150 | ], 151 | "ability_score": { 152 | "index": "wis", 153 | "name": "WIS", 154 | "url": "/api/ability-scores/wis" 155 | }, 156 | "url": "/api/skills/perception" 157 | }, 158 | { 159 | "index": "performance", 160 | "name": "Performance", 161 | "desc": [ 162 | "Your Charisma (Performance) check determines how well you can delight an audience with music, dance, acting, storytelling, or some other form of entertainment." 163 | ], 164 | "ability_score": { 165 | "index": "cha", 166 | "name": "CHA", 167 | "url": "/api/ability-scores/cha" 168 | }, 169 | "url": "/api/skills/performance" 170 | }, 171 | { 172 | "index": "persuasion", 173 | "name": "Persuasion", 174 | "desc": [ 175 | "When you attempt to influence someone or a group of people with tact, social graces, or good nature, the GM might ask you to make a Charisma (Persuasion) check. Typically, you use persuasion when acting in good faith, to foster friendships, make cordial requests, or exhibit proper etiquette. Examples of persuading others include convincing a chamberlain to let your party see the king, negotiating peace between warring tribes, or inspiring a crowd of townsfolk." 176 | ], 177 | "ability_score": { 178 | "index": "cha", 179 | "name": "CHA", 180 | "url": "/api/ability-scores/cha" 181 | }, 182 | "url": "/api/skills/persuasion" 183 | }, 184 | { 185 | "index": "religion", 186 | "name": "Religion", 187 | "desc": [ 188 | "Your Intelligence (Religion) check measures your ability to recall lore about deities, rites and prayers, religious hierarchies, holy symbols, and the practices of secret cults." 189 | ], 190 | "ability_score": { 191 | "index": "int", 192 | "name": "INT", 193 | "url": "/api/ability-scores/int" 194 | }, 195 | "url": "/api/skills/religion" 196 | }, 197 | { 198 | "index": "sleight-of-hand", 199 | "name": "Sleight of Hand", 200 | "desc": [ 201 | "Whenever you attempt an act of legerdemain or manual trickery, such as planting something on someone else or concealing an object on your person, make a Dexterity (Sleight of Hand) check. The GM might also call for a Dexterity (Sleight of Hand) check to determine whether you can lift a coin purse off another person or slip something out of another person's pocket." 202 | ], 203 | "ability_score": { 204 | "index": "dex", 205 | "name": "DEX", 206 | "url": "/api/ability-scores/dex" 207 | }, 208 | "url": "/api/skills/sleight-of-hand" 209 | }, 210 | { 211 | "index": "stealth", 212 | "name": "Stealth", 213 | "desc": [ 214 | "Make a Dexterity (Stealth) check when you attempt to conceal yourself from enemies, slink past guards, slip away without being noticed, or sneak up on someone without being seen or heard." 215 | ], 216 | "ability_score": { 217 | "index": "dex", 218 | "name": "DEX", 219 | "url": "/api/ability-scores/dex" 220 | }, 221 | "url": "/api/skills/stealth" 222 | }, 223 | { 224 | "index": "survival", 225 | "name": "Survival", 226 | "desc": [ 227 | "The GM might ask you to make a Wisdom (Survival) check to follow tracks, hunt wild game, guide your group through frozen wastelands, identify signs that owlbears live nearby, predict the weather, or avoid quicksand and other natural hazards." 228 | ], 229 | "ability_score": { 230 | "index": "wis", 231 | "name": "WIS", 232 | "url": "/api/ability-scores/wis" 233 | }, 234 | "url": "/api/skills/survival" 235 | } 236 | ] 237 | -------------------------------------------------------------------------------- /src/api/5e-SRD-Subraces.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "index": "hill-dwarf", 4 | "name": "Hill Dwarf", 5 | "race": { 6 | "index": "dwarf", 7 | "name": "Dwarf", 8 | "url": "/api/races/dwarf" 9 | }, 10 | "desc": "As a hill dwarf, you have keen senses, deep intuition, and remarkable resilience.", 11 | "ability_bonuses": [ 12 | { 13 | "ability_score": { 14 | "index": "wis", 15 | "name": "WIS", 16 | "url": "/api/ability-scores/wis" 17 | }, 18 | "bonus": 1 19 | } 20 | ], 21 | "starting_proficiencies": [], 22 | "languages": [], 23 | "racial_traits": [ 24 | { 25 | "index": "dwarven-toughness", 26 | "name": "Dwarven Toughness", 27 | "url": "/api/traits/dwarven-toughness" 28 | } 29 | ], 30 | "url": "/api/subraces/hill-dwarf" 31 | }, 32 | { 33 | "index": "high-elf", 34 | "name": "High Elf", 35 | "race": { 36 | "index": "elf", 37 | "name": "Elf", 38 | "url": "/api/races/elf" 39 | }, 40 | "desc": "As a high elf, you have a keen mind and a mastery of at least the basics of magic. In many fantasy gaming worlds, there are two kinds of high elves. One type is haughty and reclusive, believing themselves to be superior to non-elves and even other elves. The other type is more common and more friendly, and often encountered among humans and other races.", 41 | "ability_bonuses": [ 42 | { 43 | "ability_score": { 44 | "index": "int", 45 | "name": "INT", 46 | "url": "/api/ability-scores/int" 47 | }, 48 | "bonus": 1 49 | } 50 | ], 51 | "starting_proficiencies": [ 52 | { 53 | "index": "longswords", 54 | "name": "Longswords", 55 | "url": "/api/proficiencies/longswords" 56 | }, 57 | { 58 | "index": "shortswords", 59 | "name": "Shortswords", 60 | "url": "/api/proficiencies/shortswords" 61 | }, 62 | { 63 | "index": "shortbows", 64 | "name": "Shortbows", 65 | "url": "/api/proficiencies/shortbows" 66 | }, 67 | { 68 | "index": "longbows", 69 | "name": "Longbows", 70 | "url": "/api/proficiencies/longbows" 71 | } 72 | ], 73 | "languages": [], 74 | "language_options": { 75 | "choose": 1, 76 | "from": { 77 | "option_set_type": "options_array", 78 | "options": [ 79 | { 80 | "option_type": "reference", 81 | "item": { 82 | "index": "dwarvish", 83 | "name": "Dwarvish", 84 | "url": "/api/languages/dwarvish" 85 | } 86 | }, 87 | { 88 | "option_type": "reference", 89 | "item": { 90 | "index": "giant", 91 | "name": "Giant", 92 | "url": "/api/languages/giant" 93 | } 94 | }, 95 | { 96 | "option_type": "reference", 97 | "item": { 98 | "index": "gnomish", 99 | "name": "Gnomish", 100 | "url": "/api/languages/gnomish" 101 | } 102 | }, 103 | { 104 | "option_type": "reference", 105 | "item": { 106 | "index": "goblin", 107 | "name": "Goblin", 108 | "url": "/api/languages/goblin" 109 | } 110 | }, 111 | { 112 | "option_type": "reference", 113 | "item": { 114 | "index": "halfling", 115 | "name": "Halfling", 116 | "url": "/api/languages/halfling" 117 | } 118 | }, 119 | { 120 | "option_type": "reference", 121 | "item": { 122 | "index": "orc", 123 | "name": "Orc", 124 | "url": "/api/languages/orc" 125 | } 126 | }, 127 | { 128 | "option_type": "reference", 129 | "item": { 130 | "index": "abyssal", 131 | "name": "Abyssal", 132 | "url": "/api/languages/abyssal" 133 | } 134 | }, 135 | { 136 | "option_type": "reference", 137 | "item": { 138 | "index": "celestial", 139 | "name": "Celestial", 140 | "url": "/api/languages/celestial" 141 | } 142 | }, 143 | { 144 | "option_type": "reference", 145 | "item": { 146 | "index": "draconic", 147 | "name": "Draconic", 148 | "url": "/api/languages/draconic" 149 | } 150 | }, 151 | { 152 | "option_type": "reference", 153 | "item": { 154 | "index": "deep-speech", 155 | "name": "Deep Speech", 156 | "url": "/api/languages/deep-speech" 157 | } 158 | }, 159 | { 160 | "option_type": "reference", 161 | "item": { 162 | "index": "infernal", 163 | "name": "Infernal", 164 | "url": "/api/languages/infernal" 165 | } 166 | }, 167 | { 168 | "option_type": "reference", 169 | "item": { 170 | "index": "primordial", 171 | "name": "Primordial", 172 | "url": "/api/languages/primordial" 173 | } 174 | }, 175 | { 176 | "option_type": "reference", 177 | "item": { 178 | "index": "sylvan", 179 | "name": "Sylvan", 180 | "url": "/api/languages/sylvan" 181 | } 182 | }, 183 | { 184 | "option_type": "reference", 185 | "item": { 186 | "index": "undercommon", 187 | "name": "Undercommon", 188 | "url": "/api/languages/undercommon" 189 | } 190 | } 191 | ] 192 | }, 193 | "type": "language" 194 | }, 195 | "racial_traits": [ 196 | { 197 | "index": "elf-weapon-training", 198 | "name": "Elf Weapon Training", 199 | "url": "/api/traits/elf-weapon-training" 200 | }, 201 | { 202 | "index": "high-elf-cantrip", 203 | "name": "High Elf Cantrip", 204 | "url": "/api/traits/high-elf-cantrip" 205 | }, 206 | { 207 | "index": "extra-language", 208 | "name": "Extra Language", 209 | "url": "/api/traits/extra-language" 210 | } 211 | ], 212 | "url": "/api/subraces/high-elf" 213 | }, 214 | { 215 | "index": "lightfoot-halfling", 216 | "name": "Lightfoot Halfling", 217 | "race": { 218 | "index": "halfling", 219 | "name": "Halfling", 220 | "url": "/api/races/halfling" 221 | }, 222 | "desc": "As a lightfoot halfling, you can easily hide from notice, even using other people as cover. You're inclined to be affable and get along well with others. Lightfoots are more prone to wanderlust than other halflings, and often dwell alongside other races or take up a nomadic life.", 223 | "ability_bonuses": [ 224 | { 225 | "ability_score": { 226 | "index": "cha", 227 | "name": "CHA", 228 | "url": "/api/ability-scores/cha" 229 | }, 230 | "bonus": 1 231 | } 232 | ], 233 | "starting_proficiencies": [], 234 | "languages": [], 235 | "racial_traits": [ 236 | { 237 | "index": "naturally-stealthy", 238 | "name": "Naturally Stealthy", 239 | "url": "/api/traits/naturally-stealthy" 240 | } 241 | ], 242 | "url": "/api/subraces/lightfoot-halfling" 243 | }, 244 | { 245 | "index": "rock-gnome", 246 | "name": "Rock Gnome", 247 | "race": { 248 | "index": "gnome", 249 | "name": "Gnome", 250 | "url": "/api/races/gnome" 251 | }, 252 | "desc": "As a rock gnome, you have a natural inventiveness and hardiness beyond that of other gnomes.", 253 | "ability_bonuses": [ 254 | { 255 | "ability_score": { 256 | "index": "con", 257 | "name": "CON", 258 | "url": "/api/ability-scores/con" 259 | }, 260 | "bonus": 1 261 | } 262 | ], 263 | "starting_proficiencies": [ 264 | { 265 | "index": "tinkers-tools", 266 | "name": "Tinker's Tools", 267 | "url": "/api/proficiencies/tinkers-tools" 268 | } 269 | ], 270 | "languages": [], 271 | "racial_traits": [ 272 | { 273 | "index": "artificers-lore", 274 | "name": "Artificer's Lore", 275 | "url": "/api/traits/artificers-lore" 276 | }, 277 | { 278 | "index": "tinker", 279 | "name": "Tinker", 280 | "url": "/api/traits/tinker" 281 | } 282 | ], 283 | "url": "/api/subraces/rock-gnome" 284 | } 285 | ] 286 | -------------------------------------------------------------------------------- /src/api/5e-SRD-Weapon-Properties.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "index": "ammunition", 4 | "name": "Ammunition", 5 | "desc": [ 6 | "You can use a weapon that has the ammunition property to make a ranged attack only if you have ammunition to fire from the weapon. Each time you attack with the weapon, you expend one piece of ammunition. Drawing the ammunition from a quiver, case, or other container is part of the attack (you need a free hand to load a one-handed weapon).", 7 | "At the end of the battle, you can recover half your expended ammunition by taking a minute to search the battlefield. If you use a weapon that has the ammunition property to make a melee attack, you treat the weapon as an improvised weapon (see \"Improvised Weapons\" later in the section). A sling must be loaded to deal any damage when used in this way." 8 | ], 9 | "url": "/api/weapon-properties/ammunition" 10 | }, 11 | { 12 | "index": "finesse", 13 | "name": "Finesse", 14 | "desc": [ 15 | "When making an attack with a finesse weapon, you use your choice of your Strength or Dexterity modifier for the attack and damage rolls. You must use the same modifier for both rolls." 16 | ], 17 | "url": "/api/weapon-properties/finesse" 18 | }, 19 | { 20 | "index": "heavy", 21 | "name": "Heavy", 22 | "desc": [ 23 | "Small creatures have disadvantage on attack rolls with heavy weapons. A heavy weapon's size and bulk make it too large for a Small creature to use effectively." 24 | ], 25 | "url": "/api/weapon-properties/heavy" 26 | }, 27 | { 28 | "index": "light", 29 | "name": "Light", 30 | "desc": [ 31 | "A light weapon is small and easy to handle, making it ideal for use when fighting with two weapons." 32 | ], 33 | "url": "/api/weapon-properties/light" 34 | }, 35 | { 36 | "index": "loading", 37 | "name": "Loading", 38 | "desc": [ 39 | "Because of the time required to load this weapon, you can fire only one piece of ammunition from it when you use an action, bonus action, or reaction to fire it, regardless of the number of attacks you can normally make." 40 | ], 41 | "url": "/api/weapon-properties/loading" 42 | }, 43 | { 44 | "index": "reach", 45 | "name": "Reach", 46 | "desc": [ 47 | "This weapon adds 5 feet to your reach when you attack with it, as well as when determining your reach for opportunity attacks with it." 48 | ], 49 | "url": "/api/weapon-properties/reach" 50 | }, 51 | { 52 | "index": "special", 53 | "name": "Special", 54 | "desc": [ 55 | "A weapon with the special property has unusual rules governing its use, explained in the weapon's description (see \"Special Weapons\" later in this section)." 56 | ], 57 | "url": "/api/weapon-properties/special" 58 | }, 59 | { 60 | "index": "thrown", 61 | "name": "Thrown", 62 | "desc": [ 63 | "If a weapon has the thrown property, you can throw the weapon to make a ranged attack. If the weapon is a melee weapon, you use the same ability modifier for that attack roll and damage roll that you would use for a melee attack with the weapon. For example, if you throw a handaxe, you use your Strength, but if you throw a dagger, you can use either your Strength or your Dexterity, since the dagger has the finesse property." 64 | ], 65 | "url": "/api/weapon-properties/thrown" 66 | }, 67 | { 68 | "index": "two-handed", 69 | "name": "Two-Handed", 70 | "desc": ["This weapon requires two hands when you attack with it."], 71 | "url": "/api/weapon-properties/two-handed" 72 | }, 73 | { 74 | "index": "versatile", 75 | "name": "Versatile", 76 | "desc": [ 77 | "This weapon can be used with one or two hands. A damage value in parentheses appears with the property--the damage when the weapon is used with two hands to make a melee attack." 78 | ], 79 | "url": "/api/weapon-properties/versatile" 80 | }, 81 | { 82 | "index": "monk", 83 | "name": "Monk", 84 | "desc": [ 85 | "Monks gain several benefits while unarmed or wielding only monk weapons while they aren't wearing armor or wielding shields." 86 | ], 87 | "url": "/api/weapon-properties/monk" 88 | } 89 | ] 90 | -------------------------------------------------------------------------------- /src/character_sheet.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | import GObject from 'gi://GObject'; 4 | import Gtk from 'gi://Gtk'; 5 | import Gdk from 'gi://Gdk'; 6 | import GdkPixbuf from 'gi://GdkPixbuf'; 7 | import Gio from 'gi://Gio'; 8 | import GLib from 'gi://GLib'; 9 | import Adw from 'gi://Adw'; 10 | // for some reason this doesn't work 11 | // import { Tab } from "./window.js"; 12 | import { score_to_modifier } from "./window.js"; 13 | const Tab = GObject.registerClass({ 14 | GTypeName: 'Tab2', 15 | }, class extends Adw.NavigationPage { 16 | constructor() { 17 | super({}); 18 | } 19 | }); 20 | const Draggable = GObject.registerClass({ 21 | GTypeName: 'Draggable', 22 | }, class extends Gtk.Box { 23 | constructor(label, short) { 24 | super( { 25 | css_classes: ["card"], 26 | halign: Gtk.Align.CENTER, valign: Gtk.Align.CENTER, 27 | orientation: Gtk.Orientation.VERTICAL, 28 | margin_top: 15, margin_bottom: 15, 29 | spacing: 5, 30 | height_request: 118 } ); 31 | this.short = short; 32 | this.modifiers = new Gtk.Box(); 33 | this.append(this.modifiers); 34 | 35 | let drag_x; 36 | let drag_y; 37 | 38 | this.drag = new Gtk.DragSource(); 39 | this.drag.connect("prepare", (_source, x, y) => { 40 | drag_x = x; 41 | drag_y = y; 42 | const value = new GObject.Value(); 43 | value.init(Gtk.Box); 44 | value.set_object(this); 45 | return Gdk.ContentProvider.new_for_value(value); 46 | }); 47 | 48 | this.drag.connect("drag-begin", (_source, drag) => { 49 | const drag_widget = new Adw.Bin( { 50 | css_classes: ["card"], 51 | margin_start: 5, margin_top: 5, margin_end: 5, margin_bottom: 5} ); 52 | 53 | drag_widget.set_size_request(100, 30); 54 | drag_widget.child = new Gtk.Label( { label: short } ); 55 | 56 | const icon = Gtk.DragIcon.get_for_drag(drag); 57 | icon.child = drag_widget; 58 | 59 | drag.set_hotspot(drag_x, drag_y); 60 | }); 61 | 62 | this.add_controller(this.drag); 63 | 64 | 65 | 66 | this.drop = Gtk.DropTarget.new(Gtk.Box, Gdk.DragAction.COPY); 67 | this.drop.connect("accept", (data) => { log(data); return true; } ); 68 | this.drop.connect("drop", (_, data, x, y) => { log("------------------"); log(data); log(x); log(y); return true; } ); 69 | this.add_controller(this.drop); 70 | } 71 | }); 72 | 73 | 74 | const StatCard = GObject.registerClass({ 75 | GTypeName: 'StatCard', 76 | }, class extends Draggable { 77 | constructor(name, value) { 78 | super(name, name); 79 | 80 | let check = Gtk.CheckButton.new_with_label(name); 81 | check.css_classes = ["flat"]; 82 | check.margin_top = 10; 83 | check.halign = Gtk.Align.CENTER; 84 | check.valign = Gtk.Align.CENTER; 85 | this.append(check); 86 | let modifier = new Gtk.Label( { css_classes: ["title-1"], label: score_to_modifier(value) } ); 87 | this.append(modifier); 88 | let spin = new Gtk.SpinButton( { 89 | adjustment: new Gtk.Adjustment( { lower: 1, upper: 20, step_increment: 1, value: value } ), 90 | margin_start: 5, margin_end: 5, margin_bottom: 5, 91 | css_classes: ["flat"] } ); 92 | spin.connect("notify::value", () => { modifier.label = score_to_modifier(spin.value); }); 93 | this.append(spin); 94 | } 95 | }); 96 | 97 | const Value = GObject.registerClass({ 98 | GTypeName: 'Value', 99 | }, class extends Draggable { 100 | constructor(name, value, lower, upper, step) { 101 | super( { } ); 102 | let label = new Gtk.Label( { margin_top: 10, halign: Gtk.Align.CENTER, valign: Gtk.Align.CENTER, label: name } ); 103 | this.append(label); 104 | let spin = new Gtk.SpinButton( { 105 | adjustment: new Gtk.Adjustment( { lower: lower, upper: upper, step_increment: step, value: value } ), 106 | margin_start: 5, margin_end: 5, margin_bottom: 5, 107 | valign: Gtk.Align.CENTER, 108 | css_classes: ["flat"] } ); 109 | spin.connect("notify::value", () => { modifier.label = score_to_modifier(spin.value); }); 110 | this.append(spin); 111 | } 112 | }); 113 | 114 | export const SheetTab = GObject.registerClass({ 115 | GTypeName: 'SheetTab', 116 | }, class extends Tab { 117 | constructor(applied_filters, navigation_view) { 118 | super({}); 119 | setTimeout(() => { this.navigation_view.tab_page.set_title("Character Sheet"); }, 1); 120 | this.navigation_view = navigation_view; 121 | this.set_hexpand(true) 122 | this.navigation_view.append(this); 123 | 124 | this.scrolled_window = new Gtk.ScrolledWindow(); 125 | this.scrolled_window.set_halign(Gtk.Align.FILL); 126 | this.scrolled_window.set_hexpand(true); 127 | this.scrolled_window.set_size_request(400, 0); 128 | 129 | this.scrolled_window.add_css_class("undershoot-top"); 130 | this.scrolled_window.add_css_class("undershoot-bottom"); 131 | 132 | this.append(this.scrolled_window); 133 | 134 | this.back_wrapper = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL }); 135 | this.scrolled_window.set_child(this.back_wrapper); 136 | 137 | this.pin = new Gtk.Button({ 138 | icon_name: "view-pin-symbolic", 139 | halign: Gtk.Align.END, hexpand: true, 140 | margin_top: 20, margin_end: 20 }); 141 | // if (this.navigation_view.tab_page.pinned) this.pin.add_css_class("success"); 142 | this.pin.connect("clicked", () => { 143 | this.navigation_view.tab_view.set_page_pinned(this.navigation_view.tab_page, !this.navigation_view.tab_page.pinned); 144 | if (this.navigation_view.tab_page.pinned) this.pin.set_css_classes(["success"]); 145 | else this.pin.set_css_classes([]); 146 | } ); 147 | 148 | 149 | this.bar = new Gtk.Box(); 150 | this.back_wrapper.append(this.bar); 151 | 152 | this.back = new Gtk.Button({ icon_name: "go-previous-symbolic", halign: Gtk.Align.START, margin_top: 20, margin_start: 20 }); 153 | this.bar.append(this.back); 154 | this.bar.append(this.pin); 155 | this.back.connect("clicked", () => { 156 | if (!this.navigation_view.can_navigate_back) return; 157 | this.navigation_view.navigate(Adw.NavigationDirection.BACK); 158 | setTimeout(() => { this.navigation_view.remove(this); }, 1000); 159 | }); 160 | 161 | 162 | this.back_wrapper.append(new StatCard("Strength", 10)); 163 | this.back_wrapper.append(new StatCard("Dexterity", 17)); 164 | this.back_wrapper.append(new StatCard("Constitution", 15)); 165 | this.back_wrapper.append(new StatCard("Intelligence", 12)); 166 | this.back_wrapper.append(new StatCard("Wisdom", 12)); 167 | this.back_wrapper.append(new StatCard("Charisma", 20)); 168 | 169 | this.back_wrapper.append(new Value("Armor Class", 14, 0, 50, 1)); 170 | } 171 | }); 172 | -------------------------------------------------------------------------------- /src/data_builder.js: -------------------------------------------------------------------------------- 1 | // Convert the .json data files into one .js file which to be easily loaded 2 | // and convert arrays of objects with an index member into objects where 3 | // the key is that index member so the whole array does not have to be looped 4 | 5 | 6 | import Gio from 'gi://Gio'; 7 | import GLib from 'gi://GLib'; 8 | import Gtk from 'gi://Gtk'; 9 | 10 | export const process = (window) => { 11 | let paths = [ 12 | "Ability-Scores", 13 | "Alignments", 14 | "Backgrounds", 15 | "Classes", 16 | "Conditions", 17 | "Damage-Types", 18 | "Equipment", 19 | "Equipment-Categories", 20 | "Feats", 21 | "Features", 22 | "Languages", 23 | "Levels", 24 | "Magic-Items", 25 | "Magic-Schools", 26 | "Monsters", 27 | "Proficiencies", 28 | "Races", 29 | "Rules", 30 | "Rule-Sections", 31 | "Skills", 32 | "Spells", 33 | "Subclasses", 34 | "Subraces", 35 | "Traits", 36 | "Weapon-Properties" 37 | ] 38 | 39 | const decoder = new TextDecoder('utf-8'); 40 | 41 | let data = {}; 42 | 43 | for (let i in paths) { 44 | let filename = "resource:///de/hummdudel/Libellus/api/5e-SRD-"+paths[i]+".json" 45 | let file = Gio.File.new_for_uri(filename) 46 | const [_, contents] = file.load_contents(null) 47 | const contentsString = decoder.decode(contents) 48 | let old_object = JSON.parse(contentsString) 49 | let new_object = {}; 50 | for (let j in old_object) { 51 | new_object [old_object[j].index] = old_object[j]; 52 | } 53 | 54 | data[paths[i].toLowerCase(paths[i].replace("-", "_"))] = new_object; 55 | } 56 | 57 | 58 | let dialog = new Gtk.FileDialog(); 59 | dialog.save (window, null, (object, res) => { 60 | let file = dialog.save_finish(res); 61 | const outputStream = file.replace(null, false, Gio.FileCreateFlags.NONE, null); 62 | 63 | const bytes = new GLib.Bytes(JSON.stringify(data)); 64 | const bytesWritten = outputStream.write_bytes(bytes, null); 65 | 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /src/dbus.js: -------------------------------------------------------------------------------- 1 | import Gio from 'gi://Gio'; 2 | import {new_tab_from_data} from "./window.js"; 3 | 4 | 5 | export const DBUS = class { 6 | 7 | constructor() { 8 | 9 | const interfaceXml = ` 10 | 11 | 12 | 13 | 14 | 15 | 16 | `; 17 | 18 | let serviceInstance; 19 | let exportedObject; 20 | this.onBusAcquired = (connection, name) => { 21 | console.log(`${name}: connection acquired`); 22 | // Create the class instance, then the D-Bus object 23 | serviceInstance = new Service(); 24 | exportedObject = Gio.DBusExportedObject.wrapJSObject(interfaceXml, serviceInstance); 25 | 26 | // Assign the exported object to the property the class expects, then export 27 | serviceInstance._impl = exportedObject; 28 | exportedObject.export(connection, '/de/hummdudel/Libellus/View'); 29 | } 30 | this.onNameAcquired = (connection, name) => { 31 | console.log(`${name}: name acquired`); 32 | } 33 | this.onNameLost = (connection, name) => { 34 | console.log(`${name}: name lost`); 35 | } 36 | const ownerId = Gio.bus_own_name( 37 | Gio.BusType.SESSION, 38 | 'de.hummdudel.Libellus', 39 | Gio.BusNameOwnerFlags.NONE, 40 | this.onBusAcquired, 41 | this.onNameLost, 42 | this.onNameAcquired); 43 | } 44 | }; 45 | 46 | class Service { 47 | navigate(arg) { 48 | new_tab_from_data({url:arg}); 49 | console.log('navigation to '+arg+' invoked via dbus'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/de.hummdudel.Libellus.data.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | window.ui 5 | filter_dialog.ui 6 | filter_row.ui 7 | filter_page.ui 8 | source_dialog.ui 9 | source_row.ui 10 | gtk/help-overlay.ui 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/de.hummdudel.Libellus.in: -------------------------------------------------------------------------------- 1 | #!@GJS@ -m 2 | 3 | import { exit, programArgs, programInvocationName } from "system"; 4 | 5 | imports.package.init({ 6 | name: "@PACKAGE_NAME@", 7 | version: "@PACKAGE_VERSION@", 8 | prefix: "@prefix@", 9 | libdir: "@libdir@", 10 | datadir: "@datadir@", 11 | }); 12 | 13 | const { main } = await import("resource://@resource_path@/js/main.js"); 14 | const exit_code = await main([programInvocationName, ...programArgs]); 15 | exit(exit_code); 16 | -------------------------------------------------------------------------------- /src/de.hummdudel.Libellus.src.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | main.js 5 | window.js 6 | filter.js 7 | source.js 8 | api.js 9 | dnd.js 10 | modules.js 11 | results.js 12 | character_sheet.js 13 | dbus.js 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/de.hummdudel.Libellus.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | window.ui 5 | filter_dialog.ui 6 | filter_row.ui 7 | filter_page.ui 8 | source_dialog.ui 9 | source_row.ui 10 | gtk/help-overlay.ui 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/filter.js: -------------------------------------------------------------------------------- 1 | import GObject from 'gi://GObject'; 2 | import Gtk from 'gi://Gtk'; 3 | import Adw from 'gi://Adw'; 4 | import { make_filter, adapter } from './window.js'; 5 | 6 | export const FilterDialog = GObject.registerClass({ 7 | GTypeName: 'FilterDialog', 8 | Template: 'resource:///de/hummdudel/Libellus/filter_dialog.ui', 9 | Children: ["view", "filters"], 10 | Signals: { 11 | "applied": {}, 12 | }, 13 | }, class extends Adw.Dialog { 14 | constructor(filters) { 15 | super({}); 16 | 17 | for (let i in filters) { 18 | const row = new FilterRow(filters[i].title); 19 | row.connect("activated", () => { 20 | if (filters[i].choices.length == 0) { 21 | this.filter = filters[i]; 22 | this.emit("applied"); 23 | this.close(); 24 | } else { 25 | this.push_filter(filters[i]); 26 | } 27 | }); 28 | this.filters.append(row); 29 | } 30 | 31 | this.filter = null; 32 | } 33 | 34 | set_filter(filter) { 35 | while (this.view.get_previous_page(this.view.get_visible_page())) { 36 | this.view.pop(); 37 | } 38 | this.push_filter(filter); 39 | } 40 | 41 | filter_none() { 42 | this.filter = null; 43 | this.emit("applied"); 44 | this.close(); 45 | } 46 | 47 | push_filter(filter) { 48 | let page = new FilterPage(make_filter(filter)); 49 | page.connect("applied", () => { 50 | this.filter = page.filter; 51 | this.emit("applied"); 52 | this.close(); 53 | }); 54 | this.view.push(page); 55 | } 56 | }); 57 | 58 | 59 | 60 | export const FilterRow = GObject.registerClass({ 61 | GTypeName: 'FilterRow', 62 | Template: 'resource:///de/hummdudel/Libellus/filter_row.ui', 63 | }, class extends Adw.ActionRow { 64 | constructor(title) { 65 | super( { title: title } ); 66 | } 67 | }); 68 | 69 | 70 | 71 | 72 | 73 | export const FilterPage = GObject.registerClass({ 74 | GTypeName: 'FilterPage', 75 | Template: 'resource:///de/hummdudel/Libellus/filter_page.ui', 76 | Children: ["list_box"], 77 | Signals: { 78 | "applied": {}, 79 | }, 80 | }, class extends Adw.NavigationPage { 81 | constructor(filter) { 82 | super( { title: filter.title } ); 83 | 84 | this.filter = filter; 85 | 86 | if (filter.choices.length == 0) { 87 | this.list_box.visible = false; 88 | } 89 | 90 | for (let i in filter.choices) { 91 | let row; 92 | if (filter.choices[i].content) { 93 | row = new Adw.ComboRow(); 94 | row.model = Gtk.StringList.new(filter.choices[i].content); 95 | row.selected = filter.choices[i].content.indexOf(filter.choices[i].selected); 96 | row.connect("notify::selected", () => { 97 | filter.choices[i].selected = filter.choices[i].content[row.selected]; 98 | }); 99 | if (filter.choices[i].enable_search) { 100 | row.enable_search = true; 101 | } 102 | } else { 103 | row = new Adw.SpinRow(); 104 | let adjustment = Gtk.Adjustment.new(filter.choices[i].value, filter.choices[i].min, filter.choices[i].max, 1, 0, 0); 105 | if (filter.choices[i].enabled) { 106 | row.adjustment = adjustment; 107 | } else { 108 | row.adjustment = null; 109 | } 110 | let switch_button = new Gtk.Switch( { valign: Gtk.Align.CENTER } ); 111 | switch_button.active = filter.choices[i].enabled; 112 | switch_button.connect("notify::active", () => { 113 | if (switch_button.active) { 114 | filter.choices[i].enabled = true; 115 | row.adjustment = adjustment; 116 | } else { 117 | filter.choices[i].enabled = false; 118 | row.adjustment = null; 119 | } 120 | }); 121 | row.connect("notify::value", () => { 122 | filter.choices[i].value = row.value; 123 | }); 124 | row.add_suffix(switch_button); 125 | } 126 | row.title = filter.choices[i].title; 127 | this.list_box.append(row); 128 | } 129 | } 130 | 131 | apply() { 132 | this.emit("applied"); 133 | } 134 | }); 135 | -------------------------------------------------------------------------------- /src/filter_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/filter_page.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/filter_row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/gtk/help-overlay.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | True 5 | 6 | 7 | shortcuts 8 | 10 9 | 10 | 11 | Navigation 12 | 13 | 14 | Next Tab 15 | <Ctrl>Tab 16 | 17 | 18 | 19 | 20 | Previous Tab 21 | <Ctrl>+<Shift>Tab 22 | 23 | 24 | 25 | 26 | Close Tab 27 | <Ctrl>w 28 | 29 | 30 | 31 | 32 | New Tab 33 | <Ctrl>t 34 | 35 | 36 | 37 | 38 | Go to Previous Page 39 | <Alt>Left 40 | 41 | 42 | 43 | 44 | Bookmark Tab 45 | <Ctrl>b 46 | 47 | 48 | 49 | 50 | 51 | 52 | General 53 | 54 | 55 | Show Shortcuts 56 | win.show-help-overlay 57 | 58 | 59 | 60 | 61 | Quit 62 | app.quit 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /* main.js 2 | * 3 | * Copyright 2023 Luna 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | import GObject from 'gi://GObject'; 22 | import Gio from 'gi://Gio'; 23 | import GLib from 'gi://GLib'; 24 | import Gtk from 'gi://Gtk?version=4.0'; 25 | import Adw from 'gi://Adw?version=1'; 26 | 27 | import { LibellusWindow } from './window.js'; 28 | // import { process } from './data_builder.js'; 29 | 30 | pkg.initGettext(); 31 | pkg.initFormat(); 32 | 33 | export const LibellusApplication = GObject.registerClass( 34 | class LibellusApplication extends Adw.Application { 35 | constructor() { 36 | super({application_id: 'de.hummdudel.Libellus', flags: Gio.ApplicationFlags.DEFAULT_FLAGS | Gio.ApplicationFlags.HANDLES_OPEN }); 37 | 38 | const quit_action = new Gio.SimpleAction({name: 'quit'}); 39 | quit_action.connect('activate', action => { 40 | this.quit(); 41 | }); 42 | this.add_action(quit_action); 43 | this.set_accels_for_action('app.quit', ['q']); 44 | 45 | const show_about_action = new Gio.SimpleAction({name: 'about'}); 46 | show_about_action.connect('activate', action => { 47 | let aboutParams = { 48 | transient_for: this.active_window, 49 | application_name: 'Libellus', 50 | application_icon: 'de.hummdudel.Libellus', 51 | developer_name: 'Luna', 52 | version: '1.1.1', 53 | developers: [ 54 | 'Luna' 55 | ], 56 | copyright: '© 2023 Luna', 57 | license_type: Gtk.License.GPL_3_0, 58 | website: "https://libellus.hummdudel.de" 59 | }; 60 | const aboutWindow = new Adw.AboutWindow(aboutParams); 61 | aboutWindow.add_credit_section("Data", ["D&D 5e API https://www.dnd5eapi.co", "Systems Reference Document https://media.wizards.com/2016/downloads/DND/SRD-OGL_V5.1.pdf"]); 62 | aboutWindow.present(); 63 | }); 64 | this.add_action(show_about_action); 65 | 66 | this.setup_filestructure(); 67 | } 68 | 69 | setup_filestructure() { 70 | const path = GLib.get_user_data_dir(); 71 | const file = Gio.File.new_for_path(GLib.build_filenamev([path, "Sources"])); 72 | if (!file.query_exists(null)) { 73 | file.make_directory(null); 74 | log ("created empty sources file"); 75 | } 76 | } 77 | 78 | vfunc_activate() { 79 | let {active_window} = this; 80 | if (!active_window) { 81 | active_window = new LibellusWindow(this, true); 82 | } 83 | active_window.present(); 84 | 85 | // process(active_window); 86 | } 87 | } 88 | ); 89 | 90 | export function main(argv) { 91 | const application = new LibellusApplication(); 92 | return application.runAsync(argv); 93 | } 94 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | pkgdatadir = join_paths(get_option('datadir'), meson.project_name()) 2 | gnome = import('gnome') 3 | 4 | src_res = gnome.compile_resources('de.hummdudel.Libellus.src', 5 | 'de.hummdudel.Libellus.src.gresource.xml', 6 | gresource_bundle: true, 7 | install: true, 8 | install_dir: pkgdatadir, 9 | ) 10 | 11 | data_res = gnome.compile_resources('de.hummdudel.Libellus.data', 12 | 'de.hummdudel.Libellus.data.gresource.xml', 13 | gresource_bundle: true, 14 | install: true, 15 | install_dir: pkgdatadir, 16 | ) 17 | 18 | bin_conf = configuration_data() 19 | bin_conf.set('GJS', find_program('gjs').full_path()) 20 | bin_conf.set('PACKAGE_VERSION', meson.project_version()) 21 | bin_conf.set('PACKAGE_NAME', meson.project_name()) 22 | bin_conf.set('prefix', get_option('prefix')) 23 | bin_conf.set('libdir', join_paths(get_option('prefix'), get_option('libdir'))) 24 | bin_conf.set('datadir', join_paths(get_option('prefix'), get_option('datadir'))) 25 | bin_conf.set('resource_path', '/de/hummdudel/Libellus') 26 | 27 | configure_file( 28 | input: 'de.hummdudel.Libellus.in', 29 | output: 'de.hummdudel.Libellus', 30 | configuration: bin_conf, 31 | install: true, 32 | install_dir: get_option('bindir') 33 | ) 34 | -------------------------------------------------------------------------------- /src/modules.js: -------------------------------------------------------------------------------- 1 | /* modules.js 2 | * 3 | * Copyright 2023 Luna 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | import GObject from 'gi://GObject'; 22 | import Gtk from 'gi://Gtk'; 23 | import Gdk from 'gi://Gdk'; 24 | import GdkPixbuf from 'gi://GdkPixbuf'; 25 | import Gio from 'gi://Gio'; 26 | import GLib from 'gi://GLib'; 27 | import Adw from 'gi://Adw'; 28 | import Pango from 'gi://Pango'; 29 | 30 | import { adapter, navigate } from "./window.js"; 31 | 32 | export const Card = GObject.registerClass({ 33 | GTypeName: 'Card', 34 | }, class extends Gtk.Box { 35 | constructor(title, content) { 36 | super({ css_classes: ["card"], 37 | orientation: Gtk.Orientation.VERTICAL, 38 | spacing: 10, 39 | vexpand: false, 40 | valign: Gtk.Align.FILL, halign: Gtk.Align.FILL, 41 | width_request: 120 42 | }); 43 | 44 | 45 | this.label = new Gtk.Label( { 46 | label: title, 47 | hexpand: true, 48 | margin_top: 20, margin_start: 15, margin_end: 15 49 | } ); 50 | this.append(this.label); 51 | 52 | this.content = new Gtk.Label( { 53 | label: content, 54 | css_classes: ["title-4"], 55 | margin_bottom: 20, margin_start: 15, margin_end: 15, 56 | selectable: true 57 | } ); 58 | this.append(this.content); 59 | } 60 | }); 61 | 62 | export const LinkCard = GObject.registerClass({ 63 | GTypeName: 'LinkCard', 64 | }, class extends Gtk.Box { 65 | constructor(title, content, data, navigation_view) { 66 | super( { 67 | orientation: Gtk.Orientation.VERTICAL, 68 | css_classes: ["card"], 69 | spacing: 10, 70 | vexpand: false, 71 | valign: Gtk.Align.FILL, 72 | halign: Gtk.Align.FILL, 73 | width_request: 120, 74 | } ); 75 | 76 | this.navigation_view = navigation_view; 77 | this.data = data; 78 | 79 | this.label = new Gtk.Label( { 80 | hexpand: true, 81 | label: title, 82 | margin_top: 20, 83 | margin_start: 20, 84 | margin_end: 15, 85 | } ); 86 | this.append(this.label); 87 | 88 | this.content = new Gtk.Button( { 89 | label: content, 90 | margin_bottom: 20, 91 | margin_start: 15, 92 | margin_end: 15, 93 | css_classes: ["title-4", "accent"], 94 | } ); 95 | this.content.connect("clicked", () => { 96 | navigate(this.data, this.navigation_view); 97 | } ); 98 | this.append(this.content); 99 | } 100 | }); 101 | 102 | 103 | export const Link = GObject.registerClass({ 104 | GTypeName: 'Link', 105 | }, class extends Gtk.Button { 106 | constructor(data, navigation_view) { 107 | super({ 108 | label: data.name, 109 | halign: Gtk.Align.CENTER, 110 | margin_bottom: 10, 111 | margin_start: 5, 112 | margin_end: 5, 113 | css_classes: ["heading", "accent"] }); 114 | 115 | this.get_child().ellipsize = Pango.EllipsizeMode.END; 116 | this.data = data; 117 | this.navigation_view = navigation_view; 118 | 119 | this.connect("clicked", () => { 120 | navigate(this.data, this.navigation_view); 121 | } ); 122 | } 123 | }); 124 | 125 | 126 | export const ModuleTitle = GObject.registerClass({ 127 | GTypeName: 'ModuleTitle', 128 | }, class extends Gtk.Label { 129 | constructor(label, title) { 130 | super({ellipsize: Pango.EllipsizeMode.END}); 131 | 132 | this.label = label; 133 | this.add_css_class("title-" + title); 134 | } 135 | }); 136 | 137 | export const ModuleText = GObject.registerClass({ 138 | GTypeName: 'ModuleText', 139 | }, class extends Gtk.Box { 140 | constructor(label) { 141 | super({}); 142 | this.add_css_class("card"); 143 | this.label = new Gtk.Label({ 144 | label: label, 145 | wrap: true, 146 | margin_top: 10, margin_start: 10, margin_end: 10, margin_bottom: 10, 147 | hexpand: true, 148 | selectable: true, 149 | }); 150 | this.append(this.label); 151 | } 152 | }); 153 | 154 | export const ModuleMultiText = GObject.registerClass({ 155 | GTypeName: 'ModuleMultiText', 156 | }, class extends Gtk.ListBox { 157 | constructor(label) { 158 | super({}); 159 | this.add_css_class("boxed-list"); 160 | let table = null; 161 | for (let i in label) { 162 | if (label[i].split) label[i] = label[i].split("###"); 163 | } 164 | label = label.flat(); 165 | for (let i = 0; i < label.length; i++) { 166 | let listboxrow = null; 167 | if (label[i].includes("***")) { 168 | if (table != null) { 169 | this.append(new ModuleNTable(table)); 170 | table = null; 171 | } 172 | let box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL }); 173 | let label1 = new Gtk.Label({ 174 | label: label[i].slice(3, label[i].lastIndexOf("***") - 1), 175 | wrap: true, 176 | margin_top: 10, margin_start: 10, margin_end: 10, 177 | hexpand: true, 178 | css_classes: ["heading"] } ); 179 | let label2 = new Gtk.Label({ 180 | label: label[i].slice(label[i].lastIndexOf("***") + 3), 181 | wrap: true, 182 | margin_top: 5, margin_start: 10, margin_end: 10, margin_bottom: 10, 183 | hexpand: true, 184 | selectable: true, 185 | }); 186 | box.append(label1); 187 | box.append(label2); 188 | listboxrow = new Gtk.ListBoxRow({ 189 | activatable: false, selectable: false, 190 | halign: Gtk.Align.FILL, 191 | child: box }); 192 | } else if (label[i].includes("|")) { 193 | if (table == null) table = []; 194 | let s = label[i].split("|"); 195 | s = s.filter( (i) => i != "" ); 196 | if (!s[0].includes("---")) table.push(s); 197 | } else { 198 | if (table != null) { 199 | this.append(new Gtk.ListBoxRow( { 200 | child: new ModuleNTable(table), 201 | activatable: false, selectable: false } )); 202 | table = null; 203 | } 204 | listboxrow = new Gtk.ListBoxRow({ 205 | activatable: false, selectable: false, 206 | halign: Gtk.Align.FILL, 207 | child: new Gtk.Label({ 208 | label: label[i], 209 | wrap: true, 210 | margin_top: 15, margin_start: 10, margin_end: 10, margin_bottom: 15, 211 | hexpand: true, 212 | selectable: true, 213 | }) 214 | }); 215 | } 216 | if (listboxrow != null) this.append(listboxrow); 217 | } 218 | } 219 | }); 220 | 221 | 222 | // Format for object: 223 | // { 224 | // desc: "content", 225 | // desc2: "content2", 226 | // } 227 | export const Module2Table = GObject.registerClass({ 228 | GTypeName: 'Module2Table', 229 | }, class extends Gtk.Grid { 230 | constructor(object, first_desc, second_desc) { 231 | super({}); 232 | this.add_css_class("card"); 233 | this.attach(new Gtk.Label({ 234 | label: first_desc, 235 | halign: Gtk.Align.CENTER, 236 | margin_top: 10, margin_bottom: 10 }), 237 | 0, 0, 1, 1); 238 | this.attach(new Gtk.Label({ 239 | label: second_desc, 240 | halign: Gtk.Align.CENTER, 241 | margin_top: 10, margin_bottom: 10 }), 242 | 0, 2, 1, 1); 243 | 244 | this.attach(new Gtk.Separator({ 245 | orientation: Gtk.Orientation.VERTICAL, 246 | halign: Gtk.Align.START }), 247 | 1, 0, 1, 3); 248 | 249 | let counter = 2; 250 | for (let i in object) { 251 | this.attach(new Gtk.Label({ 252 | label: i, 253 | halign: Gtk.Align.CENTER, 254 | margin_top: 10, margin_bottom: 10 }), 255 | counter, 0, 1, 1); 256 | this.attach(new Gtk.Label({ 257 | label: object[i], 258 | halign: Gtk.Align.CENTER, 259 | margin_top: 10, margin_bottom: 10 }), 260 | counter, 2, 1, 1); 261 | counter++; 262 | } 263 | this.attach(new Gtk.Separator({ 264 | orientation: Gtk.Orientation.HORIZONTAL, 265 | hexpand: true }), 266 | 0, 1, counter, 1); 267 | 268 | } 269 | }); 270 | export const ModuleNTable = GObject.registerClass({ 271 | GTypeName: 'ModuleNTable', 272 | }, class extends Gtk.Grid { 273 | constructor(n) { 274 | super( { halign: Gtk.Align.FILL, hexpand: true} ); 275 | for (let i in n) { 276 | for (let j in n[i]) { 277 | let l = new Gtk.Label( { 278 | wrap: true, 279 | hexpand: true, 280 | label: n[i][j], 281 | margin_top: 10, margin_bottom: 10 } ); 282 | if (i == 0) l.css_classes = ["heading"]; 283 | this.attach(l, j*2, i*2, 1, 1); 284 | 285 | if (j < n[i].length-1) 286 | this.attach(new Gtk.Separator(), j*2+1, i*2, 1, 1); 287 | if (i < n.length-1) 288 | this.attach(new Gtk.Separator(), j*2, i*2+1, 1, 1); 289 | } 290 | } 291 | } 292 | }); 293 | 294 | 295 | export const ModuleLinkList = GObject.registerClass({ 296 | GTypeName: 'ModuleLinkList', 297 | }, class extends Gtk.ListBox { 298 | constructor(label, navigation_view) { 299 | super({}); 300 | this.add_css_class("boxed-list"); 301 | for (let i = 0; i < label.length; i++) { 302 | let listboxrow = null; 303 | let data = label[i].item; 304 | listboxrow = new Adw.ActionRow({ 305 | activatable: true, selectable: false, 306 | halign: Gtk.Align.FILL, 307 | title: data.name }); 308 | 309 | listboxrow.connect("activated", () => { 310 | navigate(data, navigation_view); 311 | } ); 312 | 313 | listboxrow.add_suffix(new Gtk.Image( { 314 | icon_name: "go-next-symbolic" } )); 315 | 316 | this.append(listboxrow); 317 | } 318 | } 319 | }); 320 | 321 | export const ModuleStatListRow = GObject.registerClass({ 322 | GTypeName: 'ModuleStatListRow', 323 | }, class extends Gtk.ListBoxRow { 324 | constructor(label, stats) { 325 | super( { activatable: false, selectable: false } ); 326 | this.child = new Gtk.Box( { } ); 327 | this.child.append(new Gtk.Label( { css_classes: ["heading"], width_request: 100, label: label, hexpand: false, halign: Gtk.Align.START, margin_top: 15, margin_bottom: 15, margin_start: 15, margin_end: 15 } )); 328 | this.child.append(new Gtk.Separator( { orientation: Gtk.Orientation.VERTICAL } )); 329 | let wrapbox = new Adw.WrapBox( { 330 | hexpand: true, line_spacing: 5, line_homogeneous: true, child_spacing: 5, 331 | margin_start: 5, margin_end: 5, margin_top: 5, margin_bottom: 5 332 | } ); 333 | for (let i = 0; i < stats.length; i++) { 334 | wrapbox.append(new Gtk.Label( { wrap: true, label:stats[i] } )); 335 | } 336 | this.child.append(wrapbox); 337 | } 338 | }); 339 | 340 | export const ModuleStatRow = GObject.registerClass({ 341 | GTypeName: 'ModuleStatRow', 342 | }, class extends Gtk.ListBoxRow { 343 | constructor(label) { 344 | super( { activatable: false, selectable: false } ); 345 | this.child = new Gtk.Label( { label: label, halign: Gtk.Align.FILL, margin_top: 15, margin_bottom: 15, margin_start: 15, margin_end: 15, wrap: true } ); 346 | } 347 | }); 348 | 349 | export const ModuleShortLinkListRow = GObject.registerClass({ 350 | GTypeName: 'ModuleShortLinkListRow', 351 | }, class extends Adw.ActionRow { 352 | constructor(label, stats, navigation_view) { 353 | super( { title: label, activatable: false, selectable: false } ); 354 | for (let i = 0; i < stats.length; i++) { 355 | let l = new Link(stats[i], navigation_view); 356 | l.margin_top = 10; 357 | this.add_suffix(l); 358 | } 359 | } 360 | }); 361 | 362 | export const ModuleLinkListRow = GObject.registerClass({ 363 | GTypeName: 'ModuleLinkListRow', 364 | }, class extends Gtk.ListBoxRow { 365 | constructor(label, stats, navigation_view) { 366 | super( { activatable: false, selectable: false } ); 367 | let vbox = new Gtk.Box( { orientation: Gtk.Orientation.VERTICAL } ); 368 | this.set_child(vbox); 369 | vbox.append(new Gtk.Label( { 370 | label: label, 371 | css_classes: ["heading"], 372 | ellipsize: Pango.EllipsizeMode.END, 373 | margin_top: 15, margin_bottom: 10 } )) 374 | stats = stats.map((i) => new Link(i, navigation_view)); 375 | vbox.append(new Div(stats)); 376 | } 377 | }); 378 | 379 | 380 | 381 | export const Image = GObject.registerClass({ 382 | GTypeName: 'Image', 383 | }, class extends Adw.Bin { 384 | constructor(image) { 385 | super({}); 386 | let response = adapter.get_any_sync(image); 387 | let loader = new GdkPixbuf.PixbufLoader() 388 | loader.write_bytes(GLib.Bytes.new(response)) 389 | loader.close() 390 | let img = new Gtk.Picture( { css_classes: ["card"], halign: Gtk.Align.CENTER, valign: Gtk.Align.FILL, vexpand: true, height_request: 300 } ); 391 | img.set_pixbuf(loader.get_pixbuf()); 392 | this.set_child(img); 393 | } 394 | }); 395 | 396 | export const ImageAsync = GObject.registerClass({ 397 | GTypeName: 'ImageAsync', 398 | }, class extends Adw.Bin { 399 | constructor(image, height = 300) { 400 | super( { css_classes: ["card"], halign: Gtk.Align.FILL, valign: Gtk.Align.FILL, vexpand: true, hexpand: true, height_request: height } ); 401 | adapter.get_any_async(image, (response) => { 402 | this.halign = Gtk.Align.CENTER; 403 | this.valign = Gtk.Align.START; 404 | this.vexpand = false; 405 | let loader = new GdkPixbuf.PixbufLoader(); 406 | loader.write_bytes(GLib.Bytes.new(response)); 407 | loader.close(); 408 | let img = new Gtk.Picture( { css_classes: ["card"] } ); 409 | img.set_pixbuf(loader.get_pixbuf()); 410 | 411 | let overlay = new Gtk.Overlay({child: img}); 412 | 413 | let revealer = new Gtk.Revealer( { child: overlay, transition_type: Gtk.RevealerTransitionType.CROSSFADE } ); 414 | this.set_child(revealer); 415 | revealer.set_reveal_child(true); 416 | 417 | let button = new Gtk.Button({ 418 | css_classes:["osd", "circular"], 419 | icon_name: "view-fullscreen-symbolic", 420 | halign: Gtk.Align.END, 421 | valign: Gtk.Align.END, 422 | margin_bottom: 5, 423 | margin_end: 5 }) 424 | button.connect("clicked", () => { 425 | let dataDir = GLib.get_user_config_dir(); 426 | let name = image.split(".")[0]; 427 | name = name.split("/").at(-1); 428 | let destination = GLib.build_filenamev([dataDir, name+'.jpeg']); 429 | loader.get_pixbuf().savev(destination, "jpeg", null, null); 430 | let file = Gio.File.new_for_path(destination); 431 | let launcher = new Gtk.FileLauncher({file:file}); 432 | launcher.launch(null, null, null); 433 | }); 434 | overlay.add_overlay(button); 435 | }); 436 | let spinner = new Adw.Spinner( { halign: Gtk.Align.FILL, valign: Gtk.Align.FILL } ); 437 | // spinner.start(); 438 | this.set_child(new Adw.Bin({child: spinner, halign: Gtk.Align.FILL, hexpand: true})); 439 | } 440 | }); 441 | 442 | export const ModuleLevelRow = GObject.registerClass({ 443 | GTypeName: 'ModuleLevelRow', 444 | }, class extends Gtk.ListBoxRow { 445 | constructor(data, navigation_view) { 446 | super( {activatable: false, selectable: false } ); 447 | let box = new Gtk.Box( { 448 | orientation: Gtk.Orientation.HORIZONTAL, 449 | valign: Gtk.Align.CENTER, 450 | vexpand: true 451 | } ); 452 | this.set_child(box); 453 | box.append( new Gtk.Label( { 454 | label: data.level.toString(), 455 | margin_start: 15, margin_top: 15, margin_bottom: 15, margin_end: 15 456 | } )); 457 | let links = []; 458 | for(let i in data.features) { 459 | let l = new Link(data.features[i], navigation_view) 460 | l.margin_top = 10; 461 | links.push(l); 462 | } 463 | box.append(new Div(links)); 464 | } 465 | }); 466 | 467 | export const Div = GObject.registerClass({ 468 | GTypeName: 'Div', 469 | }, class extends Adw.WrapBox { 470 | constructor(cards) { 471 | super({ 472 | orientation: Gtk.Orientation.HORIZONTAL, 473 | halign: Gtk.Align.CENTER, 474 | hexpand: true, 475 | justify_last_line: Adw.JustifyMode.NONE, 476 | align: 0.5, 477 | }); 478 | for (let i in cards) { 479 | this.append(cards[i]); 480 | } 481 | } 482 | }); 483 | 484 | export const BigDiv = GObject.registerClass({ 485 | GTypeName: 'BigDiv', 486 | }, class extends Adw.WrapBox { 487 | constructor(cards) { 488 | super({ 489 | orientation: Gtk.Orientation.HORIZONTAL, 490 | halign: Gtk.Align.CENTER, 491 | line_spacing: 10, 492 | child_spacing: 10, 493 | hexpand: true, 494 | justify_last_line: Adw.JustifyMode.NONE, 495 | align: 0.5, 496 | }); 497 | for (let i in cards) { 498 | this.append(cards[i]); 499 | } 500 | } 501 | }); 502 | -------------------------------------------------------------------------------- /src/results.js: -------------------------------------------------------------------------------- 1 | /* results.js 2 | * 3 | * Copyright 2023 Luna 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | import GObject from 'gi://GObject'; 22 | import Gtk from 'gi://Gtk'; 23 | import GdkPixbuf from 'gi://GdkPixbuf'; 24 | import Gio from 'gi://Gio'; 25 | import GLib from 'gi://GLib'; 26 | import Adw from 'gi://Adw'; 27 | import Pango from 'gi://Pango'; 28 | 29 | import { make_manifest, unmake_manifest, score_to_modifier, sources, toggle_bookmarked, is_bookmarked, save_state, adapter } from "./window.js"; 30 | import { ModuleTitle } from "./modules.js"; 31 | 32 | export const SearchResult = GObject.registerClass({ 33 | GTypeName: 'SearchResult', 34 | }, class extends Gtk.ListBoxRow { 35 | constructor(data, type) { 36 | super({}); 37 | this.data = data; 38 | this.type = type; 39 | 40 | this.child = new Gtk.Box(); 41 | let front = new Gtk.Box( { margin_start: 15, margin_top: 10, margin_bottom: 5, halign: Gtk.Align.START, hexpand: true, orientation: Gtk.Orientation.VERTICAL } ); 42 | this.child.append(front); 43 | front.append(new Gtk.Label( { ellipsize: Pango.EllipsizeMode.END, css_classes: ["title"], halign: Gtk.Align.START, label: this.data.name })); 44 | if (adapter.ident == "dnd5e") { 45 | front.append(new Gtk.Label( { ellipsize: Pango.EllipsizeMode.END, css_classes: ["subtitle"], halign: Gtk.Align.START, label: this.data.url 46 | .split("/")[2] 47 | .split("-") 48 | .map((str) => { return str.charAt(0).toUpperCase() + str.slice(1); } ) 49 | .join(" ") } )); 50 | } else if (adapter.ident == "pf2e") { 51 | front.append(new Gtk.Label( { ellipsize: Pango.EllipsizeMode.END, css_classes: ["subtitle"], halign: Gtk.Align.START, label: this.data.url } )); 52 | } else { 53 | front.append(new Gtk.Label( { ellipsize: Pango.EllipsizeMode.END, css_classes: ["subtitle"], halign: Gtk.Align.START, label: this.data.url } )); 54 | } 55 | 56 | // for searching 57 | this.name = this.data.name; 58 | 59 | this.child.append(new Gtk.Image( { halign: Gtk.Align.END, iconName: "go-next-symbolic", margin_end: 15 } )); 60 | this.set_activatable(true); 61 | 62 | } 63 | }); 64 | 65 | export const ResultPage = GObject.registerClass({ 66 | GTypeName: 'ResultPage', 67 | }, class extends Gtk.ScrolledWindow { 68 | constructor(data, navigation_view) { 69 | super({ 70 | halign: Gtk.Align.FILL, 71 | hexpand: true, 72 | hscrollbar_policy: Gtk.PolicyType.NEVER }); 73 | 74 | this.navigation_view = navigation_view; 75 | 76 | this.data = data; 77 | 78 | this.bookmark_accel = () => { 79 | if (toggle_bookmarked ({ name: this.data.name, url: this.data.url })) { 80 | this.pin.set_css_classes(["success"]); 81 | } else { 82 | this.pin.set_css_classes([]); 83 | } 84 | } 85 | 86 | this.back_wrapper = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL }); 87 | this.set_child(this.back_wrapper); 88 | 89 | this.pin = new Gtk.Button({ 90 | icon_name: "star-large-symbolic", 91 | halign: Gtk.Align.END, hexpand: true, 92 | margin_top: 20, margin_end: 20 }); 93 | 94 | if (is_bookmarked ({ name: this.data.name, url: this.data.url })) { 95 | this.pin.add_css_class("success"); 96 | } 97 | this.pin.connect("clicked", () => { 98 | if (toggle_bookmarked ({ name: this.data.name, url: this.data.url })) { 99 | this.pin.set_css_classes(["success"]); 100 | } else { 101 | this.pin.set_css_classes([]); 102 | } 103 | } ); 104 | 105 | this.bar = new Gtk.Box( { 106 | orientation: Gtk.Orientation.HORIZONTAL, 107 | hexpand: true, 108 | halign:Gtk.Align.FILL } ); 109 | 110 | this.bar.append(this.pin); 111 | this.back_wrapper.append(this.bar); 112 | 113 | this.clamp = new Adw.Clamp({ 114 | maximum_size: 800, 115 | margin_start: 20, margin_end: 20, margin_bottom: 20 }); 116 | this.back_wrapper.append(this.clamp); 117 | this.wrapper = new Gtk.Box({ 118 | orientation: Gtk.Orientation.VERTICAL, 119 | spacing: 20 }); 120 | this.clamp.add_css_class("undershoot-top"); 121 | this.clamp.add_css_class("undershoot-bottom"); 122 | this.clamp.set_child(this.wrapper); 123 | 124 | if (this.data.full_name) { 125 | this.wrapper.append(new ModuleTitle(this.data.full_name, 1)); 126 | } else { 127 | this.wrapper.append(new ModuleTitle(this.data.name, 1)); 128 | } 129 | 130 | this.update_title = () => { 131 | this.navigation_view.tab_page.set_title(this.data.name); 132 | } 133 | this.update_title(); 134 | } 135 | }); 136 | 137 | 138 | -------------------------------------------------------------------------------- /src/source.js: -------------------------------------------------------------------------------- 1 | import GObject from 'gi://GObject'; 2 | import Gtk from 'gi://Gtk'; 3 | import GLib from 'gi://GLib'; 4 | import Gio from 'gi://Gio'; 5 | import Adw from 'gi://Adw'; 6 | 7 | export const SourceDialog = GObject.registerClass({ 8 | GTypeName: 'SourceDialog', 9 | Template: 'resource:///de/hummdudel/Libellus/source_dialog.ui', 10 | Children: ["source_list"], 11 | Signals: { 12 | "imported_source": { 13 | "param_types": [ GObject.TYPE_STRING ], 14 | }, 15 | "load_source": { 16 | "param_types": [ GObject.TYPE_INT ], 17 | }, 18 | "delete_source": { 19 | "param_types": [ GObject.TYPE_INT ], 20 | }, 21 | }, 22 | }, class extends Adw.Dialog { 23 | constructor(sources) { 24 | super({}); 25 | for (let i in sources) { 26 | let row = new SourceRow(sources[i].name, sources[i].built_in); 27 | row.connect("activated", () => { 28 | this.emit("load_source", i); 29 | this.close(); 30 | }); 31 | row.connect("removed", () => { 32 | const file = Gio.File.new_for_path(GLib.build_filenamev( [ 33 | GLib.get_user_data_dir(), 34 | "Sources", 35 | sources[i].path ] )); 36 | 37 | file.delete(null); 38 | this.emit("delete_source", i); 39 | this.close(); 40 | }); 41 | this.source_list.append(row); 42 | } 43 | } 44 | 45 | import_source () { 46 | const dialog = new Adw.AlertDialog( { 47 | heading: "Warning", 48 | body: "Sources can execute arbitrary code, only use sources from places you trust!" } ); 49 | 50 | dialog.add_response("C", "Cancel"); 51 | dialog.add_response("O", "Ok"); 52 | dialog.set_response_appearance("C", Adw.ResponseAppearance.SUGGESTED); 53 | dialog.set_response_appearance("O", Adw.ResponseAppearance.DESTRUCTIVE); 54 | 55 | dialog.connect("response", (_, c) => { 56 | if (c == "C") { 57 | return; 58 | } else if (c == "O") { 59 | const fileDialog = new Gtk.FileDialog(); 60 | fileDialog.open(this.get_root(), null, (self, result) => { 61 | try { 62 | const file = self.open_finish(result); 63 | if (file) { 64 | const destination = Gio.File.new_for_path(GLib.build_filenamev( [ 65 | GLib.get_user_data_dir(), 66 | "Sources", 67 | file.get_basename() ] )); 68 | 69 | const bytes = file.load_bytes (null)[0]; 70 | const stream = destination.create(Gio.FileCreateFlags.NONE, null); 71 | const bytes_written = stream.write_bytes(bytes, null); 72 | 73 | // This fails with "Gio.IOErrorEnum: Error splicing file: Input/output error", 74 | // no idea how to fix that (and searching the error is also not that helpful) 75 | // file.copy(destination, Gio.FileCopyFlags.NONE, null, null); 76 | 77 | this.emit("imported_source", destination.get_basename()); 78 | this.close(); 79 | } 80 | } catch(e) { 81 | log("oops: " + e); 82 | } 83 | }); 84 | } else { 85 | log("WELL OOOOPS"); 86 | return; 87 | } 88 | }); 89 | dialog.present(this); 90 | 91 | } 92 | }); 93 | 94 | export const SourceRow = GObject.registerClass({ 95 | GTypeName: 'SourceRow', 96 | Template: 'resource:///de/hummdudel/Libellus/source_row.ui', 97 | Children: ["remove_button"], 98 | Signals: { 99 | "removed": {}, 100 | }, 101 | }, class extends Adw.ActionRow { 102 | constructor(title, built_in) { 103 | super( { title: title } ); 104 | this.removable = built_in; 105 | if (built_in) { 106 | this.remove_button.visible = false; 107 | } 108 | } 109 | 110 | remove_row() { 111 | const dialog = new Adw.AlertDialog( { 112 | heading:"Delete “"+this.title+"”?", 113 | body: "This action can not be undone." } ); 114 | 115 | dialog.add_response("C", "Cancel"); 116 | dialog.add_response("D", "Delete"); 117 | dialog.set_response_appearance("C", Adw.ResponseAppearance.SUGGESTED); 118 | dialog.set_response_appearance("D", Adw.ResponseAppearance.DESTRUCTIVE); 119 | 120 | dialog.connect("response", (_, c) => { 121 | if (c == "C") { 122 | return; 123 | } else if (c == "D") { 124 | this.emit("removed"); 125 | } else { 126 | log("WELL OOOOPS"); 127 | return; 128 | } 129 | }); 130 | dialog.present(this); 131 | } 132 | }); 133 | 134 | -------------------------------------------------------------------------------- /src/source_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/source_row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/window.js: -------------------------------------------------------------------------------- 1 | /* window.js 2 | * 3 | * Copyright 2023 Luna 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | import GObject from 'gi://GObject'; 22 | import Gtk from 'gi://Gtk'; 23 | import Gdk from 'gi://Gdk'; 24 | import GdkPixbuf from 'gi://GdkPixbuf'; 25 | import Gio from 'gi://Gio'; 26 | import GLib from 'gi://GLib'; 27 | import Adw from 'gi://Adw'; 28 | 29 | 30 | import { resolve_link, 31 | get_search_results, 32 | get_sync, 33 | get_any_sync, 34 | get_any_async, 35 | filter_options, 36 | ident } from "./dnd.js"; 37 | 38 | export let adapter = { 39 | resolve_link: resolve_link, 40 | get_search_results: get_search_results, 41 | get_sync: get_sync, 42 | get_any_sync: get_any_sync, 43 | get_any_async: get_any_async, 44 | filter_options: filter_options, 45 | ident: ident, 46 | }; 47 | 48 | import { ResultPage, SearchResult } from "./results.js"; 49 | import { FilterDialog } from "./filter.js"; 50 | import { SourceDialog } from "./source.js"; 51 | 52 | export const Tab = GObject.registerClass({ 53 | GTypeName: 'Tab', 54 | }, class extends Adw.NavigationPage { 55 | constructor() { 56 | super({title: "Search"}); 57 | } 58 | }); 59 | import { SheetTab } from "./character_sheet.js"; 60 | 61 | import { DBUS } from './dbus.js'; 62 | 63 | var window; 64 | 65 | export const LibellusWindow = GObject.registerClass({ 66 | GTypeName: 'LibellusWindow', 67 | Template: 'resource:///de/hummdudel/Libellus/window.ui', 68 | Children: [ 69 | "overview", 70 | "toolbar_view", 71 | "header_bar", 72 | "tab_view", 73 | "bookmark_popover", 74 | "bottom_bar", 75 | "menu_button", 76 | 77 | "breakpoint", 78 | 79 | "bookmark_button", 80 | "tab_button", 81 | "new_tab" , 82 | "back_button", 83 | "library_button", 84 | ], 85 | }, class LibellusWindow extends Adw.ApplicationWindow { 86 | constructor(application, main_window = false) { 87 | super({ application }); 88 | this.main_window = main_window; 89 | 90 | const provider = new Gtk.CssProvider(); 91 | provider.connect("parsing-error", (_provider, _section, error) => { log(error); }); 92 | provider.load_from_string(".bookmark-row { padding: 0; } "); 93 | Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); 94 | 95 | const builder = Gtk.Builder.new_from_resource("/de/hummdudel/Libellus/gtk/help-overlay.ui"); 96 | this.set_help_overlay(builder.get_object("help_overlay")); 97 | 98 | const close_tab_shortcut = Gtk.Shortcut.new(Gtk.ShortcutTrigger.parse_string("w"), Gtk.NamedAction.new("win.close-tab")); 99 | const close_tab_action = new Gio.SimpleAction({ name: "close-tab" }); 100 | close_tab_action.connect("activate", () => { 101 | if (this.tab_view.get_n_pages() > 1) { 102 | this.tab_view.close_page(this.tab_view.selected_page); 103 | } 104 | }); 105 | this.add_action(close_tab_action); 106 | 107 | const new_tab_shortcut = Gtk.Shortcut.new(Gtk.ShortcutTrigger.parse_string("t"), Gtk.NamedAction.new("win.new-tab")); 108 | const new_tab_action = new Gio.SimpleAction({ name: "new-tab" }); 109 | new_tab_action.connect("activate", () => { 110 | let tab = new SearchTab(new NavView()); 111 | let tab_page = this.tab_view.append(tab.navigation_view); 112 | tab.navigation_view.tab_page = this.tab_view.get_nth_page(this.tab_view.n_pages-1); 113 | tab.navigation_view.window = this; 114 | this.tab_view.selected_page = tab_page; 115 | }); 116 | this.add_action(new_tab_action); 117 | 118 | const bookmark_shortcut = Gtk.Shortcut.new(Gtk.ShortcutTrigger.parse_string("b"), Gtk.NamedAction.new("win.bookmark")); 119 | const bookmark_action = new Gio.SimpleAction({ name: "bookmark" }); 120 | bookmark_action.connect("activate", () => { 121 | this.tab_view.selected_page.child.get_visible_page().child.bookmark_accel(); 122 | }); 123 | this.add_action(bookmark_action); 124 | 125 | const shortcut_controller = new Gtk.ShortcutController(); 126 | shortcut_controller.add_shortcut(close_tab_shortcut); 127 | shortcut_controller.add_shortcut(new_tab_shortcut); 128 | shortcut_controller.add_shortcut(bookmark_shortcut); 129 | this.add_controller(shortcut_controller); 130 | 131 | if (main_window) { 132 | let dbus = new DBUS(); 133 | } 134 | 135 | this.overview.connect("create-tab", () => { 136 | let tab = new SearchTab(new NavView()); 137 | let tab_page = this.tab_view.append(tab.navigation_view); 138 | tab.navigation_view.tab_page = this.tab_view.get_nth_page(this.tab_view.n_pages-1); 139 | tab.navigation_view.window = this; 140 | this.tab_view.selected_page = tab_page; 141 | return tab_page; 142 | } ); 143 | 144 | if (main_window) { 145 | this.tabs = [ 146 | new SearchTab(new NavView()), 147 | ]; 148 | } else { 149 | this.tabs = []; 150 | } 151 | this.tab_view.connect("create-window", () => { 152 | let new_window = new LibellusWindow(application); 153 | new_window.present(); 154 | return new_window.tab_view; 155 | }); 156 | 157 | for (let i in this.tabs) { 158 | this.tab_view.append(this.tabs[i].navigation_view); 159 | this.tabs[i].navigation_view.tab_page = this.tab_view.get_nth_page(i); 160 | this.tabs[i].navigation_view.tab_view = this.tab_view; 161 | this.tabs[i].navigation_view.window = this; 162 | } 163 | this.active_tab = 0; 164 | 165 | this.new_tab.connect("clicked", () => { 166 | let tab = new SearchTab(new NavView()); 167 | let tab_page = this.tab_view.append(tab.navigation_view); 168 | tab.navigation_view.tab_page = this.tab_view.get_nth_page(this.tab_view.n_pages-1); 169 | tab.navigation_view.window = this; 170 | this.tab_view.selected_page = tab_page; 171 | } ); 172 | 173 | this.tab_button.connect("clicked", () => { this.overview.open = true; } ); 174 | 175 | this.back_button.connect("clicked", () => { this.tab_view.selected_page.child.pop(); }); 176 | 177 | this.menu = new Gio.Menu(); 178 | this.menu.append_item(Gio.MenuItem.new("Keyboard Shortcuts", "win.show-help-overlay")); 179 | this.menu.append_item(Gio.MenuItem.new("About Libellus", "app.about")); 180 | 181 | this.menu_button.menu_model = this.menu; 182 | 183 | this.bookmark_list = new Gtk.ListBox(); 184 | this.bookmark_list.connect("row-activated", (_, row) => { 185 | row.activated(); 186 | }); 187 | 188 | this.bookmark_popover.child = this.bookmark_list; 189 | 190 | this.breakpoint.connect("apply", () => { 191 | this.header_bar.remove(this.bookmark_button); 192 | this.header_bar.remove(this.library_button); 193 | this.header_bar.remove(this.back_button); 194 | this.header_bar.remove(this.tab_button); 195 | this.header_bar.remove(this.new_tab); 196 | this.bottom_bar.pack_start(this.back_button); 197 | this.bottom_bar.pack_start(this.new_tab); 198 | this.bottom_bar.pack_end(this.bookmark_button); 199 | this.bottom_bar.pack_end(this.library_button); 200 | this.bottom_bar.pack_end(this.tab_button); 201 | }); 202 | this.breakpoint.connect("unapply", () => { 203 | this.bottom_bar.remove(this.bookmark_button); 204 | this.bottom_bar.remove(this.library_button); 205 | this.bottom_bar.remove(this.back_button); 206 | this.bottom_bar.remove(this.tab_button); 207 | this.bottom_bar.remove(this.new_tab); 208 | this.header_bar.pack_start(this.back_button); 209 | this.header_bar.pack_start(this.new_tab); 210 | this.header_bar.pack_end(this.bookmark_button); 211 | this.header_bar.pack_end(this.library_button); 212 | this.header_bar.pack_end(this.tab_button); 213 | }); 214 | 215 | 216 | this.source_resource = null; 217 | this.source_index = 0; 218 | 219 | this.import_source = (path) => { 220 | const file = Gio.File.new_for_path(GLib.build_filenamev( [ GLib.get_user_data_dir(), "Sources", path ] )); 221 | let bytes = file.load_bytes (null)[0]; 222 | let resource = Gio.Resource.new_from_data(bytes); 223 | if (this.source_resource) { 224 | adapter = {}; 225 | Gio.resources_unregister(this.source_resource); 226 | } 227 | Gio.resources_register(resource); 228 | this.source_resource = resource; 229 | 230 | bytes = this.source_resource.lookup_data('/de/hummdudel/Libellus/database/manifest.json', 0); 231 | let array = bytes.toArray(); 232 | let string = new TextDecoder().decode(array); 233 | let data = JSON.parse(string); 234 | 235 | sources.push( { 236 | built_in: false, 237 | path: path, 238 | name: data.name, 239 | bookmarks: [], 240 | } ); 241 | save_state(); 242 | this.load_source (sources.length - 1); 243 | }; 244 | 245 | this.delete_source = (index) => { 246 | if (index == 0) { 247 | log("trying to delete built-in source"); 248 | return; 249 | } 250 | if (this.source_index == index) { 251 | this.load_source(0); 252 | } 253 | sources.splice(index, 1); 254 | save_state(); 255 | }; 256 | 257 | this.load_source = async (index) => { 258 | // giant ugly try / catch block to get errors printed to the console 259 | try { 260 | const source = sources[index]; 261 | if (source.built_in) { 262 | this.source_index = index; 263 | 264 | for (let i = 0; i < this.tab_view.get_n_pages(); i++) { 265 | this.tab_view.close_page(this.tab_view.get_nth_page(0)); 266 | } 267 | 268 | adapter = { 269 | resolve_link: resolve_link, 270 | get_search_results: get_search_results, 271 | get_sync: get_sync, 272 | get_any_sync: get_any_sync, 273 | get_any_async: get_any_async, 274 | filter_options: filter_options, 275 | ident: ident, 276 | }; 277 | 278 | let tab = new SearchTab(new NavView()); 279 | let tab_page = this.tab_view.append(tab.navigation_view); 280 | tab.navigation_view.tab_page = this.tab_view.get_nth_page(this.tab_view.n_pages-1); 281 | tab.navigation_view.window = this; 282 | this.tab_view.selected_page = tab_page; 283 | } else { 284 | const file = Gio.File.new_for_path(GLib.build_filenamev( [ GLib.get_user_data_dir(), "Sources", source.path ] )); 285 | 286 | let bytes = file.load_bytes (null)[0]; 287 | let resource = Gio.Resource.new_from_data(bytes); 288 | 289 | if (this.source_resource) { 290 | adapter = {}; 291 | Gio.resources_unregister(this.source_resource); 292 | } 293 | 294 | Gio.resources_register(resource); 295 | this.source_resource = resource; 296 | this.source_index = index; 297 | bytes = this.source_resource.lookup_data('/de/hummdudel/Libellus/database/manifest.json', 0); 298 | let array = bytes.toArray(); 299 | let string = new TextDecoder().decode(array); 300 | let data = JSON.parse(string); 301 | 302 | // let module = await import('resource://de/hummdudel/Libellus/database/js/adapter.js?u='+Number((new Date()))); 303 | let module = await import(data.adapter_name); 304 | 305 | for (let i = 0; i < this.tab_view.get_n_pages(); i++) { 306 | this.tab_view.close_page(this.tab_view.get_nth_page(0)); 307 | } 308 | 309 | adapter = { 310 | resolve_link: module.resolve_link, 311 | get_search_results: module.get_search_results, 312 | get_sync: module.get_sync, 313 | get_any_sync: module.get_any_sync, 314 | get_any_async: module.get_any_async, 315 | filter_options: module.filter_options, 316 | ident: module.ident, 317 | }; 318 | 319 | if (module.init) { 320 | module.init(this.source_resource); 321 | } 322 | 323 | let tab = new SearchTab(new NavView()); 324 | let tab_page = this.tab_view.append(tab.navigation_view); 325 | tab.navigation_view.tab_page = this.tab_view.get_nth_page(this.tab_view.n_pages-1); 326 | tab.navigation_view.window = this; 327 | this.tab_view.selected_page = tab_page; 328 | } 329 | update_boookmark_menu(this); 330 | } catch (e) { 331 | log("oops "+e); 332 | } 333 | }; 334 | 335 | this.library_button.connect("clicked", () => { 336 | const source_dialog = new SourceDialog(sources); 337 | source_dialog.connect("imported_source", (_, path) => { 338 | this.import_source(path); 339 | }); 340 | source_dialog.connect("load_source", (_, index) => { 341 | this.load_source(index); 342 | }); 343 | source_dialog.connect("delete_source", (_, index) => { 344 | this.delete_source(index); 345 | }); 346 | source_dialog.present (this); 347 | }); 348 | 349 | window = this; 350 | try { 351 | load_state(); 352 | } catch { 353 | save_state(); 354 | } 355 | 356 | 357 | update_boookmark_menu (this); 358 | } 359 | }); 360 | 361 | 362 | 363 | 364 | export const BookmarkRow = GObject.registerClass({ 365 | GTypeName: 'BookmarkRow', 366 | }, class BookmarkRow extends Gtk.ListBoxRow { 367 | constructor(data) { 368 | super( { selectable: false, css_classes: ["bookmark-row"]} ); 369 | this.data = data; 370 | 371 | this.child = new Gtk.Box( { spacing: 10 } ); 372 | this.child.append(new Gtk.Label({ hexpand: true, label: data.name, margin_start: 8 } )); 373 | this.delete_button = new Gtk.Button( { halign: Gtk.Align.END, valign: Gtk.Align.CENTER, margin_top: 0, margin_bottom: 0, icon_name: "edit-clear-symbolic", css_classes: ["flat", "circular"] } ); 374 | this.delete_button.connect("clicked", () => { 375 | toggle_bookmarked(this.data); 376 | } ); 377 | this.child.append(this.delete_button); 378 | this.activated = () => { 379 | new_tab_from_data(this.data); 380 | } 381 | } 382 | }); 383 | 384 | 385 | export const new_tab_from_data = (data) => { 386 | let tab_view = window.tab_view; 387 | let tab = new SearchTab(new NavView()); 388 | tab_view.append(tab.navigation_view); 389 | tab.navigation_view.tab_page = tab_view.get_nth_page(tab_view.n_pages-1); 390 | tab.navigation_view.tab_view = tab_view; 391 | tab_view.selected_page = tab_view.get_nth_page(tab_view.n_pages-1); 392 | navigate(data, tab.navigation_view); 393 | return tab; 394 | } 395 | 396 | const SearchTab = GObject.registerClass({ 397 | GTypeName: 'SearchTab', 398 | }, class extends Tab { 399 | constructor(navigation_view) { 400 | super({}); 401 | setTimeout(() => { this.navigation_view.tab_page.set_title("Search"); }, 1); 402 | this.navigation_view = navigation_view; 403 | this.set_hexpand(true) 404 | 405 | this.bookmark_accel = () => { 406 | }; 407 | 408 | this.key_controller = new Gtk.EventControllerKey(); 409 | this.key_controller.connect("key-pressed", (_controller, val, _code, _state, _data) => { 410 | let name = Gdk.keyval_name(val); 411 | if (name.length > 1) return; 412 | this.entry.grab_focus(); 413 | this.entry.text = this.entry.text + name; 414 | this.entry.set_position(this.entry.text.length); 415 | }); 416 | this.add_controller(this.key_controller); 417 | 418 | this.update_title = () => { 419 | this.navigation_view.tab_page.set_title("Search"); 420 | }; 421 | 422 | this.navigation_view.push(this); 423 | 424 | this.scrolled_window = new Gtk.ScrolledWindow( { halign: Gtk.Align.FILL, hexpand: true, hscrollbar_policy: Gtk.PolicyType.NEVER } ); 425 | this.scrolled_window.update_title = () => { 426 | this.navigation_view.tab_page.set_title("Search"); 427 | } 428 | 429 | this.scrolled_window.add_css_class("undershoot-top"); 430 | this.scrolled_window.add_css_class("undershoot-bottom"); 431 | 432 | this.child = this.scrolled_window; 433 | 434 | this.clamp = new Adw.Clamp( { maximum_size: 500, tightening_threshold: 300 } ); 435 | this.scrolled_window.set_child(this.clamp); 436 | 437 | this.list_box = new Gtk.ListBox( { 438 | halign: Gtk.Align.FILL, valign: Gtk.Align.START, 439 | margin_top: 15, margin_bottom: 15, margin_start: 15, margin_end: 15, 440 | css_classes: ["boxed-list"], 441 | selection_mode: Gtk.SelectionMode.NONE 442 | } ); 443 | this.clamp.child = this.list_box; 444 | this.entry = new Adw.EntryRow( { title: "Search..." } ); 445 | this.list_box.append(this.entry); 446 | 447 | this.filter_button = new Gtk.Button( { iconName: "funnel-symbolic" } ); 448 | this.filter_dialog = new FilterDialog(adapter.filter_options); 449 | this.filter_dialog.connect("applied", () => { 450 | this.active_filter = this.filter_dialog.filter; 451 | this.update_search(); 452 | if (this.active_filter) { 453 | this.filter_button.add_css_class("accent"); 454 | } else { 455 | this.filter_button.remove_css_class("accent"); 456 | } 457 | }); 458 | this.filter_button.connect("clicked", () => { 459 | this.filter_dialog.present(this); 460 | }); 461 | this.filter_button.add_css_class("flat"); 462 | this.filter_button.set_valign(Gtk.Align.CENTER); 463 | this.entry.add_suffix(this.filter_button); 464 | 465 | this.set_filter = (filter) => { 466 | this.active_filter = filter; 467 | this.filter_dialog.set_filter(filter); 468 | if (this.active_filter) { 469 | this.filter_button.add_css_class("accent"); 470 | } else { 471 | this.filter_button.remove_css_class("accent"); 472 | } 473 | this.update_search(); 474 | } 475 | 476 | this.active_filter = null; 477 | this.search_term = ""; 478 | this.update_search = () => { 479 | this.search_term = this.entry.get_text(); 480 | for (let i = 0; i < this.results.length; i++) { 481 | if (this.search_term == "" || this.results[i].name.toLowerCase().includes(this.search_term.toLowerCase()) || this.search_term.toLowerCase().includes(this.results[i].name.toLowerCase())) { 482 | if (!this.active_filter || this.active_filter.func(this.results[i].data.url, this.active_filter)) { 483 | this.results[i].visible = true; 484 | continue; 485 | } 486 | } 487 | this.results[i].visible = false; 488 | } 489 | } 490 | this.entry.connect("changed", this.update_search); 491 | 492 | this.results = adapter.get_search_results([]); 493 | 494 | 495 | for (let i = 0; i < this.results.length; i++) { 496 | this.list_box.append(this.results[i]); 497 | } 498 | this.list_box.connect("row_activated", (_, i) => { navigate(i.data, this.navigation_view) } ); 499 | 500 | this.close_result = () => { 501 | this.navigation_view.navigate(Adw.NavigationDirection.BACK); 502 | setTimeout(() => { this.navigation_view.remove(this.open_result_page); this.open_result_page = null; }, 100); 503 | } 504 | 505 | 506 | } 507 | }); 508 | 509 | 510 | 511 | export const score_to_modifier = (score) => { 512 | let table = {"1": "-5", 513 | "2": "-4", "3": "-4", 514 | "4": "-3", "5": "-3", 515 | "6": "-2", "7": "-2", 516 | "8": "-1", "9": "-1", 517 | "10": "0", "11": "0", 518 | "12": "+1", "13": "+1", 519 | "14": "+2", "15": "+2", 520 | "16": "+3", "17": "+3", 521 | "18": "+4", "19": "+4", 522 | "20": "+5", "21": "+5", 523 | "22": "+6", "23": "+6", 524 | "24": "+7", "25": "+7", 525 | "26": "+8", "27": "+8", 526 | "28": "+9", "29": "+9", 527 | "30": "+10"}; 528 | return table[score]; 529 | } 530 | 531 | export const navigate = (data, navigation_view) => { 532 | if (data.filter) { 533 | page = new SearchTab(navigation_view); 534 | page.set_filter(unmake_manifest(data)); 535 | return; 536 | } 537 | var page_data = adapter.get_sync(data.url); 538 | var page = adapter.resolve_link(data, navigation_view); 539 | if (page == null) { 540 | log("could not navigate to " + data.url); 541 | } 542 | 543 | navigation_view.push(new Adw.NavigationPage( { title: "temp", child: page } )); 544 | setTimeout(page.update_title, 10); 545 | log("navigated to " + data.url) 546 | return; 547 | } 548 | 549 | 550 | function read_sync(path) { 551 | const file = Gio.File.new_for_path(path); 552 | 553 | const [contents, etag] = file.load_contents(null); 554 | 555 | const decoder = new TextDecoder('utf-8'); 556 | const contentsString = decoder.decode(contents); 557 | return contentsString; 558 | } 559 | 560 | export let sources = [ 561 | { 562 | built_in: true, 563 | path: "", 564 | name: "Player's Handbook", 565 | bookmarks: [ 566 | { url: "/api/monsters/aboleth", name: "Aboleth" } 567 | ] 568 | }, 569 | ]; 570 | 571 | function update_boookmark_menu() { 572 | window.bookmark_list.remove_all(); 573 | for (let i = 0; i < sources[window.source_index].bookmarks.length; i++) { 574 | window.bookmark_list.append(new BookmarkRow(sources[window.source_index].bookmarks[i])); 575 | } 576 | } 577 | 578 | export function is_bookmarked(data) { 579 | for (let i = 0; i < sources[window.source_index].bookmarks.length; i++) { 580 | if (sources[window.source_index].bookmarks[i].url == data.url) { 581 | return true; 582 | } 583 | } 584 | return false; 585 | } 586 | export function toggle_bookmarked(data, bookmarked) { 587 | for (let i = 0; i < sources[window.source_index].bookmarks.length; i++) { 588 | if (sources[window.source_index].bookmarks[i].url == data.url) { 589 | sources[window.source_index].bookmarks.splice(i, 1); 590 | save_state(); 591 | update_boookmark_menu(); 592 | return false; 593 | } 594 | } 595 | sources[window.source_index].bookmarks.push(data); 596 | save_state(); 597 | update_boookmark_menu(); 598 | return true; 599 | } 600 | 601 | 602 | 603 | export function save_state() { 604 | let data = { 605 | sources: sources, 606 | }; 607 | let dataJSON = JSON.stringify(data); 608 | let dataDir = GLib.get_user_config_dir(); 609 | let destination = GLib.build_filenamev([dataDir, 'libellus_state.json']); 610 | let file = Gio.File.new_for_path(destination); 611 | let [success, tag] = file.replace_contents(dataJSON, null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null); 612 | if(success) log("saved state"); 613 | else log("error saving state"); 614 | } 615 | 616 | function load_state() { 617 | let dataDir = GLib.get_user_config_dir(); 618 | let destination = GLib.build_filenamev([dataDir, 'libellus_state.json']); 619 | let file = Gio.File.new_for_path(destination); 620 | 621 | try { 622 | const [ok, contents, etag] = file.load_contents(null); 623 | const decoder = new TextDecoder(); 624 | const contentsString = decoder.decode(contents); 625 | let data = JSON.parse(contentsString); 626 | if (!data.sources) { 627 | log("sources not defined, probably old version"); 628 | save_state(); 629 | return; 630 | } 631 | sources = data.sources; 632 | log("loaded state"); 633 | } catch(e) { 634 | log("ERROR WHILE LOADING STATE: " + e); 635 | save_state(); 636 | } 637 | } 638 | 639 | export const NavView = GObject.registerClass({ 640 | GTypeName: 'NavView', 641 | }, class NavView extends Adw.Bin { 642 | constructor() { 643 | super({}); 644 | this.child = new Adw.NavigationView({}); 645 | 646 | this.pop = () => { 647 | this.child.pop(); 648 | this.update_title(); 649 | }; 650 | 651 | this.update_title = () => { 652 | let page = this.child.visible_page.child; 653 | page.update_title(); 654 | }; 655 | this.child.connect("popped", this.update_title); 656 | 657 | this.push = (page) => { 658 | this.child.push(page); 659 | }; 660 | 661 | this.get_visible_page = () => { 662 | return this.child.visible_page; 663 | } 664 | } 665 | }); 666 | 667 | 668 | export const make_filter = (filter) => { 669 | return { title: filter.title, func: filter.func, choices: JSON.parse(JSON.stringify(filter.choices)) }; 670 | } 671 | 672 | 673 | 674 | // eg. "Items", ["Light Armor", "Any"] 675 | export const make_manifest = (filter, settings) => { 676 | return { filter: filter, settings: settings }; 677 | } 678 | 679 | export const unmake_manifest = (manifest) => { 680 | let filter = make_filter(filter_options[manifest.filter]); 681 | for (let i in manifest.settings) { 682 | filter.choices[i].selected = manifest.settings[i]; 683 | } 684 | return filter; 685 | } 686 | 687 | -------------------------------------------------------------------------------- /src/window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 93 | 94 | 95 | --------------------------------------------------------------------------------