Compare commits

..

No commits in common. "8daa6fa09ff87529170a43888d43a4984a55ec9b" and "11f5029412d56917ac0ebf81d6776b6dc7c56d41" have entirely different histories.

21 changed files with 464 additions and 542 deletions

View file

@ -16,24 +16,22 @@ MAKEPAGES := $(TMPDIR)/make-pages
YAMLS != find -L $(DATADIR) -name $(INFONAME) YAMLS != find -L $(DATADIR) -name $(INFONAME)
TSCRIPTS != find script -name '*.ts' SCRIPTS != find script -name '*.js'
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 = $(STYLES) $(STYLEPNGS) $(STYLESVGS) $(FONTS) STATIC = $(SCRIPTS) $(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) $(BSCRIPTS) build: $(BUILDDIR)/index.html $(BSTATIC)
$(BUILDDIR)/index.html: $(DATADIR)/index.yaml $(MAKEPAGES) $(BUILDDIR)/index.html: $(DATADIR)/index.yaml $(MAKEPAGES)
echo "[index] "$@ echo "[index] "$@
@ -45,15 +43,9 @@ $(BUILDDIR)/%: %
$(call copy,--link --force) $(call copy,--link --force)
$(BUILDDIR)/%: $(TMPDIR)/% $(BUILDDIR)/%: $(TMPDIR)/%
$(call copy,--link --force) $(call copy,--link)
$(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)
@ -78,10 +70,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),>,-quality 90) $(call resize,$(MEDW),$(MEDH),>)
$(TMPDIR)/%_big.webp: $(DATADIR)/%.webp $(TMPDIR)/%_big.webp: $(DATADIR)/%.webp
$(call copy) $(call resize,$(BIG),$(BIG),>)
$(MAKEPAGES): make-pages/*.hs make-pages/make-pages.cabal $(MAKEPAGES): make-pages/*.hs make-pages/make-pages.cabal
@ -148,7 +140,7 @@ endef
define resize define resize
echo "[resize] "$@ echo "[resize] "$@
mkdir -p "$(dir $@)" mkdir -p "$(dir $@)"
convert -resize "$(1)x$(2)$(3)" -define webp:lossless=true $(4) "$^" "$@" convert -resize "$(1)x$(2)$(3)" $(4) "$^" "$@"
endef endef
# no args # no args

View file

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

View file

@ -1,3 +1,4 @@
{-# LANGUAGE TransformListComp #-}
module GalleryPage (make) where module GalleryPage (make) where
import BuilderQQ import BuilderQQ
@ -7,15 +8,15 @@ import qualified NsfwWarning
import Control.Monad import Control.Monad
import Data.Foldable import Data.Foldable
import Data.Function ((&)) import Data.Function (on, (&))
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, sort, sortOn) import Data.List (intersperse, groupBy, sortBy, sort)
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
@ -41,7 +42,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 type=module></script> <script src=/script/gallery.js></script>
$0.nsfwScript $0.nsfwScript
<title>$title</title> <title>$title</title>
@ -89,11 +90,15 @@ 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 :: [(Int, [(FilePath, Info)])] infosByYear =
infosByYear = infos & [(the year, infopath) |
filter (not . #unlisted . snd) & infopath@(_, info) <- infos,
sortOn (Down . compareKeyFor nsfw . snd) & not $ #unlisted info,
groupOnKey (\(_, i) -> #latestYear i nsfw) then sortInfo by info,
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)) "..")
@ -109,20 +114,13 @@ make' root (GalleryInfo {title, desc, prefix, filters, hidden}) infos = [b|@0
url = [b|$root/$prefix|] url = [b|$root/$prefix|]
imagepath0 imagepath0
| (_, (p, i) : _) : _ <- infosByYear = getThumb (takeDirectory p) i | (_, (p0, i0) : _) : _ <- infosByYear = getThumb (takeDirectory p0) i0
| 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,
CompareKey (..), compareKeyFor, compareFor, sortFor, 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,8 +163,7 @@ 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 where relDate (date, us) = date <$ guard (not $ any #ignoreSort us)
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
@ -225,14 +224,8 @@ 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 $ compareKeyFor nsfw compareFor nsfw = comparing \i -> (#latestDate i nsfw, #sortEx i, #title i)
sortFor :: Bool -> [Info] -> [Info] sortFor :: Bool -> [Info] -> [Info]
sortFor = sortBy . compareFor sortFor = sortBy . compareFor

View file

@ -1,4 +1,4 @@
{-# LANGUAGE CPP, ImplicitParams #-} {-# LANGUAGE CPP, ImplicitParams, TypeApplications #-}
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 type=module></script>|] script (Just _) = [b|<script src=/script/nsfw-warning.js></script>|]
dialog :: Maybe What -> Builder dialog :: Maybe What -> Builder
dialog Nothing = "" dialog Nothing = ""

View file

@ -1,4 +1,5 @@
{-# 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,6 +45,8 @@ 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
@ -56,12 +58,11 @@ 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 let updates = sort $ updatesFor nsfw info
let updates = sort $ updatesFor nsfw info let updatesList = makeUpdates updates
let updatesList = makeUpdates updates
let makePrefetch (Image {path}) = [b|<link rel=prefetch href=$path'>|] let makePrefetch (Image {path}) = [b|<link rel=prefetch href=$path'>|]
where path' = bigFile path where path' = bigFile path
@ -135,11 +136,12 @@ 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 type=module></script> <script src=/script/single.js></script>
$nsfwScript $nsfwScript
$bgStyle $bgStyle
@ -156,6 +158,7 @@ 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>
@ -171,8 +174,6 @@ 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
@ -192,14 +193,9 @@ 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 :: Maybe Artist -> Builder makeArtist :: Artist -> Builder
makeArtist Nothing = "" makeArtist (Artist {name, url}) =
makeArtist (Just (Artist {name, url})) = [b|@0 [b|by $artistLink <br>|]
<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>|]
@ -249,10 +245,9 @@ makeButtonBar title images =
| [(_, imgs)] <- cats -> | [(_, imgs)] <- cats ->
makeButtonBar title (Uncat imgs) makeButtonBar title (Uncat imgs)
| otherwise -> | otherwise ->
makeNav "cat" $ map (uncurry makeCat) cats makeNav "cat" $ map (uncurry makeCat) cats
where where
makeNav :: CanBuild b => Text -> b -> Builder makeNav (cls :: Text) inner = [b|@0
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
@ -276,7 +271,8 @@ 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,36 +26,53 @@ executable make-pages
RSS, RSS,
ListTags, ListTags,
Options Options
default-language: GHC2024 default-language: Haskell2010
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,
TemplateHaskell MultiParamTypeClasses,
ScopedTypeVariables,
TemplateHaskell,
TransformListComp,
TypeApplications
build-depends: build-depends:
base >= 4.16.4 && < 4.21, base ^>= 4.16.4,
bytestring >= 0.11.3.1 && < 0.14, bytestring ^>= 0.11.3.1,
containers >= 0.6.0.1 && < 0.8, containers ^>= 0.6.0.1,
filemanip ^>= 0.3.6.3, filemanip ^>= 0.3.6.3,
filepath >= 1.4.2.1 && < 1.6, filepath ^>= 1.4.2.1,
hashable >= 1.3.0.0 && < 1.5, hashable ^>= 1.3.0.0,
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 && < 2.23, template-haskell ^>= 2.18.0.0,
text >= 1.2.3.1 && < 2.2, text ^>= 1.2.3.1,
time >= 1.8.0.2 && < 1.13, time >= 1.8.0.2 && < 1.10,
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

204
script/gallery.js Normal file
View file

@ -0,0 +1,204 @@
(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);
})();

View file

@ -1,224 +0,0 @@
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 {};

38
script/nsfw-warning.js Normal file
View file

@ -0,0 +1,38 @@
(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);
})();

View file

@ -1,28 +0,0 @@
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 {};

110
script/single.js Normal file
View file

@ -0,0 +1,110 @@
(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);
})();

View file

@ -1,109 +0,0 @@
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,11 +17,12 @@
/* bright colours from yummy.cricket bg */ /* bright colours from yummy.cricket bg */
--gradient: --gradient:
linear-gradient(135deg, linear-gradient(135deg,
oklch(93% 0.16 86), hsl(42deg, 92%, 70%),
oklch(84% 0.17 15), hsl(348deg, 92%, 70%),
oklch(78% 0.18 304), hsl(334deg, 100%, 80%),
oklch(78% 0.18 233), hsl(234deg, 100%, 76%),
oklch(78% 0.18 162) hsl(195deg, 100%, 67%),
hsl(155deg, 70%, 62%)
); );
--text-col: white; --text-col: white;
@ -44,6 +45,7 @@
--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;
@ -239,7 +241,7 @@ del {
.threecol { .threecol {
columns: 3; columns: 3;
grid-column-gap: 2em; column-gap: 2em;
} }
.threecol dd { .threecol dd {
@ -255,11 +257,12 @@ del {
:root { :root {
--gradient: --gradient:
linear-gradient(135deg, linear-gradient(135deg,
/* oklch(33% 0.16 86), */ hsl(42deg, 37%, 20%),
oklch(30% 0.17 15), hsl(348deg, 37%, 20%),
oklch(30% 0.18 304), hsl(334deg, 42%, 20%),
oklch(30% 0.18 233), hsl(234deg, 67%, 18%),
oklch(30% 0.18 162) hsl(195deg, 37%, 15%),
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;
grid-gap: 0.75em; gap: 0.75em;
} }
#filters h3 { #filters h3 {
@ -42,22 +42,15 @@
font-weight: 500; font-weight: 500;
font-size: 90%; font-size: 90%;
grid-column-gap: 0.5em; gap: 0.5em;
} }
.filterlist input { .filterlist input {
/* i can't believe it's not appearance: none */ 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 { .filterlist li:not([hidden]) {
list-style: none; display: block;
} }
.filterlist li:focus-within { .filterlist li:focus-within {
@ -108,7 +101,7 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
margin-top: 0; margin-top: 0;
grid-gap: 2em; gap: 2em;
} }
#filterstuff li { #filterstuff li {
@ -145,7 +138,7 @@
#filterstuff { #filterstuff {
grid-area: unset; grid-area: unset;
flex-flow: column; flex-flow: column;
grid-column-gap: 0.2em; gap: 0.2em;
} }
#filterstuff li { #filterstuff li {
@ -158,7 +151,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));
grid-gap: var(--gap); gap: var(--gap);
justify-content: center; justify-content: center;
} }
@ -174,6 +167,8 @@
} }
.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%);
} }
@ -195,7 +190,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)));
grid-gap: var(--gap); gap: var(--gap);
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 100%; height: 100%;
@ -219,8 +214,6 @@
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: grid; display: flex;
grid-template-columns: repeat(2, auto); flex-wrap: wrap;
align-items: center; align-items: center;
justify-content: space-evenly; justify-content: space-evenly;
padding: 0; padding: 0;
grid-gap: 1.5em; 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%;
grid-gap: 1em; gap: 1em;
} }
} }

