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

View file

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

View file

@ -1,3 +1,4 @@
{-# LANGUAGE TransformListComp #-}
module GalleryPage (make) where
import BuilderQQ
@ -7,15 +8,15 @@ import qualified NsfwWarning
import Control.Monad
import Data.Foldable
import Data.Function ((&))
import Data.Function (on, (&))
import qualified Data.HashMap.Strict as HashMap
import Data.HashSet (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.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
@ -41,7 +42,7 @@ make' root (GalleryInfo {title, desc, prefix, filters, hidden}) infos = [b|@0
<meta name=robots content='noai,noimageai'>
<script src=/script/gallery.js type=module></script>
<script src=/script/gallery.js></script>
$0.nsfwScript
<title>$title</title>
@ -89,11 +90,15 @@ make' root (GalleryInfo {title, desc, prefix, filters, hidden}) infos = [b|@0
where
items = map (uncurry $ makeYearItems nsfw) infosByYear
infosByYear :: [(Int, [(FilePath, Info)])]
infosByYear = infos &
filter (not . #unlisted . snd) &
sortOn (Down . compareKeyFor nsfw . snd) &
groupOnKey (\(_, i) -> #latestYear i nsfw)
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)
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|]
imagepath0
| (_, (p, i) : _) : _ <- infosByYear = getThumb (takeDirectory p) i
| (_, (p0, i0) : _) : _ <- infosByYear = getThumb (takeDirectory p0) i0
| 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,
CompareKey (..), compareKeyFor, compareFor, sortFor,
compareFor, sortFor,
Artist (..), Images' (..), Images, Image (..), Desc (..), DescField (..),
Link (..), Update (..), Bg (..),
GalleryInfo (..), GalleryFilters (..), ArtistFilter (..), NsfwFilter (..),
@ -163,8 +163,7 @@ 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 $ null us || any #ignoreSort us)
where relDate (date, us) = date <$ guard (not $ any #ignoreSort us)
instance HasField "latestYear" Info (Bool -> Int) where
getField info nsfw = #year $ #latestDate info nsfw
@ -225,14 +224,8 @@ 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 $ compareKeyFor nsfw
compareFor nsfw = comparing \i -> (#latestDate i nsfw, #sortEx i, #title i)
sortFor :: Bool -> [Info] -> [Info]
sortFor = sortBy . compareFor

View file

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

View file

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

View file

@ -45,6 +45,8 @@ 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
@ -56,12 +58,11 @@ make' root siteName prefix nsfw _dataDir dir
let download0 = fromMaybe (bigFile path0) download0'
let path0' = pageFile path0
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 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
@ -135,11 +136,12 @@ 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 type=module></script>
<script src=/script/single.js></script>
$nsfwScript
$bgStyle
@ -156,6 +158,7 @@ make' root siteName prefix nsfw _dataDir dir
$formattedDate $updateDate
</h2>
<h2 class="left corner">
$artistTag
<a href=$undir>back to gallery</a>
</h2>
</header>
@ -171,8 +174,6 @@ make' root siteName prefix nsfw _dataDir dir
</figure>
<div id=info>
$6.artistSection
$6.descSection
$6.updatesList
@ -192,14 +193,9 @@ make' root siteName prefix nsfw _dataDir dir
last' :: [a] -> Maybe a
last' xs = if null xs then Nothing else Just $ last xs
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>
|]
makeArtist :: Artist -> Builder
makeArtist (Artist {name, url}) =
[b|by $artistLink <br>|]
where
artistLink = case url of
Just u -> [b|<a href="$u">$name</a>|]
@ -249,10 +245,9 @@ makeButtonBar title images =
| [(_, imgs)] <- cats ->
makeButtonBar title (Uncat imgs)
| otherwise ->
makeNav "cat" $ map (uncurry makeCat) cats
makeNav "cat" $ map (uncurry makeCat) cats
where
makeNav :: CanBuild b => Text -> b -> Builder
makeNav cls inner = [b|@0
makeNav (cls :: Text) inner = [b|@0
<nav id=alts class=$cls aria-label="alternate versions">
$2.inner
$2.skipAll
@ -276,7 +271,8 @@ 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,36 +26,53 @@ executable make-pages
RSS,
ListTags,
Options
default-language: GHC2024
default-language: Haskell2010
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,
TemplateHaskell
MultiParamTypeClasses,
ScopedTypeVariables,
TemplateHaskell,
TransformListComp,
TypeApplications
build-depends:
base >= 4.16.4 && < 4.21,
bytestring >= 0.11.3.1 && < 0.14,
containers >= 0.6.0.1 && < 0.8,
base ^>= 4.16.4,
bytestring ^>= 0.11.3.1,
containers ^>= 0.6.0.1,
filemanip ^>= 0.3.6.3,
filepath >= 1.4.2.1 && < 1.6,
hashable >= 1.3.0.0 && < 1.5,
filepath ^>= 1.4.2.1,
hashable ^>= 1.3.0.0,
HsYAML ^>= 0.2.1.0,
optparse-applicative ^>= 0.15.1.0,
process ^>= 1.6.8.2,
template-haskell >= 2.18.0.0 && < 2.23,
text >= 1.2.3.1 && < 2.2,
time >= 1.8.0.2 && < 1.13,
template-haskell ^>= 2.18.0.0,
text ^>= 1.2.3.1,
time >= 1.8.0.2 && < 1.10,
unordered-containers ^>= 0.2.11.0
ghc-options:
-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 */
--gradient:
linear-gradient(135deg,
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)
hsl(42deg, 92%, 70%),
hsl(348deg, 92%, 70%),
hsl(334deg, 100%, 80%),
hsl(234deg, 100%, 76%),
hsl(195deg, 100%, 67%),
hsl(155deg, 70%, 62%)
);
--text-col: white;
@ -44,6 +45,7 @@
--button-radius: 1000px;
font-family: Muller;
font-size: x-large;
font-weight: 600;
background: var(--gradient) fixed;
@ -239,7 +241,7 @@ del {
.threecol {
columns: 3;
grid-column-gap: 2em;
column-gap: 2em;
}
.threecol dd {
@ -255,11 +257,12 @@ del {
:root {
--gradient:
linear-gradient(135deg,
/* 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)
hsl(42deg, 37%, 20%),
hsl(348deg, 37%, 20%),
hsl(334deg, 42%, 20%),
hsl(234deg, 67%, 18%),
hsl(195deg, 37%, 15%),
hsl(155deg, 32%, 15%)
);
--text-col: hsl(55deg, 60%, 90%);

View file

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

View file

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

View file

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

View file

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

View file

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