Compare commits
10 commits
10e4a80d2e
...
89c431a15a
| Author | SHA1 | Date | |
|---|---|---|---|
| 89c431a15a | |||
| 1c40e761f7 | |||
| 42d498f3f7 | |||
| dabb041752 | |||
| 6663677829 | |||
| 5db9c6b11b | |||
| 31851b851c | |||
| eceed3694e | |||
| fa79da3a7b | |||
| b2c04aef0c |
10 changed files with 654 additions and 470 deletions
|
|
@ -2,6 +2,8 @@ https://voussoir.net
|
|||
|
||||
## Mirrors
|
||||
|
||||
https://git.voussoir.net/voussoir/voussoir.net
|
||||
|
||||
https://github.com/voussoir/voussoir.net
|
||||
|
||||
https://gitlab.com/voussoir/voussoir.net
|
||||
|
|
|
|||
|
|
@ -86,6 +86,8 @@ def linkchecker(do_external=True):
|
|||
url = queue.popleft()
|
||||
if url == 'https://voussoir.net/writing':
|
||||
url = 'https://voussoir.net/writing/'
|
||||
if '/slamming_and_blasting' in url:
|
||||
continue
|
||||
result = dotdict.DotDict()
|
||||
result.exc = None
|
||||
result.url = url
|
||||
|
|
@ -99,7 +101,7 @@ def linkchecker(do_external=True):
|
|||
if link not in seen:
|
||||
queue.append(link)
|
||||
seen.add(link)
|
||||
elif result.head.headers['content-type'] == 'text/html' and not url.endswith('.html'):
|
||||
elif 'text/html' in result.head.headers['content-type'] and not url.endswith('.html'):
|
||||
log.debug('GET %s', url)
|
||||
response = session.get(url)
|
||||
soup = bs4.BeautifulSoup(response.text, 'html.parser')
|
||||
|
|
|
|||
|
|
@ -426,7 +426,7 @@ body.start_eating_that_trashcan .cvitem_details
|
|||
<div class="cvitem_details">
|
||||
<p>trkpt is a 24/7 GPS recorder that shows me where I've been.</p>
|
||||
<p><a href="https://voussoir.net/writing/obsessed_with_gpx">https://voussoir.net/writing/obsessed_with_gpx</a></p>
|
||||
<p><a href="https://github.com/voussoir/trkpt">https://github.com/voussoir/trkpt</a></p>
|
||||
<p><a href="https://git.voussoir.net/voussoir/trkpt">https://git.voussoir.net/voussoir/trkpt</a></p>
|
||||
</div>
|
||||
<img class="cvitem_logo" style='background-color: transparent !important' src="./cv/trkpt.png"/>
|
||||
</div>
|
||||
|
|
@ -436,7 +436,7 @@ body.start_eating_that_trashcan .cvitem_details
|
|||
<div class="cvitem_details">
|
||||
<p>BringRSS is an RSS client and newsreader with a web interface, made with Flask and SQLite3. RSS is a great way to keep up with your favorite forums, bloggers, podcasts, and newspapers since all the new posts come straight to you in a single place. BringRSS can send news objects to your own Python scripts, allowing for powerful automation like podcast downloading, email notifications, and other more niche features that would be outside the scope of the BringRSS application itself.</p>
|
||||
<p><a href="https://bringrss.voussoir.net">Live demo</a></p>
|
||||
<p><a href="https://github.com/voussoir/bringrss">https://github.com/voussoir/bringrss</a></p>
|
||||
<p><a href="https://git.voussoir.net/voussoir/bringrss">https://git.voussoir.net/voussoir/bringrss</a></p>
|
||||
</div>
|
||||
<img class="cvitem_logo" src="./cv/bringrss.png"/>
|
||||
</div>
|
||||
|
|
@ -445,7 +445,7 @@ body.start_eating_that_trashcan .cvitem_details
|
|||
<h3 class="cvitem_title" id="ycdl">YCDL</h3>
|
||||
<div class="cvitem_details">
|
||||
<p>YoutubeChannelDownloader was born out of a dissatisfaction with YouTube's own interface for keeping track of which videos I have already watched, as well as a desire to integrate with youtube-dl. YCDL makes it easy for me to watch through a channel's catalog of videos, picking which ones I'd like to download while ignoring the others. Plus, as it creates an offline database, it will retain metadata about videos even after they are removed or deleted from the original YouTube channel.</p>
|
||||
<p><a href="https://github.com/voussoir/ycdl">https://github.com/voussoir/ycdl</a></p>
|
||||
<p><a href="https://git.voussoir.net/voussoir/ycdl">https://git.voussoir.net/voussoir/ycdl</a></p>
|
||||
</div>
|
||||
<img class="cvitem_logo" src="./cv/ycdl.png"/>
|
||||
</div>
|
||||
|
|
@ -456,7 +456,7 @@ body.start_eating_that_trashcan .cvitem_details
|
|||
<p>Etiquette is a tag-based file organization system with a web interface, built with Flask and SQLite3. Tag-based systems solve problems that a traditional folder hierarchy can't: <em>which folder should a file go in if it equally belongs in both?</em> and <em>how do I make my files searchable without littering the filenames themselves with keywords?</em></p>
|
||||
<p>Etiquette is unique because the tags themselves are hierarchical. By tagging one of your vacation photos with the <code>family.parents.dad</code> tag, it will automatically appear in searches for <code>family.parents</code> and <code>family</code> as well. A traditional folder system, here called albums, is available to bundle files that always belong together without creating a bespoke tag to represent that bundle. Regardless, the files on disk are never modified.</p>
|
||||
<p><a href="https://etiquette.voussoir.net">Live demo</a></p>
|
||||
<p><a href="https://github.com/voussoir/etiquette">https://github.com/voussoir/etiquette</a></p>
|
||||
<p><a href="https://git.voussoir.net/voussoir/etiquette">https://git.voussoir.net/voussoir/etiquette</a></p>
|
||||
</div>
|
||||
<img class="cvitem_logo" src="./cv/etiquette.png"/>
|
||||
</div>
|
||||
|
|
@ -466,7 +466,7 @@ body.start_eating_that_trashcan .cvitem_details
|
|||
<div class="cvitem_details">
|
||||
<p>Timesearch is a package of tools for archiving data from reddit.com. Subreddits, user posts, comments, CSS files, and community wiki files can be downloaded and easily updated.</p>
|
||||
<p>Originally, it used the <code>timestamp</code> query parameter of reddit's elasticsearch, but since that feature's removal Timesearch instead queries the third-party pushshift.io database for preliminary data, then queries reddit for updated information about each item.</p>
|
||||
<p><a href="https://github.com/voussoir/timesearch">https://github.com/voussoir/timesearch</a></p></div>
|
||||
<p><a href="https://git.voussoir.net/voussoir/timesearch">https://git.voussoir.net/voussoir/timesearch</a></p></div>
|
||||
<img class="cvitem_logo" src="./cv/timesearch.png"/>
|
||||
</div>
|
||||
|
||||
|
|
@ -474,7 +474,7 @@ body.start_eating_that_trashcan .cvitem_details
|
|||
<h3 class="cvitem_title" id="voussoirkit">voussoirkit</h3>
|
||||
<div class="cvitem_details">
|
||||
<p>The voussoirkit library contains code that I have found useful to include in my other projects. Everything from <code>bytestring</code> that converts integer numbers of bytes into "3.145 MiB" strings, to <code>pathclass</code> and <code>spinal</code> which provide object-oriented file and directory operations and copy routines. Some modules like <code>winglob</code> boost cross-compatibility by smoothing over differences between Windows and Unix. This way I can easily deploy new features and bug fixes to all my programs.</p>
|
||||
<p><a href="https://github.com/voussoir/voussoirkit">https://github.com/voussoir/voussoirkit</a></p>
|
||||
<p><a href="https://git.voussoir.net/voussoir/voussoirkit">https://git.voussoir.net/voussoir/voussoirkit</a></p>
|
||||
</div>
|
||||
<img class="cvitem_logo" src="./cv/voussoirkit.png"/>
|
||||
</div>
|
||||
|
|
@ -483,7 +483,7 @@ body.start_eating_that_trashcan .cvitem_details
|
|||
<h3 class="cvitem_title" id="hnarchive">HN Archive</h3>
|
||||
<div class="cvitem_details">
|
||||
<p>hnarchive is a tool that downloads all submissions and comments on <a href="https://news.ycombinator.com">Hacker News</a>. HN is a forum that is mostly focused on technology and entrepreneurship. Although I am not entirely sure if all participants are <a href="https://voussoir.net/writing/cyborgs_on_hn">human</a>, it is a knowledgebase of considerable quality and in my opinion worth preserving.</p>
|
||||
<p><a href="https://github.com/voussoir/hnarchive">https://github.com/voussoir/hnarchive</a></p>
|
||||
<p><a href="https://git.voussoir.net/voussoir/hnarchive">https://git.voussoir.net/voussoir/hnarchive</a></p>
|
||||
</div>
|
||||
<img class="cvitem_logo" src="./cv/hnarchive.png"/>
|
||||
</div>
|
||||
|
|
@ -493,8 +493,8 @@ body.start_eating_that_trashcan .cvitem_details
|
|||
<div class="cvitem_details">
|
||||
<p>I use the wonderful program Sigil to edit epub files. Sigil has a python plugin system for which I have written a few modules. But, since the plugins can only operate on one book at a time while it is open in Sigil, I needed something a little different to edit epub files en masse.</p>
|
||||
<p>Epubfile is a simple library for automatically processing epubs. It comes with a number of builtin routines for what I do most often: merging multiple epubs into a single file, normalizing the internal file structure, and renaming the cover image file to leverage CBXShell so I get thumbnails in Windows Explorer.</p>
|
||||
<p><a href="https://github.com/voussoir/epubfile">https://github.com/voussoir/epubfile</a></p>
|
||||
<p><a href="https://github.com/voussoir/sigilplugins">https://github.com/voussoir/sigilplugins</a></p>
|
||||
<p><a href="https://git.voussoir.net/voussoir/epubfile">https://git.voussoir.net/voussoir/epubfile</a></p>
|
||||
<p><a href="https://git.voussoir.net/voussoir/sigilplugins">https://git.voussoir.net/voussoir/sigilplugins</a></p>
|
||||
</div>
|
||||
<img class="cvitem_logo" src="./cv/epub.png"/>
|
||||
</div>
|
||||
|
|
@ -551,6 +551,7 @@ body.start_eating_that_trashcan .cvitem_details
|
|||
<h3 class="cvitem_title" id="gitmirrors">Git mirrors</h3>
|
||||
<div class="cvitem_details">
|
||||
<p>Eggs. Baskets.</p>
|
||||
<p><a href="https://git.voussoir.net">https://git.voussoir.net</a></p>
|
||||
<p><a href="https://github.com/voussoir">https://github.com/voussoir</a></p>
|
||||
<p><a href="https://codeberg.org/voussoir">https://codeberg.org/voussoir</a></p>
|
||||
<p><a href="https://gitlab.com/voussoir">https://gitlab.com/voussoir</a></p>
|
||||
|
|
@ -882,6 +883,7 @@ const SPLASHES = [
|
|||
"impulse 101", // https://developer.valvesoftware.com/wiki/Impulse
|
||||
"in this moment, i am euphoric", // aalewis https://knowyourmeme.com/memes/in-this-moment-i-am-euphoric
|
||||
"infinity is not necessarily all-encompassing",
|
||||
"information wants to be free", // https://en.wikipedia.org/wiki/Information_wants_to_be_free
|
||||
"inject the memes into my bloodstream", // Filthy Frank, Meme Machine https://youtu.be/wl-LeTFM8zo
|
||||
"insane in the membrane", // Cypress Hill, Insane in the Brain https://youtu.be/RijB8wnJCN0
|
||||
"is a little overbearing",
|
||||
|
|
@ -1153,6 +1155,7 @@ const SPLASHES = [
|
|||
"the rivers and the hills, the forests and the streams", // Walking in the Air https://youtu.be/X986dthrhaQ
|
||||
"the sheriff is near", // Blazing Saddles (1974) https://youtu.be/sAELs42aZt4
|
||||
"the squeaky wheel gets hammered down",
|
||||
"the sun's as warm as a baked potato", // Cannibal! The Musical https://www.cannibalthemusical.net/songs.shtml
|
||||
"the water is extracted for use in rivers", // How it's Unmade - Oreo Cookies https://youtu.be/cJyGoGPXTj4
|
||||
"the waves part and they engulf me and the water is warm", // You could stop at five or six stores https://youtu.be/YCeQLeQiRP4
|
||||
"the word itself makes some men uncomfortable", // The Big Lebowski (1998) https://youtu.be/xs3OWJ53rHE
|
||||
|
|
|
|||
|
|
@ -1,330 +0,0 @@
|
|||
:root
|
||||
{
|
||||
--color_bodybg: #272822;
|
||||
--color_codebg: rgba(255, 255, 255, 0.05);
|
||||
--color_codeborder: rgba(255, 255, 255, 0.2);
|
||||
--color_h1bg: #284142;
|
||||
--color_htmlbg: #1b1c18;
|
||||
--color_blockquotebg: rgba(0, 0, 0, 0.2);
|
||||
--color_blockquoteedge: rgba(255, 255, 255, 0.2);
|
||||
--color_inlinecodebg: rgba(255, 255, 255, 0.1);
|
||||
--color_link: #ae81ff;
|
||||
--color_maintext: #ddd;
|
||||
}
|
||||
|
||||
*, *:before, *:after
|
||||
{
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
html
|
||||
{
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
|
||||
background-color: var(--color_htmlbg);
|
||||
color: var(--color_maintext);
|
||||
|
||||
font-family: Verdana, sans-serif;
|
||||
font-size: 10pt;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body
|
||||
{
|
||||
min-height: 100%;
|
||||
width: fit-content;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding: 8px;
|
||||
}
|
||||
body.noscrollbar::-webkit-scrollbar
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
body.noscrollbar
|
||||
{
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
header
|
||||
{
|
||||
width: 100%;
|
||||
max-width: 120em;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: end;
|
||||
}
|
||||
header > *
|
||||
{
|
||||
display: inline-block;
|
||||
padding: 16px;
|
||||
background-color: var(--color_bodybg);
|
||||
}
|
||||
|
||||
.album,
|
||||
.photograph
|
||||
{
|
||||
position: relative;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 8vh;
|
||||
margin-bottom: 8vh;
|
||||
}
|
||||
|
||||
.photograph
|
||||
{
|
||||
padding: 2vh;
|
||||
background-color: var(--color_bodybg);
|
||||
border-radius: 16px;
|
||||
}
|
||||
article .photograph:first-of-type
|
||||
{
|
||||
margin-top: 0;
|
||||
}
|
||||
article .photograph:last-of-type
|
||||
{
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.photograph img
|
||||
{
|
||||
max-height: 92vh;
|
||||
border-radius: 8px;
|
||||
}
|
||||
article .morelink
|
||||
{
|
||||
font-size: 2em;
|
||||
text-align: center;
|
||||
margin-top: 0;
|
||||
}
|
||||
@media not print
|
||||
{
|
||||
.photograph
|
||||
{
|
||||
box-shadow: #000 0px 0px 40px -10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 600px)
|
||||
{
|
||||
article
|
||||
{
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px)
|
||||
{
|
||||
.photograph
|
||||
{
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media not all and (pointer: fine)
|
||||
{
|
||||
#keyboardhint,
|
||||
#scrollbartoggle
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5
|
||||
{
|
||||
margin-bottom: 0;
|
||||
padding: 8px;
|
||||
}
|
||||
h1:first-child, h2:first-child, h3:first-child, h4:first-child, h5:first-child
|
||||
{
|
||||
margin-top: 0;
|
||||
}
|
||||
h2, h3, h4, h5
|
||||
{
|
||||
border-bottom: 1px solid var(--color_maintext);
|
||||
/*background-color: var(--color_h1bg);*/
|
||||
}
|
||||
h1 + p
|
||||
{
|
||||
margin-top: 0;
|
||||
}
|
||||
p:last-child
|
||||
{
|
||||
margin-bottom: 0;
|
||||
}
|
||||
h1 {font-size: 2.00em;} h1 * {font-size: inherit;}
|
||||
h2 {font-size: 1.75em;} h2 * {font-size: inherit;}
|
||||
h3 {font-size: 1.50em;} h3 * {font-size: inherit;}
|
||||
h4 {font-size: 1.25em;} h4 * {font-size: inherit;}
|
||||
h5 {font-size: 1.00em;} h5 * {font-size: inherit;}
|
||||
|
||||
.header_anchor_link {display: none; font-size: 1.0em; text-decoration: none}
|
||||
h1:hover > .header_anchor_link {display: initial;}
|
||||
h2:hover > .header_anchor_link {display: initial;}
|
||||
h3:hover > .header_anchor_link {display: initial;}
|
||||
h4:hover > .header_anchor_link {display: initial;}
|
||||
h5:hover > .header_anchor_link {display: initial;}
|
||||
|
||||
a
|
||||
{
|
||||
color: var(--color_link);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
article *
|
||||
{
|
||||
max-width: 100%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#table_of_contents
|
||||
{
|
||||
border: 1px solid var(--color_blockquoteedge);
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
blockquote
|
||||
{
|
||||
background-color: var(--color_blockquotebg);
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: 0;
|
||||
border-left: 4px solid var(--color_blockquoteedge);
|
||||
|
||||
padding: 8px;
|
||||
padding-inline-start: 20px;
|
||||
padding-inline-end: 20px;
|
||||
}
|
||||
|
||||
table
|
||||
{
|
||||
border-collapse: collapse;
|
||||
font-size: 1em;
|
||||
}
|
||||
table, table th, table td
|
||||
{
|
||||
border: 1px solid var(--color_maintext);
|
||||
}
|
||||
table th, table td
|
||||
{
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
ol ol, ul ul, ol ul, ul ol
|
||||
{
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
|
||||
*:not(pre) > code
|
||||
{
|
||||
background-color: var(--color_inlinecodebg);
|
||||
border-radius: 4px;
|
||||
line-height: 1.5;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
pre
|
||||
{
|
||||
padding: 8px;
|
||||
border: 1px solid var(--color_codeborder);
|
||||
background-color: var(--color_codebg);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
code,
|
||||
pre,
|
||||
.highlight *
|
||||
{
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/*
|
||||
Thank you richleland for pre-building this Monokai style.
|
||||
https://github.com/richleland/pygments-css
|
||||
*/
|
||||
:root
|
||||
{
|
||||
--color_monokai_bg: #272822;
|
||||
--color_monokai_purple: #ae81ff;
|
||||
--color_monokai_green: #a6e22e;
|
||||
--color_monokai_pink: #f92672;
|
||||
--color_monokai_white: #f8f8f2;
|
||||
--color_monokai_orange: #fd971f;
|
||||
--color_monokai_yellow: #e6db74;
|
||||
--color_monokai_blue: #66d9ef;
|
||||
}
|
||||
.highlight .hll { background-color: #49483e }
|
||||
.highlight { background-color: var(--color_monokai_bg); color: var(--color_monokai_white) }
|
||||
.highlight .c { color: #75715e } /* Comment */
|
||||
.highlight .err { color: #960050; background-color: #1e0010 } /* Error */
|
||||
.highlight .k { color: var(--color_monokai_pink) } /* Keyword */
|
||||
.highlight .l { color: var(--color_monokai_purple) } /* Literal */
|
||||
.highlight .n { color: var(--color_monokai_white) } /* Name */
|
||||
.highlight .o { color: var(--color_monokai_pink) } /* Operator */
|
||||
.highlight .p { color: var(--color_monokai_white) } /* Punctuation */
|
||||
.highlight .ch { color: #75715e } /* Comment.Hashbang */
|
||||
.highlight .cm { color: #75715e } /* Comment.Multiline */
|
||||
.highlight .cp { color: #75715e } /* Comment.Preproc */
|
||||
.highlight .cpf { color: #75715e } /* Comment.PreprocFile */
|
||||
.highlight .c1 { color: #75715e } /* Comment.Single */
|
||||
.highlight .cs { color: #75715e } /* Comment.Special */
|
||||
.highlight .gd { color: var(--color_monokai_pink) } /* Generic.Deleted */
|
||||
.highlight .ge { font-style: italic } /* Generic.Emph */
|
||||
.highlight .gi { color: var(--color_monokai_green) } /* Generic.Inserted */
|
||||
.highlight .gs { font-weight: bold } /* Generic.Strong */
|
||||
.highlight .gu { color: #75715e } /* Generic.Subheading */
|
||||
.highlight .kc { color: var(--color_monokai_purple) } /* Keyword.Constant */
|
||||
.highlight .kd { color: var(--color_monokai_blue) } /* Keyword.Declaration */
|
||||
.highlight .kn { color: var(--color_monokai_pink) } /* Keyword.Namespace */
|
||||
.highlight .kp { color: var(--color_monokai_blue) } /* Keyword.Pseudo */
|
||||
.highlight .kr { color: var(--color_monokai_blue) } /* Keyword.Reserved */
|
||||
.highlight .kt { color: var(--color_monokai_blue) } /* Keyword.Type */
|
||||
.highlight .ld { color: var(--color_monokai_yellow) } /* Literal.Date */
|
||||
.highlight .m { color: var(--color_monokai_purple) } /* Literal.Number */
|
||||
.highlight .s { color: var(--color_monokai_yellow) } /* Literal.String */
|
||||
.highlight .na { color: var(--color_monokai_white) } /* Name.Attribute */
|
||||
.highlight .narg {color: var(--color_monokai_orange) } /* Custom Name.Argument */
|
||||
.highlight .nb { color: var(--color_monokai_blue) } /* Name.Builtin */
|
||||
.highlight .nc { color: var(--color_monokai_white) } /* Name.Class */
|
||||
.highlight .no { color: var(--color_monokai_blue) } /* Name.Constant */
|
||||
.highlight .nd { color: var(--color_monokai_green) } /* Name.Decorator */
|
||||
.highlight .ni { color: var(--color_monokai_white) } /* Name.Entity */
|
||||
.highlight .ne { color: var(--color_monokai_blue) } /* Name.Exception */
|
||||
.highlight .nf { color: var(--color_monokai_green) } /* Name.Function */
|
||||
.highlight .nl { color: var(--color_monokai_white) } /* Name.Label */
|
||||
.highlight .nn { color: var(--color_monokai_white) } /* Name.Namespace */
|
||||
.highlight .nx { color: var(--color_monokai_green) } /* Name.Other */
|
||||
.highlight .py { color: var(--color_monokai_white) } /* Name.Property */
|
||||
.highlight .nt { color: var(--color_monokai_pink) } /* Name.Tag */
|
||||
.highlight .nv { color: var(--color_monokai_white) } /* Name.Variable */
|
||||
.highlight .ow { color: var(--color_monokai_pink) } /* Operator.Word */
|
||||
.highlight .w { color: var(--color_monokai_white) } /* Text.Whitespace */
|
||||
.highlight .mb { color: var(--color_monokai_purple) } /* Literal.Number.Bin */
|
||||
.highlight .mf { color: var(--color_monokai_purple) } /* Literal.Number.Float */
|
||||
.highlight .mh { color: var(--color_monokai_purple) } /* Literal.Number.Hex */
|
||||
.highlight .mi { color: var(--color_monokai_purple) } /* Literal.Number.Integer */
|
||||
.highlight .mo { color: var(--color_monokai_purple) } /* Literal.Number.Oct */
|
||||
.highlight .sa { color: var(--color_monokai_white) } /* Literal.String.Affix */
|
||||
.highlight .sb { color: var(--color_monokai_yellow) } /* Literal.String.Backtick */
|
||||
.highlight .sc { color: var(--color_monokai_yellow) } /* Literal.String.Char */
|
||||
.highlight .dl { color: var(--color_monokai_yellow) } /* Literal.String.Delimiter */
|
||||
.highlight .sd { color: var(--color_monokai_yellow) } /* Literal.String.Doc */
|
||||
.highlight .s2 { color: var(--color_monokai_yellow) } /* Literal.String.Double */
|
||||
.highlight .se { color: var(--color_monokai_purple) } /* Literal.String.Escape */
|
||||
.highlight .sh { color: var(--color_monokai_yellow) } /* Literal.String.Heredoc */
|
||||
.highlight .si { color: var(--color_monokai_yellow) } /* Literal.String.Interpol */
|
||||
.highlight .sx { color: var(--color_monokai_yellow) } /* Literal.String.Other */
|
||||
.highlight .sr { color: var(--color_monokai_yellow) } /* Literal.String.Regex */
|
||||
.highlight .s1 { color: var(--color_monokai_yellow) } /* Literal.String.Single */
|
||||
.highlight .ss { color: var(--color_monokai_yellow) } /* Literal.String.Symbol */
|
||||
.highlight .bp { color: var(--color_monokai_white) } /* Name.Builtin.Pseudo */
|
||||
.highlight .fm { color: var(--color_monokai_blue) } /* Name.Function.Magic */
|
||||
.highlight .vc { color: var(--color_monokai_white) } /* Name.Variable.Class */
|
||||
.highlight .vg { color: var(--color_monokai_white) } /* Name.Variable.Global */
|
||||
.highlight .vi { color: var(--color_monokai_white) } /* Name.Variable.Instance */
|
||||
.highlight .vm { color: var(--color_monokai_white) } /* Name.Variable.Magic */
|
||||
.highlight .il { color: var(--color_monokai_purple) } /* Literal.Number.Integer.Long */
|
||||
|
|
@ -1,18 +1,43 @@
|
|||
import boto3
|
||||
import io
|
||||
import jinja2
|
||||
import PIL.Image
|
||||
import sys
|
||||
import urllib.parse
|
||||
|
||||
import etiquette
|
||||
import r2_credentials
|
||||
|
||||
from voussoirkit import dotdict
|
||||
from voussoirkit import imagetools
|
||||
from voussoirkit import pathclass
|
||||
from voussoirkit import spinal
|
||||
from voussoirkit import vlogging
|
||||
|
||||
log = vlogging.get_logger(__name__, 'photography_generate')
|
||||
|
||||
pdb = etiquette.photodb.PhotoDB('D:\\Documents\\Photos\\_etiquette')
|
||||
|
||||
s3 = boto3.resource('s3',
|
||||
endpoint_url = f'https://{r2_credentials.get_account_id()}.r2.cloudflarestorage.com',
|
||||
aws_access_key_id = r2_credentials.get_access_key(),
|
||||
aws_secret_access_key = r2_credentials.get_access_secret(),
|
||||
)
|
||||
|
||||
bucket = s3.Bucket('voussoir')
|
||||
|
||||
S3_EXISTING_FILES = set(item.key for item in bucket.objects.filter(Prefix="photography/"))
|
||||
PUBLISH_TAGNAME = 'voussoir_net_publish'
|
||||
HEADLINER_TAGNAME = 'voussoir_net_headliner'
|
||||
PHOTOGRAPHY_ROOTDIR = pathclass.Path(__file__).parent
|
||||
ATOM_FILE = PHOTOGRAPHY_ROOTDIR.with_child('photography.atom')
|
||||
DOMAIN_ROOTDIR = PHOTOGRAPHY_ROOTDIR.parent
|
||||
CSS_CONTENT = PHOTOGRAPHY_ROOTDIR.with_child('dark.css').read('r', encoding='utf-8')
|
||||
DOMAIN_WEBROOT = ('file:///' + DOMAIN_ROOTDIR.absolute_path) if '--test' in sys.argv else 'https://voussoir.net'
|
||||
DOMAIN_WEBROOT = DOMAIN_WEBROOT.replace('\\', '/')
|
||||
DOMAIN_WEBROOT = 'https://voussoir.net'
|
||||
PHOTOGRAPHY_WEBROOT = 'https://voussoir.net/photography'
|
||||
S3_WEBROOT = 'https://files.voussoir.net'
|
||||
|
||||
SIZE_SMALL = 1440
|
||||
SIZE_TINY = 360
|
||||
|
||||
def webpath(path, anchor=None):
|
||||
path = path.relative_to(DOMAIN_ROOTDIR, simple=True).replace('\\', '/').lstrip('/')
|
||||
|
|
@ -22,92 +47,129 @@ def webpath(path, anchor=None):
|
|||
return path
|
||||
|
||||
class Photo:
|
||||
def __init__(self, filepath):
|
||||
self.filepath = filepath
|
||||
self.thumbnail = make_thumbnail(filepath)
|
||||
self.article_id = filepath.replace_extension('').basename
|
||||
self.anchor = f'#{self.article_id}'
|
||||
self.published = imagetools.get_exif_datetime(filepath)
|
||||
def __init__(self, etq_photo, etq_album=None):
|
||||
self.etq_photo = etq_photo
|
||||
self.article_id = self.etq_photo.real_path.replace_extension('').basename
|
||||
|
||||
if etq_album is None:
|
||||
parent_key = 'photography'
|
||||
else:
|
||||
parent_key = f'photography/{etq_album.title}'
|
||||
|
||||
self.s3_key = f'{parent_key}/{self.etq_photo.real_path.basename}'
|
||||
self.small_key = f'{parent_key}/small_{self.etq_photo.real_path.basename}'
|
||||
self.tiny_key = f'{parent_key}/tiny_{self.etq_photo.real_path.basename}'
|
||||
|
||||
self.color_class = 'monochrome' if self.etq_photo.has_tag('monochrome') else ''
|
||||
|
||||
self.s3_exists = self.s3_key in S3_EXISTING_FILES;
|
||||
self.img_url = f'{S3_WEBROOT}/{self.s3_key}'
|
||||
self.small_url = f'{S3_WEBROOT}/{self.small_key}'
|
||||
self.tiny_url = f'{S3_WEBROOT}/{self.tiny_key}'
|
||||
self.anchor_url = f'{DOMAIN_WEBROOT}/{parent_key}#{self.article_id}'
|
||||
self.published = imagetools.get_exif_datetime(self.etq_photo.real_path)
|
||||
|
||||
def prepare(self):
|
||||
if not self.s3_exists:
|
||||
self.s3_upload()
|
||||
|
||||
def make_thumbnail(self, size):
|
||||
image = PIL.Image.open(self.etq_photo.real_path.absolute_path)
|
||||
icc = image.info.get('icc_profile')
|
||||
(image_width, image_height) = image.size
|
||||
exif = image.getexif()
|
||||
(width, height) = imagetools.fit_into_bounds(image_width, image_height, size, size)
|
||||
image = image.resize((width, height), PIL.Image.LANCZOS)
|
||||
bio = io.BytesIO()
|
||||
image.save(bio, format='jpeg', quality=75, exif=exif, icc_profile=icc)
|
||||
bio.seek(0)
|
||||
return bio
|
||||
|
||||
def s3_upload(self):
|
||||
log.info('Uploading %s as %s', self.etq_photo.real_path.absolute_path, self.s3_key)
|
||||
bucket.upload_fileobj(self.make_thumbnail(SIZE_SMALL), self.small_key)
|
||||
bucket.upload_fileobj(self.make_thumbnail(SIZE_TINY), self.tiny_key)
|
||||
bucket.upload_fileobj(self.etq_photo.real_path.open('rb'), self.s3_key)
|
||||
self.s3_exists = True
|
||||
|
||||
def render_web(self, index=None, totalcount=None):
|
||||
if totalcount is not None:
|
||||
number_tag = f'<span class="number_tag">#{index}/{totalcount}</a>'
|
||||
else:
|
||||
number_tag = ''
|
||||
|
||||
def render_web(self, relative_directory=None):
|
||||
return f'''
|
||||
<article id="{self.article_id}" class="photograph">
|
||||
<a href="{webpath(self.filepath)}" target="_blank"><img src="{webpath(self.thumbnail)}" loading="lazy"/></a>
|
||||
<article id="{self.article_id}" class="photograph {self.color_class}">
|
||||
<a href="{self.img_url}" target="_blank"><img src="{self.small_url}" loading="lazy"/></a>
|
||||
{number_tag}
|
||||
</article>
|
||||
'''
|
||||
|
||||
def render_atom(self):
|
||||
href = webpath(PHOTOGRAPHY_ROOTDIR, anchor=self.anchor)
|
||||
imgsrc = webpath(self.thumbnail)
|
||||
return f'''
|
||||
<id>{self.article_id}</id>
|
||||
<title>{self.article_id}</title>
|
||||
<link rel="alternate" type="text/html" href="{href}"/>
|
||||
<link rel="alternate" type="text/html" href="{self.anchor_url}"/>
|
||||
<updated>{self.published.isoformat()}</updated>
|
||||
<content type="html">
|
||||
<![CDATA[
|
||||
<a href="{href}"><img src="{imgsrc}"/></a>
|
||||
<a href="{self.img_url}"><img src="{self.small_url}"/></a>
|
||||
]]>
|
||||
</content>
|
||||
'''
|
||||
|
||||
class Album:
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
self.article_id = path.basename
|
||||
self.link = webpath(path)
|
||||
self.published = imagetools.get_exif_datetime(sorted(path.glob_files('*.jpg'))[0])
|
||||
self.photos = list(spinal.walk(
|
||||
self.path,
|
||||
glob_filenames={'*.jpg'},
|
||||
exclude_filenames={'*_small*'},
|
||||
recurse=False,
|
||||
yield_directories=False,
|
||||
))
|
||||
self.photos.sort(key=lambda file: file.basename)
|
||||
self.photos = [Photo(file) for file in self.photos]
|
||||
def __init__(self, etq_album):
|
||||
self.etq_album = etq_album
|
||||
self.article_id = self.etq_album.title
|
||||
self.photos = list(self.etq_album.get_photos())
|
||||
self.photos = [p for p in self.photos if p.has_tag(PUBLISH_TAGNAME)]
|
||||
self.photos = [Photo(etq_photo=photo, etq_album=self.etq_album) for photo in self.photos]
|
||||
self.photos.sort(key=lambda p: p.published)
|
||||
# self.link = webpath(path)
|
||||
self.web_url = f'{PHOTOGRAPHY_WEBROOT}/{self.article_id}'
|
||||
self.published = self.photos[0].published
|
||||
|
||||
def render_web(self):
|
||||
firsts = self.photos[:5]
|
||||
remaining = self.photos[5:]
|
||||
if remaining:
|
||||
next_after_more = remaining[0]
|
||||
else:
|
||||
next_after_more = None
|
||||
def prepare(self):
|
||||
for photo in self.photos:
|
||||
photo.prepare()
|
||||
|
||||
def render_web(self, index=None, totalcount=None):
|
||||
headliners = [p for p in self.photos if p.etq_photo.has_tag(HEADLINER_TAGNAME)]
|
||||
|
||||
return jinja2.Template('''
|
||||
<article id="{{article_id}}" class="album">
|
||||
<h1><a href="{{album_path}}">{{directory.basename}}</a></h1>
|
||||
{% for photo in firsts %}
|
||||
<h1><a href="{{web_url}}">{{article_id}}</a></h1>
|
||||
<div class="albumphotos">
|
||||
{% for photo in headliners %}
|
||||
{{photo.render_web()}}
|
||||
{% endfor %}
|
||||
|
||||
{% if remaining > 0 %}
|
||||
<p class="morelink"><a href="{{album_path}}{{next_after_more.anchor}}">{{remaining}} more</a></p>
|
||||
{% endif %}
|
||||
<div class="album_tinies">
|
||||
{% for photo in photos %}
|
||||
<a class="tiny_thumbnail {{photo.color_class}}" href="{{photo.anchor_url}}"><img src="{{photo.tiny_url}}" loading="lazy"/></a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
''').render(
|
||||
article_id=self.article_id,
|
||||
directory=self.path,
|
||||
album_path=webpath(self.path),
|
||||
next_after_more=next_after_more,
|
||||
firsts=firsts,
|
||||
remaining=len(remaining),
|
||||
web_url=self.web_url,
|
||||
photos=self.photos,
|
||||
headliners=headliners,
|
||||
)
|
||||
|
||||
def render_atom(self):
|
||||
photos = []
|
||||
for photo in self.photos:
|
||||
href = webpath(photo.filepath)
|
||||
imgsrc = webpath(photo.thumbnail)
|
||||
line = f'<article><a href="{href}"><img src="{imgsrc}"/></a>'.replace('\\', '/')
|
||||
line = f'<article><a href="{photo.anchor_url}"><img src="{photo.small_url}" loading="lazy"/></a>'.replace('\\', '/')
|
||||
photos.append(line)
|
||||
photos = '\n'.join(photos)
|
||||
|
||||
return f'''
|
||||
<id>{self.article_id}</id>
|
||||
<title>{self.article_id}</title>
|
||||
<link rel="alternate" type="text/html" href="{self.link}"/>
|
||||
<link rel="alternate" type="text/html" href="{self.web_url}"/>
|
||||
<updated>{self.published.isoformat()}</updated>
|
||||
<content type="html">
|
||||
<![CDATA[
|
||||
|
|
@ -126,30 +188,12 @@ def write(path, content):
|
|||
print(path.absolute_path)
|
||||
path.write('w', content, encoding='utf-8')
|
||||
|
||||
def write_directory_index(directory):
|
||||
rss_link = webpath(ATOM_FILE) if directory == PHOTOGRAPHY_ROOTDIR else None
|
||||
back_link = webpath(PHOTOGRAPHY_ROOTDIR) if directory != PHOTOGRAPHY_ROOTDIR else None
|
||||
sort_reverse = directory == PHOTOGRAPHY_ROOTDIR
|
||||
def make_webpage(items, is_root, doctitle):
|
||||
rss_link = f'{PHOTOGRAPHY_WEBROOT}/{ATOM_FILE.basename}' if is_root else None
|
||||
back_link = None if is_root else PHOTOGRAPHY_WEBROOT
|
||||
sort_reverse = is_root
|
||||
|
||||
items = list(spinal.walk(
|
||||
directory,
|
||||
glob_filenames={'*.jpg'},
|
||||
exclude_filenames={'*_small*'},
|
||||
recurse=False,
|
||||
yield_directories=False,
|
||||
)) + list(directory.listdir_directories())
|
||||
|
||||
items2 = []
|
||||
for item in items:
|
||||
if item.is_file:
|
||||
items2.append(Photo(item))
|
||||
else:
|
||||
items2.append(Album(item))
|
||||
|
||||
items = items2
|
||||
items.sort(key=lambda item: item.published, reverse=sort_reverse)
|
||||
|
||||
page = jinja2.Template('''
|
||||
html = jinja2.Template('''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
|
@ -158,10 +202,229 @@ def write_directory_index(directory):
|
|||
{% if rss_link %}
|
||||
<link rel="alternate" type="application/atom+xml" href="{{rss_link}}"/>
|
||||
{% endif %}
|
||||
<title>{{directory.basename}}</title>
|
||||
<title>{{doctitle}}</title>
|
||||
|
||||
<style>
|
||||
{{css_content}}
|
||||
:root
|
||||
{
|
||||
--color_bodybg: #272822;
|
||||
--color_htmlbg: #1b1c18;
|
||||
--color_link: #ae81ff;
|
||||
--color_maintext: #ddd;
|
||||
|
||||
--img_borderradius: 16px;
|
||||
--img_borderradius_tiny: 4px;
|
||||
--img_sepia: 0%;
|
||||
--img_huerotate: 0deg;
|
||||
--img_saturate: 100%;
|
||||
--img_blur: 0px;
|
||||
--img_mixblendmode: normal;
|
||||
}
|
||||
|
||||
*, *:before, *:after
|
||||
{
|
||||
box-sizing: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hidden
|
||||
{
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
html
|
||||
{
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
|
||||
background-color: var(--color_htmlbg);
|
||||
color: var(--color_maintext);
|
||||
|
||||
font-family: Verdana, sans-serif;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
body
|
||||
{
|
||||
width: 100em;
|
||||
max-width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding: 8px;
|
||||
padding-bottom:8vh;
|
||||
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
grid-row-gap: 12vh;
|
||||
}
|
||||
body.noscrollbar::-webkit-scrollbar
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
body.noscrollbar
|
||||
{
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
a
|
||||
{
|
||||
color: var(--color_link);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h1
|
||||
{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
article *
|
||||
{
|
||||
max-width: 100%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
header
|
||||
{
|
||||
width: 100%;
|
||||
max-width: 120em;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: end;
|
||||
}
|
||||
header > *
|
||||
{
|
||||
display: inline-block;
|
||||
padding: 16px;
|
||||
background-color: var(--color_bodybg);
|
||||
}
|
||||
|
||||
.album,
|
||||
.photograph,
|
||||
.album_tinies
|
||||
{
|
||||
position: relative;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.album
|
||||
{
|
||||
width: 100%;
|
||||
}
|
||||
.album .albumphotos
|
||||
{
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
grid-row-gap: 12vh;
|
||||
}
|
||||
.photograph
|
||||
{
|
||||
width: fit-content;
|
||||
}
|
||||
.photograph img
|
||||
{
|
||||
display: block;
|
||||
max-height: 92vh;
|
||||
border-radius: var(--img_borderradius);
|
||||
border: 1.25vh solid var(--color_bodybg);
|
||||
filter: hue-rotate(var(--img_huerotate)) saturate(var(--img_saturate)) blur(var(--img_blur));
|
||||
mix-blend-mode: var(--img_mixblendmode);
|
||||
}
|
||||
.photograph.monochrome img,
|
||||
.tiny_thumbnail.monochrome img
|
||||
{
|
||||
filter: sepia(var(--img_sepia)) hue-rotate(var(--img_huerotate)) saturate(var(--img_saturate)) blur(var(--img_blur));
|
||||
}
|
||||
.photograph .number_tag
|
||||
{
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
text-align: center;
|
||||
background-color: white;
|
||||
padding: 1px;
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
font-family: sans-serif;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
opacity: 50%;
|
||||
}
|
||||
.album .album_tinies
|
||||
{
|
||||
max-width: 80em;
|
||||
text-align: justify;
|
||||
}
|
||||
.tiny_thumbnail
|
||||
{
|
||||
vertical-align: middle;
|
||||
display:inline-block;
|
||||
margin: 8px;
|
||||
}
|
||||
.tiny_thumbnail img
|
||||
{
|
||||
aspect-ratio: auto;
|
||||
border-radius: var(--img_borderradius_tiny);
|
||||
outline: 4px solid var(--color_bodybg);
|
||||
filter: hue-rotate(var(--img_huerotate)) saturate(var(--img_saturate)) blur(var(--img_blur));
|
||||
mix-blend-mode: var(--img_mixblendmode);
|
||||
}
|
||||
|
||||
@media not print
|
||||
{
|
||||
.photograph img
|
||||
{
|
||||
box-shadow: #000 0px 0px 40px -10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 600px)
|
||||
{
|
||||
.tiny_thumbnail img
|
||||
{
|
||||
height: 128px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px)
|
||||
{
|
||||
.tiny_thumbnail img
|
||||
{
|
||||
height: 64px;
|
||||
}
|
||||
.photograph
|
||||
{
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media not all and (pointer: fine)
|
||||
{
|
||||
#keyboardhint,
|
||||
#scrollbartoggle
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {font-size: 2.00em;} h1 * {font-size: inherit;}
|
||||
h2 {font-size: 1.75em;} h2 * {font-size: inherit;}
|
||||
h3 {font-size: 1.50em;} h3 * {font-size: inherit;}
|
||||
h4 {font-size: 1.25em;} h4 * {font-size: inherit;}
|
||||
h5 {font-size: 1.00em;} h5 * {font-size: inherit;}
|
||||
|
||||
#a_new_perspective
|
||||
{
|
||||
background-color: var(--color_bodybg);
|
||||
position: fixed;
|
||||
left: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
|
@ -178,9 +441,47 @@ def write_directory_index(directory):
|
|||
{%- endif -%}
|
||||
</header>
|
||||
|
||||
{% if not is_root %}
|
||||
<h1>{{doctitle}}</h1>
|
||||
{% endif %}
|
||||
|
||||
{% for item in items %}
|
||||
{{item.render_web()}}
|
||||
{{item.render_web(index=loop.index, totalcount=none if is_root else (items|length))}}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
<footer>
|
||||
<p>Ethan Dalool</p>
|
||||
<p>Contact me: photography@voussoir.net</p>
|
||||
</footer>
|
||||
|
||||
<p><button id="new_perspective_button" onclick="return new_perspective_button_onclick(event);">👁️ Try a different perspective</button></p>
|
||||
|
||||
<form id="a_new_perspective" class="hidden">
|
||||
<div><label>Background color: <input type="color" value="#1b1c18" oninput="return backgroundcolor_onchange(event);"/></label></div>
|
||||
<div><label>Border radius: <input type="range" min="0" value="16" max="500" oninput="return border_radius_onchange(event);"/></label> <span id="border_radius_value">16px</span></div>
|
||||
<div><label>Saturation: <input type="range" min="0" value="100" max="500" oninput="return saturate_onchange(event);"/></label> <span id="saturate_value">100%</span></div>
|
||||
<div><label>Hue rotate: <input type="range" min="0" value="0" max="360" oninput="return hue_rotate_onchange(event);"/></label> <span id="hue_rotate_value">0deg</span></div>
|
||||
<div><label>Blur: <input type="range" min="0" value="0" max="50" oninput="return blur_onchange(event);"/></label> <span id="blur_value">0%</span></div>
|
||||
<div><label>Mix blend mode: <select onchange="return mixblendmode_onchange(event);">
|
||||
<option selected>normal</option>
|
||||
<option>multiply</option>
|
||||
<option>screen</option>
|
||||
<option>overlay</option>
|
||||
<option>darken</option>
|
||||
<option>lighten</option>
|
||||
<option>color-dodge</option>
|
||||
<option>color-burn</option>
|
||||
<option>hard-light</option>
|
||||
<option>soft-light</option>
|
||||
<option>difference</option>
|
||||
<option>exclusion</option>
|
||||
<option>hue</option>
|
||||
<option>saturation</option>
|
||||
<option>color</option>
|
||||
<option>luminosity</option>
|
||||
</select></label></div>
|
||||
</form>
|
||||
</body>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
|
@ -212,70 +513,93 @@ def write_directory_index(directory):
|
|||
}
|
||||
}
|
||||
|
||||
function get_center_img()
|
||||
const SCROLL_STOPS = Array.from(document.querySelectorAll("article.photograph img, .album_tinies"));
|
||||
function get_center_stop()
|
||||
{
|
||||
let center_x = window.innerWidth / 2;
|
||||
let center_y = window.innerHeight / 2;
|
||||
while (true)
|
||||
let center_y = window.pageYOffset + (window.innerHeight / 2);
|
||||
let final;
|
||||
for (const stop of SCROLL_STOPS)
|
||||
{
|
||||
const element = document.elementFromPoint(center_x, center_y);
|
||||
if (element.tagName === "IMG")
|
||||
const stop_top = stop.getBoundingClientRect().top + window.pageYOffset;
|
||||
const stop_bottom = stop_top + stop.offsetHeight;
|
||||
console.log("-----");
|
||||
console.log(stop);
|
||||
console.log(`${center_y} versus ${stop_top}--${stop_bottom}`);
|
||||
console.log("-----");
|
||||
if (center_y > stop_top)
|
||||
{
|
||||
return element;
|
||||
final = stop;
|
||||
}
|
||||
center_y -= 20;
|
||||
if (center_y <= 0)
|
||||
else
|
||||
{
|
||||
return null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (final)
|
||||
{
|
||||
return final;
|
||||
}
|
||||
}
|
||||
function next_img(img)
|
||||
{
|
||||
const images = Array.from(document.images);
|
||||
const this_index = images.indexOf(img);
|
||||
if (this_index === images.length-1)
|
||||
const this_index = SCROLL_STOPS.indexOf(img);
|
||||
if (this_index === SCROLL_STOPS.length-1)
|
||||
{
|
||||
return img;
|
||||
}
|
||||
return images[this_index + 1];
|
||||
return SCROLL_STOPS[this_index + 1];
|
||||
}
|
||||
function previous_img(img)
|
||||
{
|
||||
const images = Array.from(document.images);
|
||||
const this_index = images.indexOf(img);
|
||||
const this_index = SCROLL_STOPS.indexOf(img);
|
||||
if (this_index === 0)
|
||||
{
|
||||
return img;
|
||||
}
|
||||
return images[this_index - 1];
|
||||
return SCROLL_STOPS[this_index - 1];
|
||||
}
|
||||
function scroll_step()
|
||||
{
|
||||
const distance = desired_scroll_position - document.body.scrollTop;
|
||||
const jump = (distance * 0.25) + (document.body.scrollTop < desired_scroll_position ? 1 : -1);
|
||||
document.body.scrollTop = document.body.scrollTop + jump;
|
||||
console.log(`${document.body.scrollTop} ${desired_scroll_position}`);
|
||||
const new_distance = desired_scroll_position - document.body.scrollTop;
|
||||
const distance = desired_scroll_position - document.documentElement.scrollTop;
|
||||
const jump = (distance * 0.25) + (document.documentElement.scrollTop < desired_scroll_position ? 1 : -1);
|
||||
document.documentElement.scrollTop = document.documentElement.scrollTop + jump;
|
||||
//console.log(`${document.documentElement.scrollTop} ${desired_scroll_position}`);
|
||||
const new_distance = desired_scroll_position - document.documentElement.scrollTop;
|
||||
if (Math.abs(new_distance / distance) < 0.97)
|
||||
{
|
||||
window.requestAnimationFrame(scroll_step);
|
||||
}
|
||||
}
|
||||
function scroll_to_img(img)
|
||||
function scroll_to_stop(stop)
|
||||
{
|
||||
const img_centerline = img.getBoundingClientRect().top + img.ownerDocument.defaultView.pageYOffset + (img.offsetHeight / 2);
|
||||
// document.body.scrollTop = img_centerline - (window.innerHeight / 2);
|
||||
if (stop.offsetHeight > window.innerHeight)
|
||||
{
|
||||
desired_scroll_position = stop.getBoundingClientRect().top + window.pageYOffset;
|
||||
}
|
||||
else
|
||||
{
|
||||
const img_centerline = stop.getBoundingClientRect().top + window.pageYOffset + (stop.offsetHeight / 2);
|
||||
// document.documentElement.scrollTop = img_centerline - (window.innerHeight / 2);
|
||||
desired_scroll_position = Math.round(img_centerline - (window.innerHeight / 2));
|
||||
}
|
||||
scroll_step();
|
||||
}
|
||||
function scroll_to_next_img()
|
||||
{
|
||||
scroll_to_img(next_img(get_center_img()));
|
||||
const current_stop = get_center_stop();
|
||||
if (current_stop)
|
||||
{
|
||||
scroll_to_stop(next_img(current_stop));
|
||||
}
|
||||
}
|
||||
function scroll_to_previous_img()
|
||||
{
|
||||
scroll_to_img(previous_img(get_center_img()));
|
||||
const current_stop = get_center_stop();
|
||||
if (current_stop)
|
||||
{
|
||||
scroll_to_stop(previous_img(current_stop));
|
||||
}
|
||||
}
|
||||
function arrowkey_listener(event)
|
||||
{
|
||||
|
|
@ -304,6 +628,78 @@ def write_directory_index(directory):
|
|||
clearTimeout(hide_cursor_timeout);
|
||||
hide_cursor_timeout = setTimeout(hide_cursor, 3000);
|
||||
}
|
||||
|
||||
function new_perspective_button_onclick(event)
|
||||
{
|
||||
const p = event.target.parentElement;
|
||||
document.getElementById("a_new_perspective").classList.remove("hidden");
|
||||
p.parentElement.removeChild(p);
|
||||
}
|
||||
|
||||
function backgroundcolor_onchange(event)
|
||||
{
|
||||
document.documentElement.style.setProperty("--color_htmlbg", event.target.value);
|
||||
}
|
||||
|
||||
function border_radius_onchange(event)
|
||||
{
|
||||
if (event.target.value == "500")
|
||||
{
|
||||
const value = "100%";
|
||||
document.documentElement.style.setProperty("--img_borderradius", value);
|
||||
document.documentElement.style.setProperty("--img_borderradius_tiny", value);
|
||||
document.getElementById("border_radius_value").textContent = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
const value = event.target.value + "px";
|
||||
document.documentElement.style.setProperty("--img_borderradius", value);
|
||||
document.documentElement.style.setProperty("--img_borderradius_tiny", (event.target.value / 4) + "px");
|
||||
document.getElementById("border_radius_value").textContent = value;
|
||||
}
|
||||
}
|
||||
|
||||
function saturate_onchange(event)
|
||||
{
|
||||
const value = event.target.value + "%";
|
||||
document.documentElement.style.setProperty("--img_saturate", value);
|
||||
if (event.target.value == "0")
|
||||
{
|
||||
document.getElementById("saturate_value").textContent = "Artistic";
|
||||
}
|
||||
else
|
||||
{
|
||||
document.getElementById("saturate_value").textContent = value;
|
||||
}
|
||||
}
|
||||
|
||||
function hue_rotate_onchange(event)
|
||||
{
|
||||
const value = event.target.value + "deg";
|
||||
if (event.target.value === "0")
|
||||
{
|
||||
document.documentElement.style.setProperty("--img_sepia", "0");
|
||||
}
|
||||
else
|
||||
{
|
||||
document.documentElement.style.setProperty("--img_sepia", "100%");
|
||||
}
|
||||
document.documentElement.style.setProperty("--img_huerotate", value);
|
||||
document.getElementById("hue_rotate_value").textContent = value;
|
||||
}
|
||||
|
||||
function blur_onchange(event)
|
||||
{
|
||||
const value = event.target.value + "px";
|
||||
document.documentElement.style.setProperty("--img_blur", value);
|
||||
document.getElementById("blur_value").textContent = value;
|
||||
}
|
||||
|
||||
function mixblendmode_onchange(event)
|
||||
{
|
||||
document.documentElement.style.setProperty("--img_mixblendmode", event.target.value);
|
||||
}
|
||||
|
||||
function on_pageload()
|
||||
{
|
||||
document.documentElement.addEventListener("keydown", arrowkey_listener);
|
||||
|
|
@ -315,23 +711,20 @@ def write_directory_index(directory):
|
|||
</script>
|
||||
</html>
|
||||
''').render(
|
||||
css_content=CSS_CONTENT,
|
||||
directory=directory,
|
||||
is_root=is_root,
|
||||
doctitle=doctitle,
|
||||
rss_link=rss_link,
|
||||
back_link=back_link,
|
||||
items=items,
|
||||
)
|
||||
write(directory.with_child('index.html'), page)
|
||||
|
||||
if rss_link:
|
||||
write_atom(items)
|
||||
return html
|
||||
|
||||
def write_atom(items):
|
||||
atom = jinja2.Template('''
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>voussoir.net/photography</title>
|
||||
<link href="webpath(PHOTOGRAPHY_ROOTDIR)}"/>
|
||||
<link href="https://voussoir.net/photography"/>
|
||||
<id>voussoir.net/photography</id>
|
||||
|
||||
{% for item in items %}
|
||||
|
|
@ -343,21 +736,41 @@ def write_atom(items):
|
|||
'''.strip()).render(items=items)
|
||||
write(ATOM_FILE, atom)
|
||||
|
||||
def make_thumbnail(photo):
|
||||
small_name = photo.replace_extension('').basename + '_small'
|
||||
small_name = photo.parent.with_child(small_name).add_extension(photo.extension)
|
||||
if small_name.is_file:
|
||||
return small_name
|
||||
image = PIL.Image.open(photo.absolute_path)
|
||||
icc = image.info.get('icc_profile')
|
||||
(image_width, image_height) = image.size
|
||||
exif = image.getexif()
|
||||
(width, height) = imagetools.fit_into_bounds(image_width, image_height, 1440, 1440)
|
||||
image = image.resize((width, height), PIL.Image.LANCZOS)
|
||||
image.save(small_name.absolute_path, quality=75, exif=exif, icc_profile=icc)
|
||||
print(small_name)
|
||||
return small_name
|
||||
# write_directory_index(PHOTOGRAPHY_ROOTDIR)
|
||||
# for directory in PHOTOGRAPHY_ROOTDIR.walk_directories():
|
||||
# write_directory_index(directory)
|
||||
|
||||
write_directory_index(PHOTOGRAPHY_ROOTDIR)
|
||||
for directory in PHOTOGRAPHY_ROOTDIR.walk_directories():
|
||||
write_directory_index(directory)
|
||||
@vlogging.main_decorator
|
||||
def main(argv):
|
||||
singlephotos = list(pdb.search(tag_mays=[PUBLISH_TAGNAME], has_albums=False, yield_albums=False, yield_photos=True).results)
|
||||
singlephotos += list(pdb.search(tag_mays=['voussoir_net_publish_single'], yield_albums=False, yield_photos=True).results)
|
||||
singlephotos = [Photo(p) for p in singlephotos]
|
||||
singlephotos.sort(key=lambda i: i.published, reverse=True)
|
||||
|
||||
albums = list(pdb.search(tag_musts=[PUBLISH_TAGNAME], tag_forbids=['voussoir_net_publish_single'], has_albums=True, yield_albums=True, yield_photos=False).results)
|
||||
albums = [Album(a) for a in albums]
|
||||
albums.sort(key=lambda i: i.published, reverse=True)
|
||||
|
||||
items = singlephotos + albums
|
||||
items.sort(key=lambda i: i.published, reverse=True)
|
||||
|
||||
for item in items:
|
||||
item.prepare()
|
||||
|
||||
log.info('Writing homepage')
|
||||
homepage_html = make_webpage(items, is_root=True, doctitle='photography')
|
||||
homepage_file = PHOTOGRAPHY_ROOTDIR.with_child('photography.html')
|
||||
homepage_file.write('w', homepage_html)
|
||||
|
||||
for album in albums:
|
||||
album_html = make_webpage(album.photos, is_root=False, doctitle=album.article_id)
|
||||
album_file = PHOTOGRAPHY_ROOTDIR.with_child(album.article_id).replace_extension('html')
|
||||
log.info('Writing %s', album_file.absolute_path)
|
||||
album_file.write('w', album_html)
|
||||
|
||||
write_atom(items)
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
|
|
|
|||
|
|
@ -203,8 +203,79 @@ pre
|
|||
-->
|
||||
|
||||
<article>
|
||||
<p><b>Reply-To</b>: grant@squadhelp.co</p>
|
||||
<p><b>From</b>: Fiona Hill <bettyraymond201@gmail.com></p>
|
||||
<p><b>Reply-To</b>: fionahill142@gmail.com</p>
|
||||
<p><b>To</b>: fionahill142@gmail.com</p>
|
||||
<p><b>Bcc</b>: writing@voussoir.net</p>
|
||||
<p><b>Subject</b>: </p>
|
||||
<p><b>Date</b>: Fri, 8 Sep 2023 15:29:07 +0000</p>
|
||||
<details>
|
||||
<summary>Headers</summary>
|
||||
<pre>
|
||||
X-Spam-Status: No, score=-2.44 required=5 tests=DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,FREEMAIL_FROM,FREEMAIL_REPLYTO,FREEMAIL_REPLYTO_END_DIGIT,RCVD_IN_MSPIKE_H2,SPF_HELO_NONE,PURELYMAIL_IP_REPUTATION autolearn=ham autolearn_force=no version=4.0.0
|
||||
X-Pm-Spam-PURELYMAIL-IP-REPUTATION: -2.64;EffectiveIp=209.85.219.41,SpamProbability=0.02
|
||||
Return-Path: <bettyraymond201@gmail.com>
|
||||
Delivered-To: ethan@voussoir.net
|
||||
X-Pm-Known-Alias: writing@voussoir.net
|
||||
X-Pm-Original-To: writing@voussoir.net
|
||||
Authentication-Results: purelymail.com; spf=pass (domain of _spf.google.com designates 209.85.219.41 as permitted sender) smtp.mailfrom=gmail.com; dkim=pass header.i=gmail.com; dmarc=pass (p=none) header.from=bettyraymond201@gmail.com
|
||||
Received: from mail-qv1-f41.google.com (EHLO mail-qv1-f41.google.com) ([209.85.219.41])
|
||||
by smtp.purelymail.com (Purelymail SMTP) with ESMTPS id -114360871
|
||||
for <writing@voussoir.net>
|
||||
(version=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384);
|
||||
Fri, 08 Sep 2023 15:29:08 +0000 (UTC)
|
||||
Received: by mail-qv1-f41.google.com with SMTP id 6a1803df08f44-64f3ad95ec0so13119666d6.1
|
||||
for <writing@voussoir.net>; Fri, 08 Sep 2023 08:29:08 -0700 (PDT)
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||
d=gmail.com; s=20221208; t=1694186948; x=1694791748; darn=voussoir.net;
|
||||
h=to:subject:message-id:date:from:reply-to:mime-version:from:to:cc
|
||||
:subject:date:message-id:reply-to;
|
||||
bh=Ba3gj8+xBPQLJTahTfzW6RbWQ/XPgESxkCi2B66PSQg=;
|
||||
b=YasTMC07fWjdPrJEgfH9/EbuJwftHxPKdoeKPhwuG0DlU3lg1Zo/j8NtchSnEt1Gdp
|
||||
5OqyuPcnmVW3BZodZUZsR6R1lmlmAnbt/2lsDUrHGednKwfjwmxjekCWURhU4cxPatDl
|
||||
DkQxA/su7gAHB2Gzb+E1qiWwqOmm7Lc/ettNaamilMQUHQ0MKdgCMQd9kDy49km6mY/1
|
||||
R6Nvap5kc1rY46b02VxitvacfSiRqR3BSyWqJSSUvtjXc1o/WcfSA50YXW2xOjlQgEG9
|
||||
t/ckwsDY7uHYmdRHUaZGokiEvdD5O0f53Rp6byCxJublcIa180ShWngjPZWwFslltr0I
|
||||
ex1g==
|
||||
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||
d=1e100.net; s=20230601; t=1694186948; x=1694791748;
|
||||
h=to:subject:message-id:date:from:reply-to:mime-version
|
||||
:x-gm-message-state:from:to:cc:subject:date:message-id:reply-to;
|
||||
bh=Ba3gj8+xBPQLJTahTfzW6RbWQ/XPgESxkCi2B66PSQg=;
|
||||
b=CBxDaKVaxeHutPqbgxvlqmwwoZ0jsMgDFsBmr8zgYCF+wiA1IbaioYudVPkENjzNNo
|
||||
0t2H0B9rRo2KmaBzwxDX2pt6dvtpCJ80M1lN3eFlpSmbZqigglv7AfyVEsFtfaLwAPxv
|
||||
MurPpOErCWMoWYNhI5TAYUhaWs95WLQaEn125TJFWmufhEnVoEd2U3RMnACcn+B7dNsT
|
||||
FUuise8xuOJSJhA+MYxrQnchDwDkCIkruhkErMt9/IWjnZ6JIeKTdtVT/2NkGOEo50o5
|
||||
CBIp2fbgi8BWchGuq9rxUxtXteVuJhpsOgXtLJu9UfRj2RGgNhtl+9IDHNSF1hfTysqo
|
||||
Oh/w==
|
||||
X-Gm-Message-State: AOJu0YyInMyJvkYc6g+p66NCNSu/cKD11y6pidrfNcAMnIBlTT50oy0v
|
||||
Ud1GUYOI+8wMUMpHNZ/7nzUAgs2vZhLU9MiwodY=
|
||||
X-Google-Smtp-Source: AGHT+IEZJmW6LWDaz7ky7Vtqc9VL15HnDlWpbje8Ob/RkQ+ioqTvE3WaJRy4/QkqQfqQjVn/BbhhjU/DI2QearkIFHA=
|
||||
X-Received: by 2002:a0c:d610:0:b0:647:1a66:181a with SMTP id
|
||||
c16-20020a0cd610000000b006471a66181amr2420913qvj.50.1694186947840; Fri, 08
|
||||
Sep 2023 08:29:07 -0700 (PDT)
|
||||
MIME-Version: 1.0
|
||||
Received: by 2002:a0c:ab5e:0:b0:64d:3e37:9bff with HTTP; Fri, 8 Sep 2023
|
||||
08:29:07 -0700 (PDT)
|
||||
Reply-To: fionahill142@gmail.com
|
||||
From: Fiona Hill <bettyraymond201@gmail.com>
|
||||
Date: Fri, 8 Sep 2023 15:29:07 +0000
|
||||
Message-ID: <CALZothvq=+PPgJAZxrSNGZa1hx4DcnMTYqxg4ZiEwuntu1TKLA@mail.gmail.com>
|
||||
Subject:
|
||||
To: fionahill142@gmail.com
|
||||
Content-Type: text/plain; charset="UTF-8"
|
||||
Bcc: writing@voussoir.net
|
||||
</pre>
|
||||
</details>
|
||||
|
||||
<hr/>
|
||||
|
||||
<p>Hello</p>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<p><b>From</b>: Grant Polachek <pola-grant@squadhelp.co></p>
|
||||
<p><b>Reply-To</b>: grant@squadhelp.co</p>
|
||||
<p><b>To</b>: contact@voussoir.net</p>
|
||||
<p><b>Subject</b>: Re: Want an exclusive guest article for voussoir.net?</p>
|
||||
<p><b>Date</b>: Tue, 05 Sep 2023 11:59:33 +0000</p>
|
||||
|
|
|
|||
|
|
@ -525,7 +525,7 @@ Spam is built upon the abuse of systems by definition. Unlike sponsors, who make
|
|||
2. are highly trafficked, because spammers need to hit as many people as possible before getting caught.
|
||||
3. provide otherwise legitimate value to your life, so that you will not simply leave the space after the spammers invade.
|
||||
|
||||
Your email inbox and your telephone are spaces for communicating with people you know, and spammers abuse it to send you junk. Web forums are spaces for conversation, and spammers abuse them by making fake posts. Grocery stores are spaces for buying food, and spammers abuse the traffic flow to hand out fliers. [Parking lots](/writing/not_just_bikes) are part of our transit system, and spammers leave advertisements under your wipers because you're not there to stop them. The front door of your house is for yourself and your guests, and spammers abuse it by turning it into ad space.
|
||||
Your [email inbox](/spam) and your telephone are spaces for communicating with people you know, and spammers abuse it to send you junk. Web forums are spaces for conversation, and spammers abuse them by making fake posts. Grocery stores are spaces for buying food, and spammers abuse the traffic flow to hand out fliers. [Parking lots](/writing/not_just_bikes) are part of our transit system, and spammers leave advertisements under your wipers because you're not there to stop them. The front door of your house is for yourself and your guests, and spammers abuse it by turning it into ad space.
|
||||
|
||||
The problem with spam is not just that it's annoying, but that it converts your entire life into an arms race. Spam is hostile. It is mean. Spam takes every aspect of your life and turns it into an opportunity to make a buck off of you. No matter how hard you work to avoid advertisements and carefully pick the publishers you want to pay for, spammers can override your decisions and advertise to you anyway. I am not kidding when I compare spam to terrorism, though I am exaggerating. Spam positions you against enemies you didn't know you had. Enemies who come from afar to your place of residence to disrespect your time, attention, and belongings, and sneak away. Unlike a TV which can be turned off or a magazine which can be closed, spam offers no means to opt-out. Spammers will hit you and run without showing their face. What are you going to do about it, not have an email address? Not have a phone number?
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,20 @@ Cyborgs on HN
|
|||
|
||||
This page collects comments which make unnecessary or tenuous analogies to computers, programming, dollar-sign $variables, sed's/replace/syntax/g, mathematics, AI/machine learning, and cryptography in discussions that aren't about those things.
|
||||
|
||||
> If you search "watch $MOVIE free" on google you're going to get netflix, Hulu, prime, Disney etc as the first results regardless of whether those sites even have it in their library.
|
||||
|
||||
-
|
||||
|
||||
> I hope these journalists are ashamed of themselves and I hope they are at least sacked, with their careers relegated to selling poutine all day.
|
||||
|
||||
> > s/selling/eating/ ?
|
||||
|
||||
-
|
||||
|
||||
> There is always plenty wrong with each new release but the comments and jokes about ${"LATEST_RELEASE"} of ${"SOFTWARE"} being unilaterally bad just make me think the person can't deal with change instead of the software actually being bad.
|
||||
|
||||
-
|
||||
|
||||
> I'm scratching my head here. The old gas/diesel dispensing stations have solved this problem of restricting people taking all of the fuel in the pump with a disruptive financial technology called blo\^H\^H\^H credit card.
|
||||
|
||||
-
|
||||
|
|
|
|||
|
|
@ -162,6 +162,11 @@ table th, table td
|
|||
padding: 4px;
|
||||
}
|
||||
|
||||
hr
|
||||
{
|
||||
border-color: var(--color_codeborder);
|
||||
}
|
||||
|
||||
ol ol, ul ul, ol ul, ul ol
|
||||
{
|
||||
padding-inline-start: 20px;
|
||||
|
|
|
|||
|
|
@ -145,6 +145,10 @@ You're overthinking it!
|
|||
|
||||
Feedback wanted.
|
||||
|
||||
---
|
||||
|
||||
**Update**: I have not received any feedback, and I've taken some more good pictures. [Scales tipped](https://voussoir.net/photography).
|
||||
|
||||

|
||||
|
||||

|
||||
|
|
|
|||
Loading…
Reference in a new issue