mirror of
https://github.com/zadam/trilium.git
synced 2026-05-07 00:36:59 +02:00
A new about dialog (#9151)
This commit is contained in:
118
.mailmap
118
.mailmap
@@ -1,2 +1,116 @@
|
||||
zadam <adam.zivner@gmail.com>
|
||||
zadam <zadam.apps@gmail.com>
|
||||
# Format: Canonical Name <canonical-email> <commit-email>
|
||||
# Merges aliases so `git shortlog`, `git log --use-mailmap`, etc. group commits per person.
|
||||
|
||||
# Core maintainers
|
||||
zadam <zadam.apps@gmail.com>
|
||||
zadam <zadam.apps@gmail.com> <adam.zivner@gmail.com>
|
||||
zadam <zadam.apps@gmail.com> <adam.zivner@gemalto.com>
|
||||
|
||||
Elian Doran <contact@eliandoran.me>
|
||||
Elian Doran <contact@eliandoran.me> <online@eliandoran.me>
|
||||
|
||||
Adorian Doran <adorian@esevo.ro>
|
||||
Adorian Doran <adorian@esevo.ro> <adoriandoran@gmail.com>
|
||||
|
||||
# Contributors with multiple emails / name variants
|
||||
Panagiotis Papadopoulos <pano_90@gmx.net> <102623907+pano9000@users.noreply.github.com>
|
||||
|
||||
Jon Fuller <jonfuller2012@gmail.com>
|
||||
|
||||
SiriusXT <1160925501@qq.com>
|
||||
SiriusXT <1160925501@qq.com> <11609255001@qq.com>
|
||||
SiriusXT <1160925501@qq.com> <37627919+SiriusXT@users.noreply.github.com>
|
||||
|
||||
JYC333 <22962980+JYC333@users.noreply.github.com>
|
||||
JYC333 <22962980+JYC333@users.noreply.github.com> <yuchuanjin333@gmail.com>
|
||||
|
||||
Nriver <6752679+Nriver@users.noreply.github.com>
|
||||
|
||||
Francis C. <normitomf@gmail.com>
|
||||
Francis C. <normitomf@gmail.com> <francistw@users.noreply.github.com>
|
||||
|
||||
Thomas Frei <7283497+thfrei@users.noreply.github.com>
|
||||
|
||||
hasecilu <hasecilu@tuta.io>
|
||||
|
||||
meinzzzz <lukas.geiselhart35@gmail.com>
|
||||
|
||||
FliegendeWurst <arne.keller@posteo.de>
|
||||
FliegendeWurst <arne.keller@posteo.de> <2012gdwu@web.de>
|
||||
FliegendeWurst <arne.keller@posteo.de> <2012gdwu+github@posteo.de>
|
||||
|
||||
MeIchthys <github.com@meichthys.com>
|
||||
MeIchthys <github.com@meichthys.com> <10717998+meichthys@users.noreply.github.com>
|
||||
|
||||
Marcel Wiechmann <marcel.wiechmann@gmail.com>
|
||||
Marcel Wiechmann <marcel.wiechmann@gmail.com> <github.y3y0w@sl.wiechmann.at>
|
||||
|
||||
Tomas Adamek <ad.tomik@seznam.cz>
|
||||
Tomas Adamek <ad.tomik@seznam.cz> <50672285+Kureii@users.noreply.github.com>
|
||||
|
||||
soulsands <407221377@qq.com>
|
||||
|
||||
chesspro13 <chesspro13@gmail.com>
|
||||
|
||||
sigaloid <69441971+sigaloid@users.noreply.github.com>
|
||||
|
||||
Marek Lewandowski <m.lewandowski@cksource.com>
|
||||
Marek Lewandowski <m.lewandowski@cksource.com> <code@mlewandowski.com>
|
||||
Marek Lewandowski <m.lewandowski@cksource.com> <mlewand@users.noreply.github.com>
|
||||
|
||||
lzinga <lucas.elzinga@outlook.com>
|
||||
lzinga <lucas.elzinga@outlook.com> <lzinga@users.noreply.github.com>
|
||||
|
||||
Sukant Gujar <sukantgujar@yahoo.com>
|
||||
|
||||
Matt Wilkie <maphew@gmail.com>
|
||||
Matt Wilkie <maphew@gmail.com> <matt.wilkie@yukon.ca>
|
||||
|
||||
Andreas Haan <andreas.mobil1@googlemail.com>
|
||||
|
||||
Potjoe-97 <42873357+Potjoe-97@users.noreply.github.com>
|
||||
Potjoe-97 <42873357+Potjoe-97@users.noreply.github.com> <giann@LAPTOPT490-GF>
|
||||
|
||||
Alex Pietsch <54153428+alexpietsch@users.noreply.github.com>
|
||||
|
||||
Laurent Cozic <laurent@cozic.net>
|
||||
Laurent Cozic <laurent@cozic.net> <laurent22@users.noreply.github.com>
|
||||
|
||||
Zexin Yuan <git@yzx9.xyz>
|
||||
Zexin Yuan <git@yzx9.xyz> <yuan.zx@outlook.com>
|
||||
|
||||
hulmgulm <hulmgulm@users.noreply.github.com>
|
||||
hulmgulm <hulmgulm@users.noreply.github.com> <12165268+hulmgulm@users.noreply.github.com>
|
||||
hulmgulm <hulmgulm@users.noreply.github.com> <github@hulmgulm.de>
|
||||
|
||||
Jules Bertholet <jules.bertholet@gmail.com>
|
||||
|
||||
Charles Dagenais <dagenais.charles@gmail.com>
|
||||
|
||||
Giulia Ye <yg97.cs@gmail.com>
|
||||
|
||||
baddate <37013819+baddate@users.noreply.github.com>
|
||||
|
||||
DerVogel101 <128903814+DerVogel101@users.noreply.github.com>
|
||||
DerVogel101 <128903814+DerVogel101@users.noreply.github.com> <jan.irmer@outlook.de>
|
||||
|
||||
Marcello Fuschi <marcellofuschi1@gmail.com>
|
||||
|
||||
Jiahao Lee <lijiahao34@live.com>
|
||||
|
||||
Dmitry Matveyev <dev@greenfork.me>
|
||||
Dmitry Matveyev <dev@greenfork.me> <info@greenfork.me>
|
||||
|
||||
Grant Zhu <a1065135230@gmail.com>
|
||||
|
||||
Sylvain Pasche <sylvain.pasche@gmail.com>
|
||||
Sylvain Pasche <sylvain.pasche@gmail.com> <spasche@spasche.net>
|
||||
|
||||
mm21 <8033134+mm21@users.noreply.github.com>
|
||||
mm21 <8033134+mm21@users.noreply.github.com> <mm21.dev@gmail.com>
|
||||
|
||||
BeatLink <git@beatlink.simplelogin.com>
|
||||
BeatLink <git@beatlink.simplelogin.com> <github@beatlink.simplelogin.com>
|
||||
|
||||
Florian Meißner <161936+Mystler@users.noreply.github.com>
|
||||
Florian Meißner <161936+Mystler@users.noreply.github.com> <developer@mystler.eu>
|
||||
|
||||
1
apps/client/src/assets/icon-classic.svg
Normal file
1
apps/client/src/assets/icon-classic.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path d="M63.966,45.043c0.008-0.009,0.021-0.021,0.027-0.029c0.938-1.156-0.823-13.453-5.063-20.125 c-1.389-2.186-2.239-3.423-3.219-4.719c-3.907-5.166-6-6.125-6-6.125S35.732,24.78,36.149,44.315 c-1.754,0.065-11.218,7.528-14.826,14.388c-1.206,2.291-1.856,3.645-2.493,5.141c-2.539,5.957-2.33,8.25-2.33,8.25 s16.271,6.79,33.014-3.294c0.007,0.021,0.013,0.046,0.02,0.063c0.537,1.389,12.08,5.979,19.976,5.621 c2.587-0.116,4.084-0.238,5.696-0.444c6.424-0.818,8.298-2.157,8.298-2.157S81.144,54.396,63.966,45.043z M50.787,65.343 c1.059-1.183,4.648-5.853,0.995-11.315c-0.253-0.377-0.496-0.236-0.496-0.236s0.063,10.822-5.162,12.359 c-5.225,1.537-13.886,4.4-20.427,0.455C25,66.186,26.924,53.606,38.544,47.229c0.546,1.599,2.836,6.854,9.292,6.409 c0.453-0.031,0.453-0.313,0.453-0.313s-9.422-5.328-8.156-10.625s3.089-14.236,9.766-17.948c0.714-0.397,10.746,7.593,10.417,20.94 c-1.606-0.319-7.377-1.004-10.226,4.864c-0.198,0.409,0.046,0.549,0.046,0.549s9.31-5.521,13.275-1.789 c3.965,3.733,10.813,9.763,10.71,17.4C74.111,67.533,62.197,72.258,50.787,65.343z M35.613,35.145c0,0-0.991,3.241-0.603,7.524 l-13.393-7.524C21.618,35.145,27.838,30.931,35.613,35.145z M21.193,36.03l13.344,7.612c-3.872,1.872-6.142,4.388-6.142,4.388 C20.78,43.531,21.193,36.03,21.193,36.03z M72.287,49.064c0,0-2.321-2.471-6.23-4.263l13.187-7.881 C79.243,36.92,79.808,44.413,72.287,49.064z M78.687,36.113l-13.237,7.794c0.3-4.291-0.754-7.511-0.754-7.511 C72.383,32.025,78.687,36.113,78.687,36.113z M42.076,73.778c0,0,3.309-0.737,6.845-3.185l0.056,15.361 C48.977,85.955,42.244,82.621,42.076,73.778z M49.956,85.888L50,70.526c3.539,2.445,6.846,3.181,6.846,3.181 C56.686,82.551,49.956,85.888,49.956,85.888z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
17
apps/client/src/assets/icon-nightly.svg
Normal file
17
apps/client/src/assets/icon-nightly.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg enable-background="new 0 0 256 256" version="1.1" viewBox="0 0 256 256" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Trilium Notes</title>
|
||||
<g>
|
||||
<path d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z" fill="#ab60e3"/>
|
||||
<path d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z" fill="#8038b8"/>
|
||||
<path d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z" fill="#560a8f"/>
|
||||
|
||||
<path d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z" fill="#bb9dd2"/>
|
||||
<path d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z" fill="#9a6cbc"/>
|
||||
<path d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z" fill="#783ba5"/>
|
||||
|
||||
<path d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z" fill="#ab60e3"/>
|
||||
<path d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z" fill="#8038b8"/>
|
||||
<path d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z" fill="#6f2796"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
28
apps/client/src/assets/icon.svg
Normal file
28
apps/client/src/assets/icon.svg
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg enable-background="new 0 0 256 256" version="1.1" viewBox="0 0 256 256" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Trilium Notes</title>
|
||||
<style type="text/css">
|
||||
.st0{fill:#95C980;}
|
||||
.st1{fill:#72B755;}
|
||||
.st2{fill:#4FA52B;}
|
||||
.st3{fill:#EE8C89;}
|
||||
.st4{fill:#E96562;}
|
||||
.st5{fill:#E33F3B;}
|
||||
.st6{fill:#EFB075;}
|
||||
.st7{fill:#E99547;}
|
||||
.st8{fill:#E47B19;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z"/>
|
||||
<path class="st1" d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z"/>
|
||||
<path class="st2" d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z"/>
|
||||
|
||||
<path class="st3" d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z"/>
|
||||
<path class="st4" d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z"/>
|
||||
<path class="st5" d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z"/>
|
||||
|
||||
<path class="st6" d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z"/>
|
||||
<path class="st7" d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z"/>
|
||||
<path class="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -1750,6 +1750,22 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
body.mobile .modal-dialog.modal-dialog-full-page-on-mobile {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: unset;
|
||||
max-width: unset;
|
||||
|
||||
.modal-content {
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
|
||||
.modal-body {
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.mobile .modal-content {
|
||||
overflow-y: auto;
|
||||
border-radius: var(--bs-modal-border-radius) var(--bs-modal-border-radius) 0 0;
|
||||
|
||||
@@ -269,9 +269,9 @@
|
||||
--timeline-connector-active-color: #ddd;
|
||||
--timeline-connector-hover-blend-mode: multiply;
|
||||
|
||||
--tooltip-background-color: rgba(255, 255, 255, 0.85);
|
||||
--tooltip-foreground-color: #000000ba;
|
||||
--tooltip-shadow-color: rgba(0, 0, 0, 0.15);
|
||||
--tooltip-background-color: rgba(0, 0, 0, 0.818);
|
||||
--tooltip-foreground-color: #ffffffeb;
|
||||
--tooltip-shadow-color: rgba(0, 0, 0, 0.2);
|
||||
|
||||
--help-background-color: #fffc;
|
||||
--help-card-background: var(--card-background-color);
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
{
|
||||
"about": {
|
||||
"title": "About Trilium Notes",
|
||||
"homepage": "Homepage:",
|
||||
"app_version": "App version:",
|
||||
"db_version": "DB version:",
|
||||
"sync_version": "Sync version:",
|
||||
"build_date": "Build date:",
|
||||
"build_revision": "Build revision:",
|
||||
"data_directory": "Data directory:"
|
||||
"about": {
|
||||
"version_label": "Version:",
|
||||
"version": "{{appVersion}} (database: {{dbVersion}}, sync protocol: {{syncVersion}})",
|
||||
"build_info": "Build: {{buildDate}}, revision: <buildRevision />",
|
||||
"contributors_label": "Contributors:",
|
||||
"contributor_roles": {
|
||||
"lead-dev": "lead developer",
|
||||
"original-dev": "original developer"
|
||||
},
|
||||
"role_brief_history": {
|
||||
"lead-dev": "Elian Doran founded TriliumNext in 2024, a community fork created after Zadam stepped back from the project. Zadam later transferred the original repository to the TriliumNext team, merging both projects back into one.",
|
||||
"original-dev": "On 25th December 2017, Zadam released the first beta of Trilium (written with a single \"l\", unlike the flower). Dissatisfied with existing note organizers, he built a powerful self-hosted hierarchical knowledge base that gathered over 22,000 GitHub stars. In 2024, as life got busier, he placed the project into maintenance mode."
|
||||
},
|
||||
"contributor_full_list": "See the entire community",
|
||||
"data_directory": "Data directory:",
|
||||
"github_tooltip": "Report bugs, suggest features, or contribute on GitHub",
|
||||
"license_tooltip": "View license",
|
||||
"donate": "Donate",
|
||||
"donate_tooltip": "Donate to support this project"
|
||||
},
|
||||
"toast": {
|
||||
"critical-error": {
|
||||
|
||||
179
apps/client/src/widgets/dialogs/about.css
Normal file
179
apps/client/src/widgets/dialogs/about.css
Normal file
@@ -0,0 +1,179 @@
|
||||
.about-dialog {
|
||||
:where(body.light-theme &) {
|
||||
--donate-button-color: #e33f3b;
|
||||
|
||||
&.nightly {
|
||||
--modal-background-color: #f2e1ff;
|
||||
}
|
||||
}
|
||||
|
||||
:where(body.dark-theme &) {
|
||||
--donate-button-color: #fba6a5;
|
||||
|
||||
&.nightly {
|
||||
--modal-background-color: #23182b;
|
||||
}
|
||||
}
|
||||
|
||||
--bs-modal-width: 680px;
|
||||
|
||||
.icon {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
|
||||
&[data-icon="default"] {
|
||||
background-image: url(../../assets/icon.svg);
|
||||
}
|
||||
|
||||
&[data-icon="nightly"] {
|
||||
background-image: url(../../assets/icon-nightly.svg);
|
||||
}
|
||||
|
||||
&[data-icon="default"],
|
||||
&[data-icon="nightly"] {
|
||||
animation: icon-intro 500ms ease-out;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
&[data-icon="classic"] {
|
||||
mask-image: url(../../assets/icon-classic.svg);
|
||||
background-color: var(--muted-text-color);
|
||||
animation: icon-classic-intro 300ms ease-in-out;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
all: unset;
|
||||
font-size: 2em;
|
||||
font-weight: 300;
|
||||
letter-spacing: 1pt;
|
||||
|
||||
.channel-name {
|
||||
opacity: .75;
|
||||
}
|
||||
}
|
||||
|
||||
.about-dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.about-dialog-property-sheet {
|
||||
margin-block: 30px;
|
||||
|
||||
&.wide {
|
||||
font-size: .85em;
|
||||
margin-inline: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.build-info {
|
||||
color: var(--muted-text-color);
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
.contributor-list {
|
||||
a, span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.contributor-role {
|
||||
text-decoration: underline dotted var(--main-text-color);
|
||||
text-underline-offset: 3px;
|
||||
text-decoration-color: var(--muted-text-color);
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.about-dialog-property-sheet.narrow & {
|
||||
line-height: 1.75;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
a {
|
||||
--_icon-size: 28px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: .9rem;
|
||||
color: var(--main-text-color);
|
||||
|
||||
&:hover {
|
||||
background: var(--icon-button-hover-background);
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: var(--_icon-size);
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: currentColor;
|
||||
height: var(--_icon-size);
|
||||
}
|
||||
|
||||
&.donate-link {
|
||||
color: var(--donate-button-color);
|
||||
|
||||
&:hover i {
|
||||
animation: heartbeat 600ms ease-in-out;
|
||||
animation-iteration-count: 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.about-dialog-brief-history-tooltip {
|
||||
--main-font-size: .9em;
|
||||
padding-inline: 30px;
|
||||
|
||||
.tooltip-inner {
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes icon-intro {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(.5);
|
||||
} to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes icon-classic-intro {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: rotate(50deg) scale(.5);
|
||||
} to {
|
||||
opacity: 1;
|
||||
transform: rotate(0deg) scale(1.25);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes heartbeat {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
} 50% {
|
||||
transform: scale(1.15);
|
||||
} 75% {
|
||||
transform: scale(1);
|
||||
} 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@@ -4,80 +4,233 @@ import { formatDateTime } from "../../utils/formatters.js";
|
||||
import server from "../../services/server.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import openService from "../../services/open.js";
|
||||
import { useState } from "preact/hooks";
|
||||
import type { CSSProperties } from "preact/compat";
|
||||
import type { AppInfo } from "@triliumnext/commons";
|
||||
import { useTriliumEvent } from "../react/hooks.jsx";
|
||||
import { useState, useCallback, useRef } from "preact/hooks";
|
||||
import type { AppInfo, Contributor, ContributorList } from "@triliumnext/commons";
|
||||
import { useTooltip, useTriliumEvent } from "../react/hooks.jsx";
|
||||
import { PropertySheet, PropertySheetItem } from "../react/PropertySheet.js";
|
||||
import "./about.css";
|
||||
import { Trans } from "react-i18next";
|
||||
import type React from "react";
|
||||
import contributors from "../../../../../contributors.json";
|
||||
import { Fragment } from "preact/jsx-runtime";
|
||||
import type { ComponentChildren } from "preact";
|
||||
import { useMemo, memo } from "preact/compat";
|
||||
import clsx from "clsx";
|
||||
|
||||
export default function AboutDialog() {
|
||||
const [appInfo, setAppInfo] = useState<AppInfo | null>(null);
|
||||
const [shown, setShown] = useState(false);
|
||||
const forceWordBreak: CSSProperties = { wordBreak: "break-all" };
|
||||
const [isShown, setIsShown] = useState(false);
|
||||
const [isNightly, setNightly] = useState(false);
|
||||
const [icon, setIcon] = useState("default");
|
||||
const [altIcon, setAltIcon] = useState<string | null>(null);
|
||||
|
||||
useTriliumEvent("openAboutDialog", () => setShown(true));
|
||||
const hasLoaded = useRef(false);
|
||||
|
||||
const onLoad = useCallback(async () => {
|
||||
if (!hasLoaded.current) {
|
||||
const info = await server.get<AppInfo>("app-info");
|
||||
if (info.appVersion.includes("test")) {
|
||||
setNightly(true);
|
||||
setIcon("nightly");
|
||||
}
|
||||
setAppInfo(info);
|
||||
hasLoaded.current = true;
|
||||
|
||||
}
|
||||
setIsShown(true);
|
||||
}, []);
|
||||
|
||||
useTriliumEvent("openAboutDialog", onLoad);
|
||||
|
||||
const createContributorHoverHandler = () => {
|
||||
let timeoutID: ReturnType<typeof setTimeout>;
|
||||
return (contributor: Contributor, isHovering: boolean, part: "name" | "role") => {
|
||||
if (part === "role" && contributor.role === "original-dev") {
|
||||
if (isHovering) {
|
||||
timeoutID = setTimeout(() => {
|
||||
setAltIcon("classic");
|
||||
}, 500);
|
||||
} else {
|
||||
clearTimeout(timeoutID);
|
||||
setAltIcon(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* Cache the contributor list to prevent its rerendering.
|
||||
* When the icon changes, it triggers a rerender of the dialog. If this happens while an
|
||||
* element with a tooltip is hovered, its tooltip will break. */
|
||||
const CachedContributors = useMemo(() => memo(function CachedContributors() {
|
||||
return <Contributors
|
||||
data={contributors as ContributorList}
|
||||
onHover={createContributorHoverHandler()}
|
||||
/>
|
||||
}), []);
|
||||
|
||||
return (
|
||||
<Modal className="about-dialog"
|
||||
size="lg"
|
||||
title={t("about.title")}
|
||||
show={shown}
|
||||
onShown={async () => {
|
||||
const appInfo = await server.get<AppInfo>("app-info");
|
||||
setAppInfo(appInfo);
|
||||
}}
|
||||
onHidden={() => setShown(false)}
|
||||
<Modal
|
||||
className={clsx(["about-dialog", {"nightly": isNightly}])}
|
||||
size="md"
|
||||
isFullPageOnMobile
|
||||
show={isShown}
|
||||
onHidden={() => setIsShown(false)}
|
||||
>
|
||||
<table className="table table-borderless">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{t("about.homepage")}</th>
|
||||
<td className="selectable-text"><a className="tn-link external" href="https://github.com/TriliumNext/Trilium" style={forceWordBreak}>https://github.com/TriliumNext/Trilium</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t("about.app_version")}</th>
|
||||
<td className="app-version selectable-text">{appInfo?.appVersion}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t("about.db_version")}</th>
|
||||
<td className="db-version selectable-text">{appInfo?.dbVersion}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t("about.sync_version")}</th>
|
||||
<td className="sync-version selectable-text">{appInfo?.syncVersion}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t("about.build_date")}</th>
|
||||
<td className="build-date selectable-text">
|
||||
{appInfo?.buildDate ? formatDateTime(appInfo.buildDate) : ""}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t("about.build_revision")}</th>
|
||||
<td className="selectable-text">
|
||||
{appInfo?.buildRevision && <a className="tn-link build-revision external" href={`https://github.com/TriliumNext/Trilium/commit/${appInfo.buildRevision}`} target="_blank" style={forceWordBreak}>{appInfo.buildRevision}</a>}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t("about.data_directory")}</th>
|
||||
<td className="data-directory">
|
||||
{appInfo?.dataDirectory && (<DirectoryLink directory={appInfo.dataDirectory} style={forceWordBreak} />)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="about-dialog-content">
|
||||
|
||||
<div className={"icon"} data-icon={altIcon ?? icon} />
|
||||
<h2>Trilium Notes {isNightly && <span className="channel-name">Nightly</span>}</h2>
|
||||
<a className="tn-link" href="https://triliumnotes.org/" target="_blank" rel="noopener noreferrer">
|
||||
triliumnotes.org
|
||||
</a>
|
||||
|
||||
<PropertySheet className="about-dialog-property-sheet">
|
||||
<PropertySheetItem label={t("about.version_label")}>
|
||||
{t("about.version", {
|
||||
appVersion: appInfo?.appVersion,
|
||||
dbVersion: appInfo?.dbVersion,
|
||||
syncVersion: appInfo?.syncVersion
|
||||
})}
|
||||
<div className="build-info">
|
||||
<Trans
|
||||
i18nKey="about.build_info"
|
||||
values={{
|
||||
buildDate: appInfo?.buildDate ? formatDateTime(appInfo.buildDate) : ""
|
||||
}}
|
||||
components={{
|
||||
buildRevision: <RevisionLink appInfo={appInfo} /> as React.ReactElement
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</PropertySheetItem>
|
||||
|
||||
<PropertySheetItem className="contributor-list use-tn-links" label={t("about.contributors_label")}>
|
||||
<CachedContributors />
|
||||
|
||||
<a href="https://github.com/TriliumNext/Trilium/graphs/contributors" target="_blank" rel="noopener noreferrer">
|
||||
{t("about.contributor_full_list")}
|
||||
</a>
|
||||
</PropertySheetItem>
|
||||
|
||||
<PropertySheetItem label={t("about.data_directory")}>
|
||||
<div style={{wordBreak: "break-all"}}>
|
||||
{appInfo?.dataDirectory && (<DirectoryLink directory={appInfo.dataDirectory} />)}
|
||||
</div>
|
||||
</PropertySheetItem>
|
||||
</PropertySheet>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<FooterLink
|
||||
text="GitHub"
|
||||
url="https://github.com/TriliumNext/Trilium"
|
||||
tooltip={t("about.github_tooltip")}>
|
||||
|
||||
<i className='bx bxl-github'></i>
|
||||
</FooterLink>
|
||||
|
||||
<FooterLink
|
||||
text="AGPL 3.0"
|
||||
url="https://docs.triliumnotes.org/user-guide/misc/license"
|
||||
tooltip={t("about.license_tooltip")}>
|
||||
|
||||
{/* https://pictogrammers.com/library/mdi/icon/scale-balance/ */}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,3C10.73,3 9.6,3.8 9.18,5H3V7H4.95L2,14C1.53,16 3,17 5.5,17C8,17 9.56,16 9,14L6.05,7H9.17C9.5,7.85 10.15,8.5 11,8.83V20H2V22H22V20H13V8.82C13.85,8.5 14.5,7.85 14.82,7H17.95L15,14C14.53,16 16,17 18.5,17C21,17 22.56,16 22,14L19.05,7H21V5H14.83C14.4,3.8 13.27,3 12,3M12,5A1,1 0 0,1 13,6A1,1 0 0,1 12,7A1,1 0 0,1 11,6A1,1 0 0,1 12,5M5.5,10.25L7,14H4L5.5,10.25M18.5,10.25L20,14H17L18.5,10.25Z" /></svg>
|
||||
</FooterLink>
|
||||
|
||||
<FooterLink
|
||||
text={t("about.donate")}
|
||||
url="https://triliumnotes.org/en/support-us"
|
||||
tooltip={t("about.donate_tooltip")}
|
||||
className="donate-link">
|
||||
|
||||
<i className='bx bx-heart' ></i>
|
||||
</FooterLink>
|
||||
</footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function DirectoryLink({ directory, style }: { directory: string, style?: CSSProperties }) {
|
||||
function RevisionLink({appInfo}: {appInfo: AppInfo | null}) {
|
||||
return <>
|
||||
{appInfo?.buildRevision && <a href={`https://github.com/TriliumNext/Trilium/commit/${appInfo.buildRevision}`} target="_blank" rel="noopener noreferrer" className="tn-link">
|
||||
{appInfo.buildRevision.substring(0, 7)}
|
||||
</a>}
|
||||
</>;
|
||||
}
|
||||
|
||||
function FooterLink(props: {children: ComponentChildren, text: string, url: string, tooltip: string, className?: string}) {
|
||||
|
||||
const linkRef = useRef<HTMLAnchorElement>(null);
|
||||
|
||||
useTooltip(linkRef, {
|
||||
title: props.tooltip,
|
||||
delay: 250,
|
||||
placement: "bottom"
|
||||
})
|
||||
|
||||
return <a ref={linkRef} href={props.url} className={props.className} target="_blank" rel="noopener noreferrer" draggable={false}>
|
||||
{props.children}
|
||||
{props.text}
|
||||
</a>
|
||||
}
|
||||
|
||||
type HoverCallback = (contributor: Contributor, isHovering: boolean, part: "name" | "role") => void;
|
||||
|
||||
function Contributors({data, onHover}: {data: ContributorList, onHover?: HoverCallback}) {
|
||||
return data.contributors.map((c, index, array) => {
|
||||
return <Fragment key={c.name}>
|
||||
<ContributorListItem data={c} onHover={onHover} />
|
||||
|
||||
{/* Add a comma between items */}
|
||||
{(index < array.length - 1) ? ", " : ". "}
|
||||
</Fragment>
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function ContributorListItem({data, onHover}: {data: Contributor, onHover?: HoverCallback}) {
|
||||
const roleRef = useRef<HTMLSpanElement>(null);
|
||||
const roleString = (data.role) ? t(`about.contributor_roles.${data.role}`) : "";
|
||||
|
||||
useTooltip(roleRef, (data.role) ? {
|
||||
title: t(`about.role_brief_history.${data.role}`),
|
||||
customClass: "about-dialog-brief-history-tooltip",
|
||||
placement: "bottom",
|
||||
offset: [0, 10],
|
||||
delay: 500
|
||||
}: {});
|
||||
|
||||
return <>
|
||||
<a
|
||||
href={data.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onMouseEnter={() => onHover?.(data, true, "name")}
|
||||
onMouseLeave={() => onHover?.(data, false, "name")}>
|
||||
|
||||
{data.fullName ?? data.name}
|
||||
</a>
|
||||
|
||||
{roleString && <span
|
||||
ref={roleRef}
|
||||
onMouseEnter={() => onHover?.(data, true, "role")}
|
||||
onMouseLeave={() => onHover?.(data, false, "role")}>
|
||||
|
||||
(<span className="contributor-role">{roleString}</span>)
|
||||
</span>}
|
||||
</>
|
||||
}
|
||||
|
||||
function DirectoryLink({ directory }: { directory: string}) {
|
||||
if (utils.isElectron()) {
|
||||
const onClick = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
openService.openDirectory(directory);
|
||||
};
|
||||
|
||||
return <a className="tn-link selectable-text" href="#" onClick={onClick} style={style}>{directory}</a>
|
||||
return <a className="tn-link selectable-text" href="#" onClick={onClick}>{directory}</a>
|
||||
} else {
|
||||
return <span className="selectable-text" style={style}>{directory}</span>;
|
||||
return <span className="selectable-text">{directory}</span>;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
apps/client/src/widgets/react/FluidWrapper.tsx
Normal file
43
apps/client/src/widgets/react/FluidWrapper.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import clsx from "clsx";
|
||||
import { ComponentChildren } from "preact";
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
interface FluidWrapperParams {
|
||||
className?: string;
|
||||
breakpoints: {[key: string]: number};
|
||||
children: ComponentChildren;
|
||||
}
|
||||
|
||||
export function FluidWrapper({className, breakpoints, children}: FluidWrapperParams) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const sortedBreakpoints = useMemo(() => {
|
||||
return Object.entries(breakpoints).sort(([, a], [, b]) => a - b)
|
||||
}, [breakpoints]);
|
||||
const [activeBreakpoint, setActiveBreakpoint] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
const onWidthChanged = (width: number) => {
|
||||
let match = sortedBreakpoints[0]?.[0] ?? null;
|
||||
for (const [name, min] of sortedBreakpoints) {
|
||||
if (width >= min) match = name;
|
||||
else break;
|
||||
}
|
||||
setActiveBreakpoint(match);
|
||||
};
|
||||
|
||||
const observer = new ResizeObserver(([entry]) => onWidthChanged(entry.contentRect.width));
|
||||
observer.observe(el);
|
||||
onWidthChanged(el.getBoundingClientRect().width);
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [sortedBreakpoints]);
|
||||
|
||||
return <div ref={ref} className="fluid-container">
|
||||
<div className={clsx(className, activeBreakpoint)}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -15,7 +15,7 @@ interface CustomTitleBarButton {
|
||||
|
||||
export interface ModalProps {
|
||||
className: string;
|
||||
title: string | ComponentChildren;
|
||||
title?: string | ComponentChildren;
|
||||
customTitleBarButtons?: (CustomTitleBarButton | null)[];
|
||||
size: "xl" | "lg" | "md" | "sm";
|
||||
children: ComponentChildren;
|
||||
@@ -83,9 +83,13 @@ export interface ModalProps {
|
||||
* spanning the entire height alongside the header, body and footer.
|
||||
*/
|
||||
sidebar?: ComponentChildren;
|
||||
/**
|
||||
* Indicates if the dialog will be displayed as a full page on mobile devices.
|
||||
*/
|
||||
isFullPageOnMobile?: boolean;
|
||||
}
|
||||
|
||||
export default function Modal({ children, className, size, title, customTitleBarButtons: titleBarButtons, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden, modalRef: externalModalRef, formRef, bodyStyle, show, stackable, keepInDom, noFocus, sidebar }: ModalProps) {
|
||||
export default function Modal({ children, className, size, title, customTitleBarButtons: titleBarButtons, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden, modalRef: externalModalRef, formRef, bodyStyle, show, stackable, keepInDom, noFocus, sidebar, isFullPageOnMobile }: ModalProps) {
|
||||
const modalRef = useSyncedRef<HTMLDivElement>(externalModalRef);
|
||||
const modalInstanceRef = useRef<BootstrapModal>();
|
||||
const elementToFocus = useRef<Element | null>();
|
||||
@@ -149,7 +153,7 @@ export default function Modal({ children, className, size, title, customTitleBar
|
||||
|
||||
return (
|
||||
<div className={`modal fade mx-auto ${className}`} tabIndex={-1} style={dialogStyle} role="dialog" ref={modalRef}>
|
||||
{(show || keepInDom) && <div className={`modal-dialog modal-${size} ${scrollable ? "modal-dialog-scrollable" : ""}`} style={documentStyle} role="document">
|
||||
{(show || keepInDom) && <div className={clsx("modal-dialog", `modal-${size}`, {"modal-dialog-scrollable": scrollable, "modal-dialog-full-page-on-mobile": isFullPageOnMobile, "modal-content-with-sidebar": sidebar})} style={documentStyle} role="document">
|
||||
<div className={clsx("modal-content", sidebar && "modal-content-with-sidebar")}>
|
||||
{sidebar && <div className="modal-sidebar">
|
||||
{title && <div className="modal-sidebar-header">
|
||||
|
||||
82
apps/client/src/widgets/react/PropertySheet.css
Normal file
82
apps/client/src/widgets/react/PropertySheet.css
Normal file
@@ -0,0 +1,82 @@
|
||||
:where(.property-sheet) {
|
||||
--border-radius: 8px;
|
||||
}
|
||||
|
||||
.property-sheet {
|
||||
dl {
|
||||
background: var(--card-background-color);
|
||||
|
||||
dt {
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
dd {
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.property-sheet-container.wide .property-sheet {
|
||||
display: table;
|
||||
border-spacing: 0 2px;
|
||||
border-collapse: separate;
|
||||
|
||||
dl {
|
||||
display: table-row;
|
||||
--_br: var(--border-radius);
|
||||
|
||||
&:first-child {
|
||||
clip-path: inset(0 round var(--_br) var(--_br) 0 0);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
clip-path: inset(0 round 0 0 var(--_br) var(--_br));
|
||||
}
|
||||
}
|
||||
|
||||
dt, dd {
|
||||
display: table-cell;
|
||||
padding: 10px 16px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
dt {
|
||||
white-space: nowrap;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
dl {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.property-sheet-container.narrow .property-sheet {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
|
||||
dl {
|
||||
margin: 0;
|
||||
padding: 16px 20px;
|
||||
|
||||
&:first-child {
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
}
|
||||
|
||||
dt {
|
||||
margin-bottom: 8px;
|
||||
font-size: .85em;
|
||||
font-weight: 550;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .75pt;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
apps/client/src/widgets/react/PropertySheet.tsx
Normal file
28
apps/client/src/widgets/react/PropertySheet.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ComponentChildren } from "preact";
|
||||
import clsx from "clsx";
|
||||
import "./PropertySheet.css";
|
||||
import { FluidWrapper } from "./FluidWrapper";
|
||||
|
||||
interface PropertySheetParams {
|
||||
className?: string;
|
||||
children: ComponentChildren;
|
||||
wideLayoutBreakpoint?: number;
|
||||
}
|
||||
|
||||
export function PropertySheet({className, children, wideLayoutBreakpoint}: PropertySheetParams) {
|
||||
return <FluidWrapper
|
||||
className={clsx("property-sheet-container", className)}
|
||||
breakpoints={{narrow: 0, wide: wideLayoutBreakpoint || 600}}>
|
||||
|
||||
<div className="property-sheet">
|
||||
{children}
|
||||
</div>
|
||||
</FluidWrapper>
|
||||
}
|
||||
|
||||
export function PropertySheetItem({className, label, children}: {className?: string, label: string, children: ComponentChildren}) {
|
||||
return <dl>
|
||||
<dt>{label}</dt>
|
||||
<dd className={className}>{children}</dd>
|
||||
</dl>
|
||||
}
|
||||
35
contributors.json
Normal file
35
contributors.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"⚠️": "Auto-generated file. Run `pnpm run update-contributors` to regenerate.",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "eliandoran",
|
||||
"fullName": "Elian Doran",
|
||||
"url": "https://github.com/eliandoran",
|
||||
"role": "lead-dev"
|
||||
},
|
||||
{
|
||||
"name": "zadam",
|
||||
"fullName": "Zadam",
|
||||
"url": "https://github.com/zadam",
|
||||
"role": "original-dev"
|
||||
},
|
||||
{
|
||||
"name": "adoriandoran",
|
||||
"fullName": "Adorian Doran",
|
||||
"url": "https://github.com/adoriandoran"
|
||||
},
|
||||
{
|
||||
"name": "perfectra1n",
|
||||
"fullName": "Jon Fuller",
|
||||
"url": "https://github.com/perfectra1n"
|
||||
},
|
||||
{
|
||||
"name": "JYC333",
|
||||
"url": "https://github.com/JYC333"
|
||||
},
|
||||
{
|
||||
"name": "Nriver",
|
||||
"url": "https://github.com/Nriver"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -42,7 +42,8 @@
|
||||
"dev:linter-check": "cross-env NODE_OPTIONS=--max_old_space_size=4096 eslint .",
|
||||
"dev:linter-fix": "cross-env NODE_OPTIONS=--max_old_space_size=4096 eslint . --fix",
|
||||
"postinstall": "tsx scripts/electron-rebuild.mts && pnpm prepare",
|
||||
"prepare": "pnpm run --filter pdfjs-viewer --filter share-theme build && pnpm run --filter web-clipper postinstall"
|
||||
"prepare": "pnpm run --filter pdfjs-viewer --filter share-theme build && pnpm run --filter web-clipper postinstall",
|
||||
"update-contributors": "tsx ./scripts/update-contributors.ts"
|
||||
},
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
|
||||
@@ -8,6 +8,7 @@ export * from "./lib/mime_type.js";
|
||||
export * from "./lib/bulk_actions.js";
|
||||
export * from "./lib/server_api.js";
|
||||
export * from "./lib/shared_constants.js";
|
||||
export * from "./lib/shared_types.js";
|
||||
export * from "./lib/ws_api.js";
|
||||
export * from "./lib/attribute_names.js";
|
||||
export * from "./lib/utils.js";
|
||||
|
||||
10
packages/commons/src/lib/shared_types.ts
Normal file
10
packages/commons/src/lib/shared_types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface ContributorList {
|
||||
contributors: Contributor[];
|
||||
}
|
||||
|
||||
export interface Contributor {
|
||||
name: string;
|
||||
fullName?: string;
|
||||
url: string;
|
||||
role?: "lead-dev" | "original-dev";
|
||||
}
|
||||
223
scripts/update-contributors.ts
Normal file
223
scripts/update-contributors.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
interface ContributorEntry {
|
||||
name: string;
|
||||
fullName?: string;
|
||||
url: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
interface ContributorFile {
|
||||
contributors: ContributorEntry[];
|
||||
}
|
||||
|
||||
interface ContributorInfo {
|
||||
name: string;
|
||||
fullName?: string;
|
||||
email?: string;
|
||||
commitCount: number;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
interface ShowTableParams {
|
||||
title: string;
|
||||
comment?: string;
|
||||
contributors: ContributorInfo[];
|
||||
columns: (keyof ContributorInfo)[];
|
||||
}
|
||||
|
||||
const TRANSLATION_PATHS = [
|
||||
"apps/client/src/translations/",
|
||||
"apps/server/src/assets/translations/"
|
||||
];
|
||||
|
||||
/** Authors that are bots or automated tools, not real contributors. */
|
||||
const EXCLUDED_AUTHORS = new Set([
|
||||
"Languages add-on",
|
||||
"Hosted Weblate",
|
||||
"renovate[bot]"
|
||||
]);
|
||||
|
||||
const NOREPLY_PATTERN = /^(?:\d+\+)?(.+)@users\.noreply\.github\.com$/;
|
||||
|
||||
/**
|
||||
* Manual mapping for contributors whose git email doesn't reveal their
|
||||
* GitHub username (i.e. no noreply email in .mailmap).
|
||||
*/
|
||||
const EMAIL_TO_GITHUB: Record<string, string> = {
|
||||
"contact@eliandoran.me": "eliandoran",
|
||||
"zadam.apps@gmail.com": "zadam",
|
||||
"adorian@esevo.ro": "adoriandoran",
|
||||
"jonfuller2012@gmail.com": "perfectra1n",
|
||||
};
|
||||
|
||||
const CONTRIBUTORS_PATH = join(__dirname, "..", "contributors.json");
|
||||
|
||||
/**
|
||||
* Resolves a GitHub username from an email address.
|
||||
*
|
||||
* 1. Checks the manual mapping.
|
||||
* 2. Extracts from GitHub noreply emails (e.g. "12345+user@…").
|
||||
* 3. Scans .mailmap for alternate emails that match the noreply pattern.
|
||||
*/
|
||||
function resolveGitHub(email: string, name: string): string | undefined {
|
||||
if (EMAIL_TO_GITHUB[email]) return EMAIL_TO_GITHUB[email];
|
||||
|
||||
const noreply = email.match(NOREPLY_PATTERN);
|
||||
if (noreply) return noreply[1];
|
||||
|
||||
// Grep .mailmap for alternate emails that match the noreply pattern
|
||||
try {
|
||||
const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const mailmapContent = execSync(`grep -i "${escapedName}" .mailmap 2>/dev/null`).toString();
|
||||
for (const line of mailmapContent.split("\n")) {
|
||||
// Extract all emails from the line (inside angle brackets)
|
||||
for (const [, email] of line.matchAll(/<([^>]+)>/g)) {
|
||||
const match = email.match(NOREPLY_PATTERN);
|
||||
if (match) return match[1];
|
||||
}
|
||||
}
|
||||
} catch { /* no matches */ }
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parseShortlog(rawOutput: string): Map<string, { email: string; commitCount: number }> {
|
||||
const result = new Map<string, { email: string; commitCount: number }>();
|
||||
for (const line of rawOutput.split("\n")) {
|
||||
const match = line.match(/^\s*(\d+)\s+(.+?)\s+<(.+)>$/);
|
||||
if (match) {
|
||||
result.set(match[2], { email: match[3], commitCount: parseInt(match[1]) });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { developers } = listLocalGitContributors();
|
||||
await listGitHubContributors();
|
||||
updateContributorsJson(developers);
|
||||
}
|
||||
|
||||
function listLocalGitContributors() {
|
||||
const allOutput = execSync("git shortlog -sne --no-merges HEAD -- src/ apps/").toString();
|
||||
const translationOutput = execSync(`git shortlog -sne --no-merges HEAD -- ${TRANSLATION_PATHS.join(" ")}`).toString();
|
||||
|
||||
const allContribs = parseShortlog(allOutput);
|
||||
const translationContribs = parseShortlog(translationOutput);
|
||||
|
||||
const developers: ContributorInfo[] = [];
|
||||
const translators: ContributorInfo[] = [];
|
||||
const MIN_COMMITS = 100;
|
||||
for (const [name, { email, commitCount }] of allContribs) {
|
||||
if (EXCLUDED_AUTHORS.has(name)) continue;
|
||||
|
||||
const translationCommitCount = translationContribs.get(name)?.commitCount ?? 0;
|
||||
const isTranslator = translationCommitCount > commitCount * 0.5;
|
||||
|
||||
const githubUsername = resolveGitHub(email, name);
|
||||
const url = githubUsername ? `https://github.com/${githubUsername}` : undefined;
|
||||
const entry: ContributorInfo = { name, email, commitCount, url };
|
||||
|
||||
if (isTranslator) {
|
||||
if (commitCount >= 20) translators.push(entry);
|
||||
} else if (commitCount >= MIN_COMMITS) {
|
||||
developers.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// showTable({
|
||||
// title: "Local Git Contributors (Developers)",
|
||||
// columns: ["name", "url", "commitCount"],
|
||||
// contributors: developers
|
||||
// });
|
||||
|
||||
// showTable({
|
||||
// title: "Local Git Contributors (Translators)",
|
||||
// comment: "Contributors where >50% of commits are to translation files.",
|
||||
// columns: ["name", "url", "commitCount"],
|
||||
// contributors: translators
|
||||
// });
|
||||
|
||||
return { developers, translators };
|
||||
}
|
||||
|
||||
async function listGitHubContributors() {
|
||||
let list: any[] | null = null;
|
||||
|
||||
const response = await fetch("https://api.github.com/repos/TriliumNext/Trilium/contributors");
|
||||
if (response.ok) {
|
||||
list = await response.json();
|
||||
} else {
|
||||
console.error(`Unable to request the contributor list from GitHub. Reason: ${response.statusText}`);
|
||||
}
|
||||
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
|
||||
const MIN_CONTRIBUTIONS = 125;
|
||||
const contributors: ContributorInfo[] = list
|
||||
.filter((c) => c.contributions >= MIN_CONTRIBUTIONS)
|
||||
.map((c) => {
|
||||
return {
|
||||
name: c.login,
|
||||
url: c.html_url,
|
||||
commitCount: c.contributions
|
||||
} as ContributorInfo;
|
||||
});
|
||||
|
||||
// showTable({
|
||||
// title: "GitHub Contributor List",
|
||||
// comment: "Note: the GitHub list also include contributors that did not directly contribute to Trilium, but to submodules used in the Trilium's repo.",
|
||||
// contributors: contributors,
|
||||
// columns: ["name", "url", "commitCount"]
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates contributors.json, preserving pinned entries (those with special
|
||||
* roles like lead-dev, original-dev) and regenerating the rest from git data.
|
||||
*/
|
||||
function updateContributorsJson(developers: ContributorInfo[]) {
|
||||
// Read existing file to preserve pinned entries
|
||||
const existing: ContributorFile = JSON.parse(readFileSync(CONTRIBUTORS_PATH, "utf-8"));
|
||||
const pinnedRoles = new Set<string>(["lead-dev", "original-dev"]);
|
||||
const pinned = existing.contributors.filter((c) => c.role && pinnedRoles.has(c.role));
|
||||
|
||||
// Build a set of pinned GitHub usernames to avoid duplicates
|
||||
const pinnedNames = new Set(pinned.map((c) => c.name));
|
||||
|
||||
const contributors: ContributorEntry[] = [...pinned];
|
||||
|
||||
// Add developers (skip those already pinned)
|
||||
for (const dev of developers) {
|
||||
const githubName = dev.url?.replace("https://github.com/", "");
|
||||
if (!githubName || pinnedNames.has(githubName)) continue;
|
||||
|
||||
contributors.push({
|
||||
name: githubName,
|
||||
fullName: dev.name !== githubName ? dev.name : undefined,
|
||||
url: dev.url!
|
||||
});
|
||||
}
|
||||
|
||||
const output = {
|
||||
"⚠️": "Auto-generated file. Run `pnpm run update-contributors` to regenerate.",
|
||||
contributors
|
||||
};
|
||||
writeFileSync(CONTRIBUTORS_PATH, JSON.stringify(output, null, 4) + "\n");
|
||||
console.log(`\n✅ Updated ${CONTRIBUTORS_PATH} with ${contributors.length} contributors.`);
|
||||
}
|
||||
|
||||
function showTable(params: ShowTableParams) {
|
||||
console.log(`\n──── ${params.title} ────`);
|
||||
if (params.comment) {
|
||||
console.log(`\n${params.comment}\n`);
|
||||
}
|
||||
console.table(params.contributors, params.columns);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user