View file

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

View file

@ -64,7 +64,6 @@ 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;
@ -92,7 +91,7 @@ body {
#info { #info {
display: grid; display: grid;
grid-template-columns: min-content auto; grid-template-columns: min-content auto;
grid-column-gap: 1em; column-gap: 1em;
align-items: baseline; align-items: baseline;
} }
@ -145,7 +144,7 @@ body {
#updates dl { #updates dl {
display: grid; display: grid;
grid-template-columns: min-content auto; grid-template-columns: min-content auto;
grid-gap: 0.5em; gap: 0.5em;
align-items: baseline; align-items: baseline;
} }
@ -184,7 +183,7 @@ footer {
margin: 1.5em 0; margin: 1.5em 0;
text-align: center; text-align: center;
display: grid; display: grid;
grid-gap: 0.5em; gap: 0.5em;
grid-template-columns: minmax(auto, 10em) auto minmax(auto, 10em); grid-template-columns: minmax(auto, 10em) auto minmax(auto, 10em);
} }
@ -215,64 +214,11 @@ footer {
grid-area: 1 / 3 / auto / auto; grid-area: 1 / 3 / auto / auto;
} }
#tags ul, #links ul { :is(#tags, #links) ul {
padding: 0; padding: 0;
display: flex;
flex-flow: wrap row;
} }
#tags li, #links li { :is(#tags, #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; }

View file

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