Compare commits

..

34 commits

Author SHA1 Message Date
8daa6fa09f normal font size 2024-07-07 22:13:55 +02:00
85e2c6e5ac tweak for filterlist style 2024-07-07 22:12:44 +02:00
00a238e00f add styling for cohost/fedi posts in descriptions 2024-07-07 22:12:21 +02:00
58b88ed23c make bold text bolder in a cw 2024-07-07 22:08:37 +02:00
0561f9dab1 use grid instead of flex for index page 2024-07-07 22:07:05 +02:00
02214bca0b pass click through emblems 2024-07-07 22:06:44 +02:00
56795be3ac remove border already hidden by clip-path 2024-07-07 22:06:22 +02:00
ab0e1a4127 filters always collapsed on page load 2024-07-07 22:05:59 +02:00
f2aeb8e187 add shortcut for certain filters 2024-07-07 22:05:08 +02:00
d93e38b618 appearance: none has accessibility problems 2024-07-07 22:01:05 +02:00
411c93e860 gap → grid-gap 2024-07-07 22:00:44 +02:00
5d576d358c replace nsfw warning with a positive test 2024-07-07 21:49:29 +02:00
38120bc60f improve some null handling 2024-07-07 21:49:29 +02:00
e89970d339 instanceof instead of nodeType 2024-07-07 21:49:29 +02:00
cd469f33db remove an unneeded null check 2024-07-07 21:49:29 +02:00
762300dcaf rename a variable 2024-07-07 21:49:29 +02:00
b2e2db77dd const instead of let where possible 2024-07-07 21:49:29 +02:00
0de54d15d4 make a type more general 2024-07-07 21:49:29 +02:00
fe338b2f3f type signatures 2024-07-07 21:49:29 +02:00
97660781a7 replace Array.from with a splat or whatever it's called 2024-07-07 21:49:29 +02:00
b1b0f806d5 type signature on fillSets 2024-07-07 21:49:29 +02:00
5c70198e38 remove twitter username 2024-07-07 21:49:29 +02:00
d06f749b8c switch to GHC2024 2024-07-07 21:49:29 +02:00
5031058bde lowercase name 2024-07-07 20:03:19 +02:00
fa8fa4a18b update cabal deps up to ghc 9.10 2024-07-07 19:48:26 +02:00
a1453db34a move artist to main info for visiblity 2024-07-07 19:45:50 +02:00
06ca357d60 90% quality for medium webp and full size for big 2024-07-07 19:44:26 +02:00
1f84b3e83e fix a copy rule in Makefile 2024-07-07 19:43:58 +02:00
61fd58413f remove TransformListComp which panics in ghc ≥ 9.6 2024-07-07 16:22:24 +02:00
6554dfd54c no line breaking within tags 2024-07-07 14:09:38 +02:00
bde75df8ca fix sfw sort with nsfw update
if the last update to an item is nsfw-only, then it would affect the
sfw sorting, even tho it is not relevant, and even if it had ignore-sort
set. now it works properly in that situation
2024-01-09 15:35:31 +01:00
09897f27d9 gradients 2023-12-28 13:11:28 +01:00
7a08c05cea you WOULD type a script 2023-09-07 01:17:21 +02:00
8cb6752168 add flag for lossless webp in convert calls 2023-09-06 20:31:45 +02:00
21 changed files with 542 additions and 464 deletions

View file

