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)
SCRIPTS != find script -name '*.js'
TSCRIPTS != find script -name '*.ts'
STYLES != find style -name '*.css'
STYLESVGS != find style -name '*.svg'
STYLEPNGS != find style -name '*.png'
FONTS != find fonts \
-iname '*.eot' -or -iname '*.svg' -or \
-iname '*.ttf' -or -iname '*.woff' -or \
-iname '*.woff2' -or -iname '*.css'
STATIC = $(SCRIPTS) $(STYLES) $(STYLEPNGS) $(STYLESVGS) $(FONTS)
BSTATIC = $(patsubst %,$(BUILDDIR)/%,$(STATIC))
STATIC = $(STYLES) $(STYLEPNGS) $(STYLESVGS) $(FONTS)
BSTATIC = $(patsubst %,$(BUILDDIR)/%,$(STATIC))
BSCRIPTS = $(patsubst %.ts,$(BUILDDIR)/%.js,$(TSCRIPTS))
.PHONY: all build
all: build
build: $(BUILDDIR)/index.html $(BSTATIC)
build: $(BUILDDIR)/index.html $(BSTATIC) $(BSCRIPTS)
$(BUILDDIR)/index.html: $(DATADIR)/index.yaml $(MAKEPAGES)
echo "[index] "$@
@ -43,9 +45,15 @@ $(BUILDDIR)/%: %
$(call copy,--link --force)
$(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
$(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)
$(TMPDIR)/%_med.webp: $(DATADIR)/%.webp
$(call resize,$(MEDW),$(MEDH),>)
$(call resize,$(MEDW),$(MEDH),>,-quality 90)
$(TMPDIR)/%_big.webp: $(DATADIR)/%.webp
$(call resize,$(BIG),$(BIG),>)
$(call copy)
$(MAKEPAGES): make-pages/*.hs make-pages/make-pages.cabal
@ -140,7 +148,7 @@ endef
define resize
echo "[resize] "$@
mkdir -p "$(dir $@)"
convert -resize "$(1)x$(2)$(3)" $(4) "$^" "$@"
convert -resize "$(1)x$(2)$(3)" -define webp:lossless=true $(4) "$^" "$@"
endef
# no args

View file

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

View file

@ -1,4 +1,3 @@
{-# LANGUAGE TransformListComp #-}
module GalleryPage (make) where
import BuilderQQ
@ -8,15 +7,15 @@ import qualified NsfwWarning
import Control.Monad
import Data.Foldable
import Data.Function (on, (&))
import Data.Function ((&))
import qualified Data.HashMap.Strict as HashMap
import Data.HashSet (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.Ord (Down (..))
import qualified Data.Text.Lazy as Lazy
import System.FilePath (takeDirectory, joinPath, splitPath)
import GHC.Exts (Down (..), the)
make :: Text -> GalleryInfo -> [(FilePath, Info)] -> Lazy.Text
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'>
<script src=/script/gallery.js></script>
<script src=/script/gallery.js type=module></script>
$0.nsfwScript
<title>$title</title>
@ -90,15 +89,11 @@ make' root (GalleryInfo {title, desc, prefix, filters, hidden}) infos = [b|@0
where
items = map (uncurry $ makeYearItems nsfw) infosByYear
infosByYear =
[(the year, infopath) |
infopath@(_, info) <- infos,
not $ #unlisted info,
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)
infosByYear :: [(Int, [(FilePath, Info)])]
infosByYear = infos &
filter (not . #unlisted . snd) &
sortOn (Down . compareKeyFor nsfw . snd) &
groupOnKey (\(_, i) -> #latestYear i nsfw)
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|]
imagepath0
| (_, (p0, i0) : _) : _ <- infosByYear = getThumb (takeDirectory p0) i0
| (_, (p, i) : _) : _ <- infosByYear = getThumb (takeDirectory p) i
| otherwise = "/style/card.png"
nsfw' = NsfwWarning.Gallery <$ guard nsfw
nsfwScript = NsfwWarning.script 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 prefix initial tag count = [b|@0
<li$hidden>

View file

@ -4,7 +4,7 @@
module Info
(Info (..),
tagsFor, descFor, imagesFor, linksFor, updatesFor, lastUpdate,
compareFor, sortFor,
CompareKey (..), compareKeyFor, compareFor, sortFor,
Artist (..), Images' (..), Images, Image (..), Desc (..), DescField (..),
Link (..), Update (..), Bg (..),
GalleryInfo (..), GalleryFilters (..), ArtistFilter (..), NsfwFilter (..),
@ -163,7 +163,8 @@ instance HasField "notMine" Info Bool where getField = isJust . #artist
instance HasField "latestDate" Info (Bool -> Date) where
getField info@(Info {date=date}) nsfw =
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
getField info nsfw = #year $ #latestDate info nsfw
@ -224,8 +225,14 @@ lastUpdate :: Bool -> Info -> Maybe Date
lastUpdate nsfw info =
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 nsfw = comparing \i -> (#latestDate i nsfw, #sortEx i, #title i)
compareFor nsfw = comparing $ compareKeyFor nsfw
sortFor :: Bool -> [Info] -> [Info]
sortFor = sortBy . compareFor

View file

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

View file

@ -12,7 +12,7 @@ instance CanBuild What where
script :: Maybe What -> Builder
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 Nothing = ""

View file

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

View file

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

View file

@ -3,8 +3,8 @@ name: make-pages
version: 0.1.0
license: AGPL-3.0-or-later
author: Rhiannon Morris <rhi@rhiannon.website>
maintainer: Rhiannon Morris <rhi@rhiannon.website>
author: rhiannon morris <rhi@rhiannon.website>
maintainer: rhiannon morris <rhi@rhiannon.website>
flag pretty-verbose
description: pretty-print the verbose output
@ -26,53 +26,36 @@ executable make-pages
RSS,
ListTags,
Options
default-language: Haskell2010
default-language: GHC2024
default-extensions:
BlockArguments,
ConstraintKinds,
DataKinds,
DeriveAnyClass,
DeriveTraversable,
DerivingStrategies,
DerivingVia,
DuplicateRecordFields,
FlexibleContexts,
FlexibleInstances,
GeneralizedNewtypeDeriving,
LambdaCase,
NamedFieldPuns,
OverloadedLabels,
OverloadedLists,
OverloadedStrings,
PatternSynonyms,
QuasiQuotes,
RankNTypes,
ScopedTypeVariables,
StandaloneDeriving,
TupleSections,
TypeSynonymInstances,
ViewPatterns
other-extensions:
PatternSynonyms,
CPP,
ImplicitParams,
MultiParamTypeClasses,
ScopedTypeVariables,
TemplateHaskell,
TransformListComp,
TypeApplications
TemplateHaskell
build-depends:
base ^>= 4.16.4,
bytestring ^>= 0.11.3.1,
containers ^>= 0.6.0.1,
base >= 4.16.4 && < 4.21,
bytestring >= 0.11.3.1 && < 0.14,
containers >= 0.6.0.1 && < 0.8,
filemanip ^>= 0.3.6.3,
filepath ^>= 1.4.2.1,
hashable ^>= 1.3.0.0,
filepath >= 1.4.2.1 && < 1.6,
hashable >= 1.3.0.0 && < 1.5,
HsYAML ^>= 0.2.1.0,
optparse-applicative ^>= 0.15.1.0,
process ^>= 1.6.8.2,
template-haskell ^>= 2.18.0.0,
text ^>= 1.2.3.1,
time >= 1.8.0.2 && < 1.10,
template-haskell >= 2.18.0.0 && < 2.23,
text >= 1.2.3.1 && < 2.2,
time >= 1.8.0.2 && < 1.13,
unordered-containers ^>= 0.2.11.0
ghc-options:
-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 */
--gradient:
linear-gradient(135deg,
hsl(42deg, 92%, 70%),
hsl(348deg, 92%, 70%),
hsl(334deg, 100%, 80%),
hsl(234deg, 100%, 76%),
hsl(195deg, 100%, 67%),
hsl(155deg, 70%, 62%)
oklch(93% 0.16 86),
oklch(84% 0.17 15),
oklch(78% 0.18 304),
oklch(78% 0.18 233),
oklch(78% 0.18 162)
);
--text-col: white;
@ -45,7 +44,6 @@
--button-radius: 1000px;
font-family: Muller;
font-size: x-large;
font-weight: 600;
background: var(--gradient) fixed;
@ -241,7 +239,7 @@ del {
.threecol {
columns: 3;
column-gap: 2em;
grid-column-gap: 2em;
}
.threecol dd {
@ -257,12 +255,11 @@ del {
:root {
--gradient:
linear-gradient(135deg,
hsl(42deg, 37%, 20%),
hsl(348deg, 37%, 20%),
hsl(334deg, 42%, 20%),
hsl(234deg, 67%, 18%),
hsl(195deg, 37%, 15%),
hsl(155deg, 32%, 15%)
/* oklch(33% 0.16 86), */
oklch(30% 0.17 15),
oklch(30% 0.18 304),
oklch(30% 0.18 233),
oklch(30% 0.18 162)
);
--text-col: hsl(55deg, 60%, 90%);

View file

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

View file

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

View file

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

View file

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