@ -16,22 +16,24 @@ MAKEPAGES := $(TMPDIR)/make-pages
YAMLS != find -L $(DATADIR) -name $(INFONAME) YAMLS != find -L $(DATADIR) -name $(INFONAME)
SCRIPTS != find script -name '*.js' TSCRIPTS != find script -name '*.ts'
STYLES != find style -name '*.css' STYLES != find style -name '*.css'
STYLESVGS != find style -name '*.svg' STYLESVGS != find style -name '*.svg'
STYLEPNGS != find style -name '*.png' STYLEPNGS != find style -name '*.png'
FONTS != find fonts \ FONTS != find fonts \
-iname '*.eot' -or -iname '*.svg' -or \ -iname '*.eot' -or -iname '*.svg' -or \
-iname '*.ttf' -or -iname '*.woff' -or \ -iname '*.ttf' -or -iname '*.woff' -or \
-iname '*.woff2' -or -iname '*.css' -iname '*.woff2' -or -iname '*.css'
STATIC = $(SCRIPTS) $(STYLES) $(STYLEPNGS) $(STYLESVGS) $(FONTS) STATIC = $(STYLES) $(STYLEPNGS) $(STYLESVGS) $(FONTS)
BSTATIC = $(patsubst %,$(BUILDDIR)/%,$(STATIC)) BSTATIC = $(patsubst %,$(BUILDDIR)/%,$(STATIC))
BSCRIPTS = $(patsubst %.ts,$(BUILDDIR)/%.js,$(TSCRIPTS))
.PHONY: all build .PHONY: all build
all: build all: build
build: $(BUILDDIR)/index.html $(BSTATIC) build: $(BUILDDIR)/index.html $(BSTATIC) $(BSCRIPTS)
$(BUILDDIR)/index.html: $(DATADIR)/index.yaml $(MAKEPAGES) $(BUILDDIR)/index.html: $(DATADIR)/index.yaml $(MAKEPAGES)
echo "[index] "$@ echo "[index] "$@
@ -43,9 +45,15 @@ $(BUILDDIR)/%: %
$(call copy,--link --force) $(call copy,--link --force)
$(BUILDDIR)/%: $(TMPDIR)/% $(BUILDDIR)/%: $(TMPDIR)/%
$(call copy,--link) $(call copy,--link --force)
$(TMPDIR)/%.js: %.ts
echo "[tsc] "$@
tsc --strict --noEmitOnError \
--lib dom,es2021 --target es2015 \
--outDir $(dir $@) $^
$(TMPDIR)/%_small.png: $(DATADIR)/%.png $(TMPDIR)/%_small.png: $(DATADIR)/%.png
$(call resize,$(SMALL),$(SMALL),^,-gravity center -crop 1:1+0) $(call resize,$(SMALL),$(SMALL),^,-gravity center -crop 1:1+0)
@ -70,10 +78,10 @@ $(TMPDIR)/%_small.webp: $(DATADIR)/%.webp
$(call resize,$(SMALL),$(SMALL),^,-gravity center -crop 1:1+0) $(call resize,$(SMALL),$(SMALL),^,-gravity center -crop 1:1+0)
$(TMPDIR)/%_med.webp: $(DATADIR)/%.webp $(TMPDIR)/%_med.webp: $(DATADIR)/%.webp
$(call resize,$(MEDW),$(MEDH),>) $(call resize,$(MEDW),$(MEDH),>,-quality 90)
$(TMPDIR)/%_big.webp: $(DATADIR)/%.webp $(TMPDIR)/%_big.webp: $(DATADIR)/%.webp
$(call resize,$(BIG),$(BIG),>) $(call copy)
$(MAKEPAGES): make-pages/*.hs make-pages/make-pages.cabal $(MAKEPAGES): make-pages/*.hs make-pages/make-pages.cabal
@ -140,7 +148,7 @@ endef
define resize define resize
echo "[resize] "$@ echo "[resize] "$@
mkdir -p "$(dir $@)" mkdir -p "$(dir $@)"
convert -resize "$(1)x$(2)$(3)" $(4) "$^" "$@" convert -resize "$(1)x$(2)$(3)" -define webp:lossless=true $(4) "$^" "$@"
endef endef
# no args # no args

View file

@ -1,4 +1,4 @@
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE PatternSynonyms, TemplateHaskell #-}
module BuilderQQ module BuilderQQ
(b, (b,
Builder, toStrictText, toLazyText, fromText, fromString, fromChar, Builder, toStrictText, toLazyText, fromText, fromString, fromChar,

View file

@ -1,4 +1,3 @@
{-# LANGUAGE TransformListComp #-}
module GalleryPage (make) where module GalleryPage (make) where
import BuilderQQ import BuilderQQ
@ -8,15 +7,15 @@ import qualified NsfwWarning
import Control.Monad import Control.Monad
import Data.Foldable import Data.Foldable
import Data.Function (on, (&)) import Data.Function ((&))
import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HashMap
import Data.HashSet (HashSet) import Data.HashSet (HashSet)
import qualified Data.HashSet as HashSet import qualified Data.HashSet as HashSet
import Data.List (intersperse, groupBy, sortBy, sort) import Data.List (intersperse, sort, sortOn)
import Data.Maybe import Data.Maybe
import Data.Ord (Down (..))
import qualified Data.Text.Lazy as Lazy import qualified Data.Text.Lazy as Lazy
import System.FilePath (takeDirectory, joinPath, splitPath) import System.FilePath (takeDirectory, joinPath, splitPath)
import GHC.Exts (Down (..), the)
make :: Text -> GalleryInfo -> [(FilePath, Info)] -> Lazy.Text make :: Text -> GalleryInfo -> [(FilePath, Info)] -> Lazy.Text
make root ginfo infos = toLazyText $ make' root ginfo infos make root ginfo infos = toLazyText $ make' root ginfo infos
@ -42,7 +41,7 @@ make' root (GalleryInfo {title, desc, prefix, filters, hidden}) infos = [b|@0
<meta name=robots content='noai,noimageai'> <meta name=robots content='noai,noimageai'>
<script src=/script/gallery.js></script> <script src=/script/gallery.js type=module></script>
$0.nsfwScript $0.nsfwScript
<title>$title</title> <title>$title</title>
@ -90,15 +89,11 @@ make' root (GalleryInfo {title, desc, prefix, filters, hidden}) infos = [b|@0
where where
items = map (uncurry $ makeYearItems nsfw) infosByYear items = map (uncurry $ makeYearItems nsfw) infosByYear
infosByYear = infosByYear :: [(Int, [(FilePath, Info)])]
[(the year, infopath) | infosByYear = infos &
infopath@(_, info) <- infos, filter (not . #unlisted . snd) &
not $ #unlisted info, sortOn (Down . compareKeyFor nsfw . snd) &
then sortInfo by info, groupOnKey (\(_, i) -> #latestYear i nsfw)
let year = #latestYear info nsfw,
then group by Down year using groupBy']
sortInfo f = sortBy $ flip (compareFor nsfw `on` f)
groupBy' f = groupBy ((==) `on` f)
undir = joinPath (replicate (length (splitPath prefix)) "..") undir = joinPath (replicate (length (splitPath prefix)) "..")
@ -114,13 +109,20 @@ make' root (GalleryInfo {title, desc, prefix, filters, hidden}) infos = [b|@0
url = [b|$root/$prefix|] url = [b|$root/$prefix|]
imagepath0 imagepath0
| (_, (p0, i0) : _) : _ <- infosByYear = getThumb (takeDirectory p0) i0 | (_, (p, i) : _) : _ <- infosByYear = getThumb (takeDirectory p) i
| otherwise = "/style/card.png" | otherwise = "/style/card.png"
nsfw' = NsfwWarning.Gallery <$ guard nsfw nsfw' = NsfwWarning.Gallery <$ guard nsfw
nsfwScript = NsfwWarning.script nsfw' nsfwScript = NsfwWarning.script nsfw'
nsfwDialog = NsfwWarning.dialog nsfw' nsfwDialog = NsfwWarning.dialog nsfw'
-- from @extra@
groupOnKey :: Eq k => (a -> k) -> [a] -> [(k, [a])]
groupOnKey _ [] = []
groupOnKey f (x:xs) = (fx, x:yes) : groupOnKey f no where
fx = f x
(yes, no) = span (\y -> fx == f y) xs
makeFilter :: Text -> HashSet Text -> Text -> Int -> Builder makeFilter :: Text -> HashSet Text -> Text -> Int -> Builder
makeFilter prefix initial tag count = [b|@0 makeFilter prefix initial tag count = [b|@0
<li$hidden> <li$hidden>

View file

@ -4,7 +4,7 @@
module Info module Info
(Info (..), (Info (..),
tagsFor, descFor, imagesFor, linksFor, updatesFor, lastUpdate, tagsFor, descFor, imagesFor, linksFor, updatesFor, lastUpdate,
compareFor, sortFor, CompareKey (..), compareKeyFor, compareFor, sortFor,
Artist (..), Images' (..), Images, Image (..), Desc (..), DescField (..), Artist (..), Images' (..), Images, Image (..), Desc (..), DescField (..),
Link (..), Update (..), Bg (..), Link (..), Update (..), Bg (..),
GalleryInfo (..), GalleryFilters (..), ArtistFilter (..), NsfwFilter (..), GalleryInfo (..), GalleryFilters (..), ArtistFilter (..), NsfwFilter (..),
@ -163,7 +163,8 @@ instance HasField "notMine" Info Bool where getField = isJust . #artist
instance HasField "latestDate" Info (Bool -> Date) where instance HasField "latestDate" Info (Bool -> Date) where
getField info@(Info {date=date}) nsfw = getField info@(Info {date=date}) nsfw =
maximum $ date : mapMaybe relDate (updatesFor nsfw info) maximum $ date : mapMaybe relDate (updatesFor nsfw info)
where relDate (date, us) = date <$ guard (not $ any #ignoreSort us) where
relDate (date, us) = date <$ guard (not $ null us || any #ignoreSort us)
instance HasField "latestYear" Info (Bool -> Int) where instance HasField "latestYear" Info (Bool -> Int) where
getField info nsfw = #year $ #latestDate info nsfw getField info nsfw = #year $ #latestDate info nsfw
@ -224,8 +225,14 @@ lastUpdate :: Bool -> Info -> Maybe Date
lastUpdate nsfw info = lastUpdate nsfw info =
case updatesFor nsfw info of [] -> Nothing; us -> Just $ fst $ last us case updatesFor nsfw info of [] -> Nothing; us -> Just $ fst $ last us
data CompareKey = MkCompareKey !Date !Text !Text
deriving (Eq, Ord)
compareKeyFor :: Bool -> Info -> CompareKey
compareKeyFor nsfw i = MkCompareKey (#latestDate i nsfw) (#sortEx i) (#title i)
compareFor :: Bool -> Info -> Info -> Ordering compareFor :: Bool -> Info -> Info -> Ordering
compareFor nsfw = comparing \i -> (#latestDate i nsfw, #sortEx i, #title i) compareFor nsfw = comparing $ compareKeyFor nsfw
sortFor :: Bool -> [Info] -> [Info] sortFor :: Bool -> [Info] -> [Info]
sortFor = sortBy . compareFor sortFor = sortBy . compareFor

View file

@ -1,4 +1,4 @@
{-# LANGUAGE CPP, ImplicitParams, TypeApplications #-} {-# LANGUAGE CPP, ImplicitParams #-}
module Main (main) where module Main (main) where
import Control.Monad import Control.Monad

View file

@ -12,7 +12,7 @@ instance CanBuild What where
script :: Maybe What -> Builder script :: Maybe What -> Builder
script Nothing = "" script Nothing = ""
script (Just _) = [b|<script src=/script/nsfw-warning.js></script>|] script (Just _) = [b|<script src=/script/nsfw-warning.js type=module></script>|]
dialog :: Maybe What -> Builder dialog :: Maybe What -> Builder
dialog Nothing = "" dialog Nothing = ""

View file

@ -1,5 +1,4 @@
{-# OPTIONS_GHC -Wno-orphans #-} {-# OPTIONS_GHC -Wno-orphans #-}
{-# LANGUAGE MultiParamTypeClasses, ScopedTypeVariables, TypeApplications #-}
module Records (HasField (..)) where module Records (HasField (..)) where
import GHC.Records import GHC.Records

View file

@ -45,8 +45,6 @@ make' root siteName prefix nsfw _dataDir dir
let undir = joinPath (replicate (length (splitPath dir)) "..") let undir = joinPath (replicate (length (splitPath dir)) "..")
let artistTag = ifJust artist makeArtist
let formattedDate = formatLong date let formattedDate = formatLong date
let buttonBar = makeButtonBar title $ addIds images let buttonBar = makeButtonBar title $ addIds images
@ -58,6 +56,7 @@ make' root siteName prefix nsfw _dataDir dir
let download0 = fromMaybe (bigFile path0) download0' let download0 = fromMaybe (bigFile path0) download0'
let path0' = pageFile path0 let path0' = pageFile path0
let artistSection = makeArtist artist
let descSection = makeDesc $ descFor nsfw info let descSection = makeDesc $ descFor nsfw info
let tagsList = makeTags undir $ tagsFor nsfw info let tagsList = makeTags undir $ tagsFor nsfw info
let linksList = extLinks $ linksFor nsfw info let linksList = extLinks $ linksFor nsfw info
@ -136,12 +135,11 @@ make' root siteName prefix nsfw _dataDir dir
<meta property=og:site_name content="$siteName"> <meta property=og:site_name content="$siteName">
<meta property=og:description content="$desc"> <meta property=og:description content="$desc">
<meta property=og:url content="$url"> <meta property=og:url content="$url">
<meta name=twitter:site content=@2_gecs>
$imageMeta $imageMeta
<meta name=robots content='noai,noimageai'> <meta name=robots content='noai,noimageai'>
<script src=/script/single.js></script> <script src=/script/single.js type=module></script>
$nsfwScript $nsfwScript
$bgStyle $bgStyle
@ -158,7 +156,6 @@ make' root siteName prefix nsfw _dataDir dir
$formattedDate $updateDate $formattedDate $updateDate
</h2> </h2>
<h2 class="left corner"> <h2 class="left corner">
$artistTag
<a href=$undir>back to gallery</a> <a href=$undir>back to gallery</a>
</h2> </h2>
</header> </header>
@ -174,6 +171,8 @@ make' root siteName prefix nsfw _dataDir dir
</figure> </figure>
<div id=info> <div id=info>
$6.artistSection
$6.descSection $6.descSection
$6.updatesList $6.updatesList
@ -193,9 +192,14 @@ make' root siteName prefix nsfw _dataDir dir
last' :: [a] -> Maybe a last' :: [a] -> Maybe a
last' xs = if null xs then Nothing else Just $ last xs last' xs = if null xs then Nothing else Just $ last xs
makeArtist :: Artist -> Builder makeArtist :: Maybe Artist -> Builder
makeArtist (Artist {name, url}) = makeArtist Nothing = ""
[b|by $artistLink <br>|] makeArtist (Just (Artist {name, url})) = [b|@0
<section id=desc class=info-section>
<h2>by</h2>
<div>$artistLink</div>
</section>
|]
where where
artistLink = case url of artistLink = case url of
Just u -> [b|<a href="$u">$name</a>|] Just u -> [b|<a href="$u">$name</a>|]
@ -247,7 +251,8 @@ makeButtonBar title images =
| otherwise -> | otherwise ->
makeNav "cat" $ map (uncurry makeCat) cats makeNav "cat" $ map (uncurry makeCat) cats
where where
makeNav (cls :: Text) inner = [b|@0 makeNav :: CanBuild b => Text -> b -> Builder
makeNav cls inner = [b|@0
<nav id=alts class=$cls aria-label="alternate versions"> <nav id=alts class=$cls aria-label="alternate versions">
$2.inner $2.inner
$2.skipAll $2.skipAll
@ -271,8 +276,7 @@ makeButtonBar title images =
<label for=skipAll>skip warnings</label> <label for=skipAll>skip warnings</label>
</div> </div>
|] |]
else else ""
""
flatten :: [(Text, [(Image, a)])] -> [(Image, Text)] flatten :: [(Text, [(Image, a)])] -> [(Image, Text)]
flatten cats = flatten cats =

View file

@ -3,8 +3,8 @@ name: make-pages
version: 0.1.0 version: 0.1.0
license: AGPL-3.0-or-later license: AGPL-3.0-or-later
author: Rhiannon Morris <rhi@rhiannon.website> author: rhiannon morris <rhi@rhiannon.website>
maintainer: Rhiannon Morris <rhi@rhiannon.website> maintainer: rhiannon morris <rhi@rhiannon.website>
flag pretty-verbose flag pretty-verbose
description: pretty-print the verbose output description: pretty-print the verbose output
@ -26,53 +26,36 @@ executable make-pages
RSS, RSS,
ListTags, ListTags,
Options Options
default-language: Haskell2010 default-language: GHC2024
default-extensions: default-extensions:
BlockArguments, BlockArguments,
ConstraintKinds,
DataKinds,
DeriveAnyClass, DeriveAnyClass,
DeriveTraversable,
DerivingStrategies,
DerivingVia, DerivingVia,
DuplicateRecordFields, DuplicateRecordFields,
FlexibleContexts,
FlexibleInstances,
GeneralizedNewtypeDeriving,
LambdaCase,
NamedFieldPuns,
OverloadedLabels, OverloadedLabels,
OverloadedLists, OverloadedLists,
OverloadedStrings, OverloadedStrings,
PatternSynonyms,
QuasiQuotes, QuasiQuotes,
RankNTypes,
ScopedTypeVariables,
StandaloneDeriving,
TupleSections,
TypeSynonymInstances, TypeSynonymInstances,
ViewPatterns ViewPatterns
other-extensions: other-extensions:
PatternSynonyms,
CPP, CPP,
ImplicitParams, ImplicitParams,
MultiParamTypeClasses, TemplateHaskell
ScopedTypeVariables,
TemplateHaskell,
TransformListComp,
TypeApplications
build-depends: build-depends:
base ^>= 4.16.4, base >= 4.16.4 && < 4.21,
bytestring ^>= 0.11.3.1, bytestring >= 0.11.3.1 && < 0.14,
containers ^>= 0.6.0.1, containers >= 0.6.0.1 && < 0.8,
filemanip ^>= 0.3.6.3, filemanip ^>= 0.3.6.3,
filepath ^>= 1.4.2.1, filepath >= 1.4.2.1 && < 1.6,
hashable ^>= 1.3.0.0, hashable >= 1.3.0.0 && < 1.5,
HsYAML ^>= 0.2.1.0, HsYAML ^>= 0.2.1.0,
optparse-applicative ^>= 0.15.1.0, optparse-applicative ^>= 0.15.1.0,
process ^>= 1.6.8.2, process ^>= 1.6.8.2,
template-haskell ^>= 2.18.0.0, template-haskell >= 2.18.0.0 && < 2.23,
text ^>= 1.2.3.1, text >= 1.2.3.1 && < 2.2,
time >= 1.8.0.2 && < 1.10, time >= 1.8.0.2 && < 1.13,
unordered-containers ^>= 0.2.11.0 unordered-containers ^>= 0.2.11.0
ghc-options: ghc-options:
-Wall -threaded -rtsopts -with-rtsopts=-N -O -Wall -threaded -rtsopts -with-rtsopts=-N -O

View file

@ -1,204 +0,0 @@
(function() {
'use strict';
let reqBoxes;
let excBoxes;
let allBoxes;
let tags;
let itemsByYear;
let showSingles = false;
function fillSets() {
let checkedValues = boxes =>
new Set(boxes.filter(b => b.checked).map(b => b.value));
return [checkedValues(reqBoxes), checkedValues(excBoxes)];
}
function updateItems() {
let [reqTags, excTags] = fillSets();
let anyReq = reqTags.size > 0;
for (let [year, items] of itemsByYear) {
let hide = true;
for (let item of items) {
let req = tags.get(item).some(x => reqTags.has(x));
let exc = tags.get(item).some(x => excTags.has(x));
let hidden = exc || (anyReq && !req);
item.hidden = hidden;
hide &&= hidden;
}
document.getElementById(`marker-${year}`).hidden = hide;
}
function disp(pfx, tags) {
return Array(...tags).map(x => pfx + x).join('\u2003'); // em space
}
let plus = disp('+\u2009', reqTags); // thin space
let minus = disp('-\u2009', excTags);
document.getElementById('filters-details').dataset.filters =
`${plus}\u2003${minus}`.trim();
}
function update() {
updateItems();
history.pushState(null, "", makeFragment());
}
function converseId(id) {
if (id.match(/^require/)) {
return id.replace('require', 'exclude');
} else {
return id.replace('exclude', 'require');
}
}
function toggle(checkbox) {
if (checkbox.checked)
document.getElementById(converseId(checkbox.id)).checked = false;
update();
}
function clearForm() {
allBoxes.forEach(b => b.checked = b.defaultChecked);
}
function clear(e) {
clearForm();
update();
if (e) e.preventDefault();
}
function toggleSingles(e) {
showSingles = !showSingles;
for (let li of document.querySelectorAll('.filterlist li')) {
let count = +li.querySelector('label').dataset.count;
if (count <= 1) {
li.hidden = !showSingles;
}
}
if (e) e.preventDefault();
}
function makeFragment() {
let ids = allBoxes.filter(b => b.checked).map(b => b.id);
if (ids.length == 0) {
return '#all';
} else if (allBoxes.every(b => b.checked == b.defaultChecked)) {
return '#';
} else {
return '#' + ids.join(';');
}
}
function useFragment() {
let frag = decodeURIComponent(location.hash).replace(/^#/, '');
let details = document.getElementById('filters-details');
if (!frag) {
clearForm();
} else if (frag == 'all') {
allBoxes.forEach(b => b.checked = false);
details.open = false;
} else {
let set = new Set(frag.split(';'));
let re = /^(require|exclude)_|hide_filters/;
if (Array.from(set).every(x => re.test(x))) {
allBoxes.forEach(b => b.checked = set.has(b.id));
details.open = !frag.match(/hide_filters|example\b/);
}
}
updateItems();
}
function sortFilters(cmp) {
function sort1(id) {
let elt = document.getElementById(id);
let children = [...elt.childNodes];
children.sort(cmp);
for (let c of children) {
elt.removeChild(c);
elt.appendChild(c);
}
}
sort1('require');
sort1('exclude');
}
function sortFiltersAlpha(e) {
function getName(x) {
if (x.nodeType == Node.ELEMENT_NODE) {
return x.getElementsByTagName('input')[0].value;
} else {
return '';
}
}
sortFilters((a, b) => getName(a).localeCompare(getName(b)));
e.preventDefault();
}
function sortFiltersUses(e) {
function getUses(x) {
if (x.nodeType == Node.ELEMENT_NODE) {
return parseInt(x.getElementsByTagName('label')[0].dataset.count);
} else {
return 0;
}
}
sortFilters((a, b) => getUses(b) - getUses(a));
e.preventDefault();
}
function setup() {
function inputs(id) {
let iter = document.getElementById(id).getElementsByTagName('input');
return Array.from(iter);
}
let items = Array.from(document.getElementsByClassName('post'));
itemsByYear = new Map;
for (let item of items) {
let year = item.dataset.year;
if (!itemsByYear.has(year)) itemsByYear.set(year, new Set);
itemsByYear.get(year).add(item);
}
reqBoxes = inputs('require');
excBoxes = inputs('exclude');
allBoxes = [...reqBoxes, ...excBoxes];
tags = new Map(items.map(item => [item, item.dataset.tags.split(';')]));
allBoxes.forEach(b => b.addEventListener('change', () => toggle(b)));
function addClick(id, f) {
document.getElementById(id).addEventListener('click', f);
}
addClick('clear', clear);
addClick('sortalpha', sortFiltersAlpha);
addClick('sortuses', sortFiltersUses);
addClick('singles', toggleSingles);
window.addEventListener('popstate', useFragment);
useFragment();
}
window.addEventListener('DOMContentLoaded', setup);
})();

224
script/gallery.ts Normal file
View file

@ -0,0 +1,224 @@
type Boxes = Set<HTMLInputElement>;
let reqBoxes: Boxes;
let excBoxes: Boxes;
let allBoxes: Boxes;
let tags: Map<HTMLElement, string[]>;
let itemsByYear: Map<string, Set<HTMLElement>>;
let showSingles = false;
function fillSets(): [Set<string>, Set<string>] {
function checkedValues(boxes: Boxes) {
return new Set([...boxes].filter(b => b.checked).map(b => b.value));
}
return [checkedValues(reqBoxes), checkedValues(excBoxes)];
}
function updateItems() {
const [reqTags, excTags] = fillSets();
const anyReq = reqTags.size > 0;
for (const [year, items] of itemsByYear) {
let hideMarker = true;
for (const item of items) {
const req = tags.get(item)?.some(x => reqTags.has(x)) ?? false;
const exc = tags.get(item)?.some(x => excTags.has(x)) ?? false;
const hidden = exc || (anyReq && !req);
item.hidden = hidden;
hideMarker &&= hidden;
}
const marker = document.getElementById(`marker-${year}`);
if (marker !== null) marker.hidden = hideMarker;
}
function disp(pfx: string, tags: Iterable<string>) {
return [...tags].map(x => pfx + x).join('\u2003'); // em space
}
const plus = disp('+\u2009', reqTags); // thin space
const minus = disp('-\u2009', excTags);
document.getElementById('filters-details')!.dataset.filters =
`${plus}\u2003${minus}`.trim();
}
function update() {
updateItems();
history.pushState(null, "", makeFragment());
}
function converseId(id: string) {
if (id.match(/^require/)) {
return id.replace('require', 'exclude');
} else {
return id.replace('exclude', 'require');
}
}
function toggle(checkbox: HTMLInputElement) {
if (checkbox.checked) {
const converse = document.getElementById(converseId(checkbox.id)) as HTMLInputElement;
converse.checked = false;
}
update();
}
function clearForm() {
allBoxes.forEach(b => b.checked = b.defaultChecked);
}
function clear(e: Event) {
clearForm();
update();
e.preventDefault();
}
function toggleSingles(e: Event) {
showSingles = !showSingles;
const elems = Array.from(document.querySelectorAll('.filterlist li')) as HTMLElement[];
for (const li of elems) {
const countStr = li.querySelector('label')?.dataset.count;
const count = countStr ? +countStr : 0;
if (count <= 1 && li instanceof HTMLElement) {
li.hidden = !showSingles;
}
}
e.preventDefault();
}
function makeFragment() {
const allBoxesArr = Array.from(allBoxes);
const ids = allBoxesArr.filter(b => b.checked).map(b => b.id);
if (ids.length == 0) {
return '#all';
} else if (allBoxesArr.every(b => b.checked == b.defaultChecked)) {
return '#';
} else {
return '#' + ids.join(';');
}
}
function useFragment() {
const frag = decodeURIComponent(location.hash).replace(/^#/, '');
const details = document.getElementById('filters-details') as HTMLDetailsElement;
if (!frag) {
clearForm();
} else if (frag == 'all') {
allBoxes.forEach(b => b.checked = false);
details.open = false;
} else {
const pieces =
frag == 'summary' ? ['require_artsummary'] :
frag == 'colourexamples' ? ['require_colourexample'] :
frag == 'flatexamples' ? ['require_flatexample'] :
frag == 'sketchexamples' ? ['require_sketchexample'] :
frag == 'iconexamples' ? ['require_iconexample'] :
frag == 'curated' ? ['require_curated'] : frag.split(';');
const set = new Set(pieces);
const re = /^(require|exclude)_|hide_filters/;
if (pieces.every(x => re.test(x))) {
allBoxes.forEach(b => b.checked = set.has(b.id));
}
}
updateItems();
}
function sortFilters(cmp: (a: Node, b: Node) => number) {
function sort1(id: string) {
const elt = document.getElementById(id);
if (elt === null) return;
const children = Array.from(elt.childNodes);
children.sort(cmp);
for (const c of children) {
elt.removeChild(c);
elt.appendChild(c);
}
}
sort1('require');
sort1('exclude');
}
function sortFiltersAlpha(e: Event) {
function getName(node: Node): string {
if (node instanceof Element) {
return node.getElementsByTagName('input')[0].value;
} else {
return '';
}
}
sortFilters((a, b) => getName(a).localeCompare(getName(b)));
e.preventDefault();
}
function sortFiltersUses(e: Event) {
function getUses(node: Node) {
if (node instanceof Element) {
const countStr = node.getElementsByTagName('label')[0].dataset.count;
return countStr ? +countStr : 0;
} else {
return 0;
}
}
sortFilters((a, b) => getUses(b) - getUses(a));
e.preventDefault();
}
function setup() {
function inputs(id: string): Boxes {
const iter = document.getElementById(id)!.getElementsByTagName('input');
return new Set(Array.from(iter));
}
const items = Array.from(document.getElementsByClassName('post')) as HTMLElement[];
itemsByYear = new Map;
for (const item of items) {
const year = item.dataset.year;
if (year !== undefined) {
if (!itemsByYear.has(year)) {
itemsByYear.set(year, new Set([item]));
} else {
itemsByYear.get(year)?.add(item);
}
}
}
reqBoxes = inputs('require');
excBoxes = inputs('exclude');
allBoxes = new Set([...reqBoxes, ...excBoxes]);
tags = new Map(items.map(item => [item, item.dataset.tags?.split(';') ?? []]));
allBoxes.forEach(b => b.addEventListener('change', () => toggle(b)));
function addClick(id: string, f: (e: Event) => void) {
document.getElementById(id)!.addEventListener('click', f);
}
addClick('clear', clear);
addClick('sortalpha', sortFiltersAlpha);
addClick('sortuses', sortFiltersUses);
addClick('singles', toggleSingles);
window.addEventListener('popstate', useFragment);
useFragment();
}
window.addEventListener('DOMContentLoaded', setup);
export {};

View file

@ -1,38 +0,0 @@
(function () {
'use strict';
let nsfwOk = 'nsfw-ok';
function alreadyYes() {
return localStorage.getItem(nsfwOk);
}
function dismiss() {
let dialog = document.getElementById('nsfw-dialog');
dialog.parentElement.removeChild(dialog);
}
function yes() {
localStorage.setItem(nsfwOk, 1);
dismiss();
}
// now just a normal link
/*
function no() {
document.location = '//crouton.net';
}
*/
function setup() {
if (alreadyYes()) {
dismiss();
} else {
document.getElementById('nsfw-yes').onclick = yes;
// document.getElementById('nsfw-no').onclick = no;
}
}
document.addEventListener('DOMContentLoaded', setup);
})();

28
script/nsfw-warning.ts Normal file
View file

@ -0,0 +1,28 @@
const nsfwOk = 'nsfw-ok';
function alreadyYes() {
return localStorage.getItem(nsfwOk) == '1';
}
function dismiss() {
const dialog = document.getElementById('nsfw-dialog')!;
dialog.parentElement?.removeChild(dialog);
}
function yes() {
localStorage.setItem(nsfwOk, '1');
dismiss();
}
function setup() {
if (alreadyYes()) {
dismiss();
} else {
document.getElementById('nsfw-yes')!.onclick = yes;
// nsfw-no is a normal link
}
}
document.addEventListener('DOMContentLoaded', setup);
export {};

View file

@ -1,110 +0,0 @@
(function() {
'use strict';
let mainfig;
let mainimg;
let mainlink;
let altButtons;
let skipAll;
let opened = new Set;
function openCW(id, caption, focusLink = false) {
if (id) opened.add(id);
mainfig.removeChild(caption);
mainlink.tabIndex = 0;
if (focusLink) mainlink.focus();
}
function addCWListeners(id, caption) {
if (caption) {
caption.addEventListener('click', e => openCW(id, caption));
caption.addEventListener('keyup',
e => { if (e.key == 'Enter') openCW(id, caption, true) });
}
}
function setImage(id, src, href, cw) {
let caption = document.getElementById('cw');
let newCaption;
let checked = skipAll ? skipAll.checked : false;
if (!checked && !opened.has(id) && cw) {
newCaption = document.getElementById('cw-template')
.content.firstElementChild.cloneNode(true);
newCaption.querySelector('#cw-text').innerHTML = cw;
addCWListeners(id, newCaption);
}
if (caption) {
openCW(null, caption);
}
if (newCaption) {
mainfig.insertBefore(newCaption, mainlink);
mainlink.tabIndex = -1;
}
mainimg.src = src;
mainlink.href = href;
}
function activateButton(button, doPush = true) {
setImage(button.id, button.value,
button.dataset.link, button.dataset.warning);
if (doPush) history.pushState(null, '', '#' + button.id);
}
function useFragment(firstLoad = false) {
let button = altButtons[0];
let frag = decodeURIComponent(location.hash).replace(/^#/, '');
if (frag) {
let selected = document.getElementById(frag);
if (selected) button = selected;
}
let id;
if (button) {
id = button.id;
button.checked = true;
activateButton(button, false);
}
if (firstLoad) addCWListeners(id, document.getElementById('cw'));
}
function setup() {
mainfig = document.getElementById('mainfig');
mainimg = document.getElementById('mainimg');
mainlink = document.getElementById('mainlink');
skipAll = document.getElementById('skipAll');
let alts = document.getElementById('alts');
if (alts) {
let inputs = Array.from(alts.getElementsByTagName('input'));
altButtons = inputs.filter(e => e.name == 'variant');
} else {
altButtons = [];
}
for (let button of altButtons) {
button.onchange = e => { if (button.checked) activateButton(button); };
}
if (skipAll) {
skipAll.onchange = e => { if (skipAll.checked) {
let caption = document.getElementById('cw');
if (caption) { openCW(null, caption, false); }
} };
}
window.addEventListener('popstate', e => useFragment());
useFragment(true);
}
window.addEventListener('DOMContentLoaded', setup);
})();

109
script/single.ts Normal file
View file

@ -0,0 +1,109 @@
let mainfig: HTMLElement;
let mainimg: HTMLImageElement;
let mainlink: HTMLAnchorElement;
let altButtons: HTMLInputElement[];
let skipAll: HTMLInputElement;
const opened: Set<string> = new Set;
function openCW(id: string | null, caption: HTMLElement, focusLink = false): void {
if (id !== null) opened.add(id);
if (caption.parentElement) {
mainfig.removeChild(caption);
}
mainlink.tabIndex = 0;
if (focusLink) mainlink.focus();
}
function addCWListeners(id: string | null, caption: HTMLElement): void {
caption.addEventListener('click', _e => openCW(id, caption));
caption.addEventListener('keyup',
e => { if (e.key == 'Enter') openCW(id, caption, true) });
}
function setImage(id: string, src: string, href: string, cw: string): void {
const caption = document.getElementById('cw');
const checked = skipAll ? skipAll.checked : false;
if (!checked && !opened.has(id) && cw) {
const template = document.getElementById('cw-template') as HTMLTemplateElement;
const newCaption = template.content.firstElementChild!.cloneNode(true) as HTMLElement;
newCaption.querySelector('#cw-text')!.innerHTML = cw;
if (caption) openCW(null, caption);
mainfig.insertBefore(newCaption, mainlink);
mainlink.tabIndex = -1;
addCWListeners(id, newCaption);
} else {
if (caption) openCW(null, caption);
}
mainimg.src = src;
mainlink.href = href;
}
function activateButton(button: HTMLInputElement, doPush = true): void {
setImage(button.id, button.value,
button.dataset.link!,
button.dataset.warning!);
if (doPush) history.pushState(null, '', '#' + button.id);
}
function useFragment(firstLoad = false): void {
let button = altButtons[0];
const frag = decodeURIComponent(location.hash).replace(/^#/, '');
if (frag) {
const selected = document.getElementById(frag) as HTMLInputElement;
if (selected) button = selected;
}
let id: string | null = null;
if (button) {
id = button.id;
button.checked = true;
activateButton(button, false);
}
if (firstLoad) {
const cw = document.getElementById('cw');
if (cw) addCWListeners(id, cw);
}
}
function setup() {
mainfig = document.getElementById('mainfig')!;
mainimg = document.getElementById('mainimg') as HTMLImageElement;
mainlink = document.getElementById('mainlink') as HTMLAnchorElement;
skipAll = document.getElementById('skipAll') as HTMLInputElement;
const alts = document.getElementById('alts');
if (alts) {
const inputs = Array.from(alts.getElementsByTagName('input'));
altButtons = inputs.filter(e => e.name == 'variant');
} else {
altButtons = [];
}
for (const button of altButtons) {
button.onchange = _e => { if (button.checked) activateButton(button); };
}
if (skipAll) {
skipAll.onchange = _e => { if (skipAll.checked) {
const caption = document.getElementById('cw');
if (caption) { openCW(null, caption, false); }
} };
}
window.addEventListener('popstate', _e => useFragment());
useFragment(true);
}
window.addEventListener('DOMContentLoaded', setup);
export {};

View file

@ -17,12 +17,11 @@
/* bright colours from yummy.cricket bg */ /* bright colours from yummy.cricket bg */
--gradient: --gradient:
linear-gradient(135deg, linear-gradient(135deg,
hsl(42deg, 92%, 70%), oklch(93% 0.16 86),
hsl(348deg, 92%, 70%), oklch(84% 0.17 15),
hsl(334deg, 100%, 80%), oklch(78% 0.18 304),
hsl(234deg, 100%, 76%), oklch(78% 0.18 233),
hsl(195deg, 100%, 67%), oklch(78% 0.18 162)
hsl(155deg, 70%, 62%)
); );
--text-col: white; --text-col: white;
@ -45,7 +44,6 @@
--button-radius: 1000px; --button-radius: 1000px;
font-family: Muller; font-family: Muller;
font-size: x-large;
font-weight: 600; font-weight: 600;
background: var(--gradient) fixed; background: var(--gradient) fixed;
@ -241,7 +239,7 @@ del {
.threecol { .threecol {
columns: 3; columns: 3;
column-gap: 2em; grid-column-gap: 2em;
} }
.threecol dd { .threecol dd {
@ -257,12 +255,11 @@ del {
:root { :root {
--gradient: --gradient:
linear-gradient(135deg, linear-gradient(135deg,
hsl(42deg, 37%, 20%), /* oklch(33% 0.16 86), */
hsl(348deg, 37%, 20%), oklch(30% 0.17 15),
hsl(334deg, 42%, 20%), oklch(30% 0.18 304),
hsl(234deg, 67%, 18%), oklch(30% 0.18 233),
hsl(195deg, 37%, 15%), oklch(30% 0.18 162)
hsl(155deg, 32%, 15%)
); );
--text-col: hsl(55deg, 60%, 90%); --text-col: hsl(55deg, 60%, 90%);

View file

@ -23,7 +23,7 @@
display: grid; display: grid;
grid-template-columns: 15% auto; grid-template-columns: 15% auto;
align-items: baseline; align-items: baseline;
gap: 0.75em; grid-gap: 0.75em;
} }
#filters h3 { #filters h3 {
@ -42,15 +42,22 @@
font-weight: 500; font-weight: 500;
font-size: 90%; font-size: 90%;
gap: 0.5em; grid-column-gap: 0.5em;
} }
.filterlist input { .filterlist input {
appearance: none; /* i can't believe it's not appearance: none */
clip: rect(0 0 0 0);
clip-path: inset(100%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
} }
.filterlist li:not([hidden]) { .filterlist li {
display: block; list-style: none;
} }
.filterlist li:focus-within { .filterlist li:focus-within {
@ -101,7 +108,7 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
margin-top: 0; margin-top: 0;
gap: 2em; grid-gap: 2em;
} }
#filterstuff li { #filterstuff li {
@ -138,7 +145,7 @@
#filterstuff { #filterstuff {
grid-area: unset; grid-area: unset;
flex-flow: column; flex-flow: column;
gap: 0.2em; grid-column-gap: 0.2em;
} }
#filterstuff li { #filterstuff li {
@ -151,7 +158,7 @@
padding: 0; padding: 0;
display: grid; display: grid;
grid: auto-flow / repeat(auto-fit, var(--image-size)); grid: auto-flow / repeat(auto-fit, var(--image-size));
gap: var(--gap); grid-gap: var(--gap);
justify-content: center; justify-content: center;
} }
@ -167,8 +174,6 @@
} }
.item:not(.year-marker) { .item:not(.year-marker) {
box-shadow: var(--text-shadow);
outline: var(--border-thickness) solid var(--border-col);
background: hsl(0, 0%, 0%, 50%); background: hsl(0, 0%, 0%, 50%);
clip-path: polygon(5% 0, 95% 10%, 95% 100%, 5% 90%); clip-path: polygon(5% 0, 95% 10%, 95% 100%, 5% 90%);
} }
@ -190,7 +195,7 @@
--gap: 0.2em; --gap: 0.2em;
display: grid; display: grid;
grid-template-columns: repeat(2, calc(50% - 3 * var(--gap))); grid-template-columns: repeat(2, calc(50% - 3 * var(--gap)));
gap: var(--gap); grid-gap: var(--gap);
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 100%; height: 100%;
@ -214,6 +219,8 @@
position: absolute; position: absolute;
mix-blend-mode: hard-light; mix-blend-mode: hard-light;
pointer-events: none;
} }
.item.nsfw::before { .item.nsfw::before {

View file

@ -62,12 +62,12 @@
.list { .list {
display: flex; display: grid;
flex-wrap: wrap; grid-template-columns: repeat(2, auto);
align-items: center; align-items: center;
justify-content: space-evenly; justify-content: space-evenly;
padding: 0; padding: 0;
gap: 1.5em; grid-gap: 1.5em;
font-size: 175%; font-size: 175%;
} }
@ -93,7 +93,7 @@ main {
.list { .list {
font-size: 300%; font-size: 300%;
grid-template-columns: 100%; grid-template-columns: 100%;
gap: 1em; grid-gap: 1em;
} }
} }

View file

@ -20,7 +20,7 @@
"icon text" "icon text"
"buttons buttons" "buttons buttons"
/ 1fr 3fr; / 1fr 3fr;
gap: 0.5em; grid-gap: 0.5em;
align-items: center; align-items: center;
min-height: 20vh; min-height: 20vh;

View file

@ -64,6 +64,7 @@ body {
padding: 0.25em 0.5em; padding: 0.25em 0.5em;
border-radius: 0.5em; border-radius: 0.5em;
} }
#cw-text b { font-weight: 900; }
#cw-text::before { #cw-text::before {
content: 'cw: '; content: 'cw: ';
font-weight: 600; font-weight: 600;
@ -91,7 +92,7 @@ body {
#info { #info {
display: grid; display: grid;
grid-template-columns: min-content auto; grid-template-columns: min-content auto;
column-gap: 1em; grid-column-gap: 1em;
align-items: baseline; align-items: baseline;
} }
@ -144,7 +145,7 @@ body {
#updates dl { #updates dl {
display: grid; display: grid;
grid-template-columns: min-content auto; grid-template-columns: min-content auto;
gap: 0.5em; grid-gap: 0.5em;
align-items: baseline; align-items: baseline;
} }
@ -183,7 +184,7 @@ footer {
margin: 1.5em 0; margin: 1.5em 0;
text-align: center; text-align: center;
display: grid; display: grid;
gap: 0.5em; grid-gap: 0.5em;
grid-template-columns: minmax(auto, 10em) auto minmax(auto, 10em); grid-template-columns: minmax(auto, 10em) auto minmax(auto, 10em);
} }
@ -214,11 +215,64 @@ footer {
grid-area: 1 / 3 / auto / auto; grid-area: 1 / 3 / auto / auto;
} }
:is(#tags, #links) ul { #tags ul, #links ul {
padding: 0; padding: 0;
display: flex;
flex-flow: wrap row;
} }
:is(#tags, #links) li { #tags li, #links li {
display: inline; display: inline;
margin-right: 0.75em; margin-right: 0.75em;
white-space: nowrap;
} }
.chost, .toot {
--avatar-size: 4em;
display: grid;
grid-template: 'avatar user' 1lh
'avatar post' auto / var(--avatar-size) 1fr;
grid-gap: 5px 15px;
margin: 0;
}
:is(.chost, .toot) .user {
grid-area: user;
font-weight: 800;
}
:is(.chost, .toot) :is(.user a, a.user) {
font-weight: 800;
text-decoration: none;
}
:is(.chost, .toot) .username {
margin-left: 1em;
font-size: 90%;
font-weight: 500;
}
:is(.chost, .toot) .post { grid-area: post; }
:is(.chost, .toot) .avatar { grid-area: avatar; }
:is(.chost, .toot) :is(.avatar img, img.avatar) {
height: var(--avatar-size); width: var(--avatar-size);
border-radius: 1000em;
}
.chost .squircle {
mask-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTAwIDBDMjAgMCAwIDIwIDAgMTAwczIwIDEwMCAxMDAgMTAwIDEwMC0yMCAxMDAtMTAwUzE4MCAwIDEwMCAweiIvPjwvc3ZnPg==);
mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
border-radius: 0;
}
:is(.chost, .toot) h1 {
margin: 0 0 0.5em;
font-size: 150%;
font-weight: 700;
}
:is(.chost, .toot) + :is(.chost, .toot) { margin-top: 1.5em; }

8
tsconfig.json Normal file
View file

@ -0,0 +1,8 @@
{
"compilerOptions": {
"strict": true,
"noEmitOnError": true,
"lib": ["ES2021", "dom"],
"target": "ES2015"
}
}