Compare commits
34 commits
11f5029412
...
8daa6fa09f
Author | SHA1 | Date | |
---|---|---|---|
8daa6fa09f | |||
85e2c6e5ac | |||
00a238e00f | |||
58b88ed23c | |||
0561f9dab1 | |||
02214bca0b | |||
56795be3ac | |||
ab0e1a4127 | |||
f2aeb8e187 | |||
d93e38b618 | |||
411c93e860 | |||
5d576d358c | |||
38120bc60f | |||
e89970d339 | |||
cd469f33db | |||
762300dcaf | |||
b2e2db77dd | |||
0de54d15d4 | |||
fe338b2f3f | |||
97660781a7 | |||
b1b0f806d5 | |||
5c70198e38 | |||
d06f749b8c | |||
5031058bde | |||
fa8fa4a18b | |||
a1453db34a | |||
06ca357d60 | |||
1f84b3e83e | |||
61fd58413f | |||
6554dfd54c | |||
bde75df8ca | |||
09897f27d9 | |||
7a08c05cea | |||
8cb6752168 |
21 changed files with 542 additions and 464 deletions
22
Makefile
22
Makefile
|
@ -16,22 +16,24 @@ MAKEPAGES := $(TMPDIR)/make-pages
|
||||||
|
|
||||||
YAMLS != find -L $(DATADIR) -name $(INFONAME)
|
YAMLS != find -L $(DATADIR) -name $(INFONAME)
|
||||||
|
|
||||||
SCRIPTS != find script -name '*.js'
|
TSCRIPTS != find script -name '*.ts'
|
||||||
STYLES != find style -name '*.css'
|
STYLES != find style -name '*.css'
|
||||||
STYLESVGS != find style -name '*.svg'
|
STYLESVGS != find style -name '*.svg'
|
||||||
STYLEPNGS != find style -name '*.png'
|
STYLEPNGS != find style -name '*.png'
|
||||||
|
|
||||||
|
|
||||||
FONTS != find fonts \
|
FONTS != find fonts \
|
||||||
-iname '*.eot' -or -iname '*.svg' -or \
|
-iname '*.eot' -or -iname '*.svg' -or \
|
||||||
-iname '*.ttf' -or -iname '*.woff' -or \
|
-iname '*.ttf' -or -iname '*.woff' -or \
|
||||||
-iname '*.woff2' -or -iname '*.css'
|
-iname '*.woff2' -or -iname '*.css'
|
||||||
|
|
||||||
STATIC = $(SCRIPTS) $(STYLES) $(STYLEPNGS) $(STYLESVGS) $(FONTS)
|
STATIC = $(STYLES) $(STYLEPNGS) $(STYLESVGS) $(FONTS)
|
||||||
BSTATIC = $(patsubst %,$(BUILDDIR)/%,$(STATIC))
|
BSTATIC = $(patsubst %,$(BUILDDIR)/%,$(STATIC))
|
||||||
|
BSCRIPTS = $(patsubst %.ts,$(BUILDDIR)/%.js,$(TSCRIPTS))
|
||||||
|
|
||||||
.PHONY: all build
|
.PHONY: all build
|
||||||
all: build
|
all: build
|
||||||
build: $(BUILDDIR)/index.html $(BSTATIC)
|
build: $(BUILDDIR)/index.html $(BSTATIC) $(BSCRIPTS)
|
||||||
|
|
||||||
$(BUILDDIR)/index.html: $(DATADIR)/index.yaml $(MAKEPAGES)
|
$(BUILDDIR)/index.html: $(DATADIR)/index.yaml $(MAKEPAGES)
|
||||||
echo "[index] "$@
|
echo "[index] "$@
|
||||||
|
@ -43,9 +45,15 @@ $(BUILDDIR)/%: %
|
||||||
$(call copy,--link --force)
|
$(call copy,--link --force)
|
||||||
|
|
||||||
$(BUILDDIR)/%: $(TMPDIR)/%
|
$(BUILDDIR)/%: $(TMPDIR)/%
|
||||||
$(call copy,--link)
|
$(call copy,--link --force)
|
||||||
|
|
||||||
|
|
||||||
|
$(TMPDIR)/%.js: %.ts
|
||||||
|
echo "[tsc] "$@
|
||||||
|
tsc --strict --noEmitOnError \
|
||||||
|
--lib dom,es2021 --target es2015 \
|
||||||
|
--outDir $(dir $@) $^
|
||||||
|
|
||||||
$(TMPDIR)/%_small.png: $(DATADIR)/%.png
|
$(TMPDIR)/%_small.png: $(DATADIR)/%.png
|
||||||
$(call resize,$(SMALL),$(SMALL),^,-gravity center -crop 1:1+0)
|
$(call resize,$(SMALL),$(SMALL),^,-gravity center -crop 1:1+0)
|
||||||
|
|
||||||
|
@ -70,10 +78,10 @@ $(TMPDIR)/%_small.webp: $(DATADIR)/%.webp
|
||||||
$(call resize,$(SMALL),$(SMALL),^,-gravity center -crop 1:1+0)
|
$(call resize,$(SMALL),$(SMALL),^,-gravity center -crop 1:1+0)
|
||||||
|
|
||||||
$(TMPDIR)/%_med.webp: $(DATADIR)/%.webp
|
$(TMPDIR)/%_med.webp: $(DATADIR)/%.webp
|
||||||
$(call resize,$(MEDW),$(MEDH),>)
|
$(call resize,$(MEDW),$(MEDH),>,-quality 90)
|
||||||
|
|
||||||
$(TMPDIR)/%_big.webp: $(DATADIR)/%.webp
|
$(TMPDIR)/%_big.webp: $(DATADIR)/%.webp
|
||||||
$(call resize,$(BIG),$(BIG),>)
|
$(call copy)
|
||||||
|
|
||||||
|
|
||||||
$(MAKEPAGES): make-pages/*.hs make-pages/make-pages.cabal
|
$(MAKEPAGES): make-pages/*.hs make-pages/make-pages.cabal
|
||||||
|
@ -140,7 +148,7 @@ endef
|
||||||
define resize
|
define resize
|
||||||
echo "[resize] "$@
|
echo "[resize] "$@
|
||||||
mkdir -p "$(dir $@)"
|
mkdir -p "$(dir $@)"
|
||||||
convert -resize "$(1)x$(2)$(3)" $(4) "$^" "$@"
|
convert -resize "$(1)x$(2)$(3)" -define webp:lossless=true $(4) "$^" "$@"
|
||||||
endef
|
endef
|
||||||
|
|
||||||
# no args
|
# no args
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE PatternSynonyms, TemplateHaskell #-}
|
||||||
module BuilderQQ
|
module BuilderQQ
|
||||||
(b,
|
(b,
|
||||||
Builder, toStrictText, toLazyText, fromText, fromString, fromChar,
|
Builder, toStrictText, toLazyText, fromText, fromString, fromChar,
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
{-# LANGUAGE TransformListComp #-}
|
|
||||||
module GalleryPage (make) where
|
module GalleryPage (make) where
|
||||||
|
|
||||||
import BuilderQQ
|
import BuilderQQ
|
||||||
|
@ -8,15 +7,15 @@ import qualified NsfwWarning
|
||||||
|
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Data.Foldable
|
import Data.Foldable
|
||||||
import Data.Function (on, (&))
|
import Data.Function ((&))
|
||||||
import qualified Data.HashMap.Strict as HashMap
|
import qualified Data.HashMap.Strict as HashMap
|
||||||
import Data.HashSet (HashSet)
|
import Data.HashSet (HashSet)
|
||||||
import qualified Data.HashSet as HashSet
|
import qualified Data.HashSet as HashSet
|
||||||
import Data.List (intersperse, groupBy, sortBy, sort)
|
import Data.List (intersperse, sort, sortOn)
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
|
import Data.Ord (Down (..))
|
||||||
import qualified Data.Text.Lazy as Lazy
|
import qualified Data.Text.Lazy as Lazy
|
||||||
import System.FilePath (takeDirectory, joinPath, splitPath)
|
import System.FilePath (takeDirectory, joinPath, splitPath)
|
||||||
import GHC.Exts (Down (..), the)
|
|
||||||
|
|
||||||
make :: Text -> GalleryInfo -> [(FilePath, Info)] -> Lazy.Text
|
make :: Text -> GalleryInfo -> [(FilePath, Info)] -> Lazy.Text
|
||||||
make root ginfo infos = toLazyText $ make' root ginfo infos
|
make root ginfo infos = toLazyText $ make' root ginfo infos
|
||||||
|
@ -42,7 +41,7 @@ make' root (GalleryInfo {title, desc, prefix, filters, hidden}) infos = [b|@0
|
||||||
|
|
||||||
<meta name=robots content='noai,noimageai'>
|
<meta name=robots content='noai,noimageai'>
|
||||||
|
|
||||||
<script src=/script/gallery.js></script>
|
<script src=/script/gallery.js type=module></script>
|
||||||
$0.nsfwScript
|
$0.nsfwScript
|
||||||
|
|
||||||
<title>$title</title>
|
<title>$title</title>
|
||||||
|
@ -90,15 +89,11 @@ make' root (GalleryInfo {title, desc, prefix, filters, hidden}) infos = [b|@0
|
||||||
where
|
where
|
||||||
items = map (uncurry $ makeYearItems nsfw) infosByYear
|
items = map (uncurry $ makeYearItems nsfw) infosByYear
|
||||||
|
|
||||||
infosByYear =
|
infosByYear :: [(Int, [(FilePath, Info)])]
|
||||||
[(the year, infopath) |
|
infosByYear = infos &
|
||||||
infopath@(_, info) <- infos,
|
filter (not . #unlisted . snd) &
|
||||||
not $ #unlisted info,
|
sortOn (Down . compareKeyFor nsfw . snd) &
|
||||||
then sortInfo by info,
|
groupOnKey (\(_, i) -> #latestYear i nsfw)
|
||||||
let year = #latestYear info nsfw,
|
|
||||||
then group by Down year using groupBy']
|
|
||||||
sortInfo f = sortBy $ flip (compareFor nsfw `on` f)
|
|
||||||
groupBy' f = groupBy ((==) `on` f)
|
|
||||||
|
|
||||||
undir = joinPath (replicate (length (splitPath prefix)) "..")
|
undir = joinPath (replicate (length (splitPath prefix)) "..")
|
||||||
|
|
||||||
|
@ -114,13 +109,20 @@ make' root (GalleryInfo {title, desc, prefix, filters, hidden}) infos = [b|@0
|
||||||
|
|
||||||
url = [b|$root/$prefix|]
|
url = [b|$root/$prefix|]
|
||||||
imagepath0
|
imagepath0
|
||||||
| (_, (p0, i0) : _) : _ <- infosByYear = getThumb (takeDirectory p0) i0
|
| (_, (p₀, i₀) : _) : _ <- infosByYear = getThumb (takeDirectory p₀) i₀
|
||||||
| otherwise = "/style/card.png"
|
| otherwise = "/style/card.png"
|
||||||
|
|
||||||
nsfw' = NsfwWarning.Gallery <$ guard nsfw
|
nsfw' = NsfwWarning.Gallery <$ guard nsfw
|
||||||
nsfwScript = NsfwWarning.script nsfw'
|
nsfwScript = NsfwWarning.script nsfw'
|
||||||
nsfwDialog = NsfwWarning.dialog nsfw'
|
nsfwDialog = NsfwWarning.dialog nsfw'
|
||||||
|
|
||||||
|
-- from @extra@
|
||||||
|
groupOnKey :: Eq k => (a -> k) -> [a] -> [(k, [a])]
|
||||||
|
groupOnKey _ [] = []
|
||||||
|
groupOnKey f (x:xs) = (fx, x:yes) : groupOnKey f no where
|
||||||
|
fx = f x
|
||||||
|
(yes, no) = span (\y -> fx == f y) xs
|
||||||
|
|
||||||
makeFilter :: Text -> HashSet Text -> Text -> Int -> Builder
|
makeFilter :: Text -> HashSet Text -> Text -> Int -> Builder
|
||||||
makeFilter prefix initial tag count = [b|@0
|
makeFilter prefix initial tag count = [b|@0
|
||||||
<li$hidden>
|
<li$hidden>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
module Info
|
module Info
|
||||||
(Info (..),
|
(Info (..),
|
||||||
tagsFor, descFor, imagesFor, linksFor, updatesFor, lastUpdate,
|
tagsFor, descFor, imagesFor, linksFor, updatesFor, lastUpdate,
|
||||||
compareFor, sortFor,
|
CompareKey (..), compareKeyFor, compareFor, sortFor,
|
||||||
Artist (..), Images' (..), Images, Image (..), Desc (..), DescField (..),
|
Artist (..), Images' (..), Images, Image (..), Desc (..), DescField (..),
|
||||||
Link (..), Update (..), Bg (..),
|
Link (..), Update (..), Bg (..),
|
||||||
GalleryInfo (..), GalleryFilters (..), ArtistFilter (..), NsfwFilter (..),
|
GalleryInfo (..), GalleryFilters (..), ArtistFilter (..), NsfwFilter (..),
|
||||||
|
@ -163,7 +163,8 @@ instance HasField "notMine" Info Bool where getField = isJust . #artist
|
||||||
instance HasField "latestDate" Info (Bool -> Date) where
|
instance HasField "latestDate" Info (Bool -> Date) where
|
||||||
getField info@(Info {date=date₀}) nsfw =
|
getField info@(Info {date=date₀}) nsfw =
|
||||||
maximum $ date₀ : mapMaybe relDate (updatesFor nsfw info)
|
maximum $ date₀ : mapMaybe relDate (updatesFor nsfw info)
|
||||||
where relDate (date, us) = date <$ guard (not $ any #ignoreSort us)
|
where
|
||||||
|
relDate (date, us) = date <$ guard (not $ null us || any #ignoreSort us)
|
||||||
|
|
||||||
instance HasField "latestYear" Info (Bool -> Int) where
|
instance HasField "latestYear" Info (Bool -> Int) where
|
||||||
getField info nsfw = #year $ #latestDate info nsfw
|
getField info nsfw = #year $ #latestDate info nsfw
|
||||||
|
@ -224,8 +225,14 @@ lastUpdate :: Bool -> Info -> Maybe Date
|
||||||
lastUpdate nsfw info =
|
lastUpdate nsfw info =
|
||||||
case updatesFor nsfw info of [] -> Nothing; us -> Just $ fst $ last us
|
case updatesFor nsfw info of [] -> Nothing; us -> Just $ fst $ last us
|
||||||
|
|
||||||
|
data CompareKey = MkCompareKey !Date !Text !Text
|
||||||
|
deriving (Eq, Ord)
|
||||||
|
|
||||||
|
compareKeyFor :: Bool -> Info -> CompareKey
|
||||||
|
compareKeyFor nsfw i = MkCompareKey (#latestDate i nsfw) (#sortEx i) (#title i)
|
||||||
|
|
||||||
compareFor :: Bool -> Info -> Info -> Ordering
|
compareFor :: Bool -> Info -> Info -> Ordering
|
||||||
compareFor nsfw = comparing \i -> (#latestDate i nsfw, #sortEx i, #title i)
|
compareFor nsfw = comparing $ compareKeyFor nsfw
|
||||||
|
|
||||||
sortFor :: Bool -> [Info] -> [Info]
|
sortFor :: Bool -> [Info] -> [Info]
|
||||||
sortFor = sortBy . compareFor
|
sortFor = sortBy . compareFor
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{-# LANGUAGE CPP, ImplicitParams, TypeApplications #-}
|
{-# LANGUAGE CPP, ImplicitParams #-}
|
||||||
module Main (main) where
|
module Main (main) where
|
||||||
|
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
|
|
|
@ -12,7 +12,7 @@ instance CanBuild What where
|
||||||
|
|
||||||
script :: Maybe What -> Builder
|
script :: Maybe What -> Builder
|
||||||
script Nothing = ""
|
script Nothing = ""
|
||||||
script (Just _) = [b|<script src=/script/nsfw-warning.js></script>|]
|
script (Just _) = [b|<script src=/script/nsfw-warning.js type=module></script>|]
|
||||||
|
|
||||||
dialog :: Maybe What -> Builder
|
dialog :: Maybe What -> Builder
|
||||||
dialog Nothing = ""
|
dialog Nothing = ""
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
{-# OPTIONS_GHC -Wno-orphans #-}
|
{-# OPTIONS_GHC -Wno-orphans #-}
|
||||||
{-# LANGUAGE MultiParamTypeClasses, ScopedTypeVariables, TypeApplications #-}
|
|
||||||
module Records (HasField (..)) where
|
module Records (HasField (..)) where
|
||||||
|
|
||||||
import GHC.Records
|
import GHC.Records
|
||||||
|
|
|
@ -45,8 +45,6 @@ make' root siteName prefix nsfw _dataDir dir
|
||||||
|
|
||||||
let undir = joinPath (replicate (length (splitPath dir)) "..")
|
let undir = joinPath (replicate (length (splitPath dir)) "..")
|
||||||
|
|
||||||
let artistTag = ifJust artist makeArtist
|
|
||||||
|
|
||||||
let formattedDate = formatLong date
|
let formattedDate = formatLong date
|
||||||
|
|
||||||
let buttonBar = makeButtonBar title $ addIds images
|
let buttonBar = makeButtonBar title $ addIds images
|
||||||
|
@ -58,6 +56,7 @@ make' root siteName prefix nsfw _dataDir dir
|
||||||
let download0 = fromMaybe (bigFile path0) download0'
|
let download0 = fromMaybe (bigFile path0) download0'
|
||||||
let path0' = pageFile path0
|
let path0' = pageFile path0
|
||||||
|
|
||||||
|
let artistSection = makeArtist artist
|
||||||
let descSection = makeDesc $ descFor nsfw info
|
let descSection = makeDesc $ descFor nsfw info
|
||||||
let tagsList = makeTags undir $ tagsFor nsfw info
|
let tagsList = makeTags undir $ tagsFor nsfw info
|
||||||
let linksList = extLinks $ linksFor nsfw info
|
let linksList = extLinks $ linksFor nsfw info
|
||||||
|
@ -136,12 +135,11 @@ make' root siteName prefix nsfw _dataDir dir
|
||||||
<meta property=og:site_name content="$siteName">
|
<meta property=og:site_name content="$siteName">
|
||||||
<meta property=og:description content="$desc">
|
<meta property=og:description content="$desc">
|
||||||
<meta property=og:url content="$url">
|
<meta property=og:url content="$url">
|
||||||
<meta name=twitter:site content=@2_gecs>
|
|
||||||
$imageMeta
|
$imageMeta
|
||||||
|
|
||||||
<meta name=robots content='noai,noimageai'>
|
<meta name=robots content='noai,noimageai'>
|
||||||
|
|
||||||
<script src=/script/single.js></script>
|
<script src=/script/single.js type=module></script>
|
||||||
$nsfwScript
|
$nsfwScript
|
||||||
$bgStyle
|
$bgStyle
|
||||||
|
|
||||||
|
@ -158,7 +156,6 @@ make' root siteName prefix nsfw _dataDir dir
|
||||||
$formattedDate $updateDate
|
$formattedDate $updateDate
|
||||||
</h2>
|
</h2>
|
||||||
<h2 class="left corner">
|
<h2 class="left corner">
|
||||||
$artistTag
|
|
||||||
<a href=$undir>back to gallery</a>
|
<a href=$undir>back to gallery</a>
|
||||||
</h2>
|
</h2>
|
||||||
</header>
|
</header>
|
||||||
|
@ -174,6 +171,8 @@ make' root siteName prefix nsfw _dataDir dir
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<div id=info>
|
<div id=info>
|
||||||
|
$6.artistSection
|
||||||
|
|
||||||
$6.descSection
|
$6.descSection
|
||||||
|
|
||||||
$6.updatesList
|
$6.updatesList
|
||||||
|
@ -193,9 +192,14 @@ make' root siteName prefix nsfw _dataDir dir
|
||||||
last' :: [a] -> Maybe a
|
last' :: [a] -> Maybe a
|
||||||
last' xs = if null xs then Nothing else Just $ last xs
|
last' xs = if null xs then Nothing else Just $ last xs
|
||||||
|
|
||||||
makeArtist :: Artist -> Builder
|
makeArtist :: Maybe Artist -> Builder
|
||||||
makeArtist (Artist {name, url}) =
|
makeArtist Nothing = ""
|
||||||
[b|by $artistLink <br>|]
|
makeArtist (Just (Artist {name, url})) = [b|@0
|
||||||
|
<section id=desc class=info-section>
|
||||||
|
<h2>by</h2>
|
||||||
|
<div>$artistLink</div>
|
||||||
|
</section>
|
||||||
|
|]
|
||||||
where
|
where
|
||||||
artistLink = case url of
|
artistLink = case url of
|
||||||
Just u -> [b|<a href="$u">$name</a>|]
|
Just u -> [b|<a href="$u">$name</a>|]
|
||||||
|
@ -247,7 +251,8 @@ makeButtonBar title images =
|
||||||
| otherwise ->
|
| otherwise ->
|
||||||
makeNav "cat" $ map (uncurry makeCat) cats
|
makeNav "cat" $ map (uncurry makeCat) cats
|
||||||
where
|
where
|
||||||
makeNav (cls :: Text) inner = [b|@0
|
makeNav :: CanBuild b => Text -> b -> Builder
|
||||||
|
makeNav cls inner = [b|@0
|
||||||
<nav id=alts class=$cls aria-label="alternate versions">
|
<nav id=alts class=$cls aria-label="alternate versions">
|
||||||
$2.inner
|
$2.inner
|
||||||
$2.skipAll
|
$2.skipAll
|
||||||
|
@ -271,8 +276,7 @@ makeButtonBar title images =
|
||||||
<label for=skipAll>skip warnings</label>
|
<label for=skipAll>skip warnings</label>
|
||||||
</div>
|
</div>
|
||||||
|]
|
|]
|
||||||
else
|
else ""
|
||||||
""
|
|
||||||
|
|
||||||
flatten :: [(Text, [(Image, a)])] -> [(Image, Text)]
|
flatten :: [(Text, [(Image, a)])] -> [(Image, Text)]
|
||||||
flatten cats =
|
flatten cats =
|
||||||
|
|
|
@ -3,8 +3,8 @@ name: make-pages
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
license: AGPL-3.0-or-later
|
license: AGPL-3.0-or-later
|
||||||
|
|
||||||
author: Rhiannon Morris <rhi@rhiannon.website>
|
author: rhiannon morris <rhi@rhiannon.website>
|
||||||
maintainer: Rhiannon Morris <rhi@rhiannon.website>
|
maintainer: rhiannon morris <rhi@rhiannon.website>
|
||||||
|
|
||||||
flag pretty-verbose
|
flag pretty-verbose
|
||||||
description: pretty-print the verbose output
|
description: pretty-print the verbose output
|
||||||
|
@ -26,53 +26,36 @@ executable make-pages
|
||||||
RSS,
|
RSS,
|
||||||
ListTags,
|
ListTags,
|
||||||
Options
|
Options
|
||||||
default-language: Haskell2010
|
default-language: GHC2024
|
||||||
default-extensions:
|
default-extensions:
|
||||||
BlockArguments,
|
BlockArguments,
|
||||||
ConstraintKinds,
|
|
||||||
DataKinds,
|
|
||||||
DeriveAnyClass,
|
DeriveAnyClass,
|
||||||
DeriveTraversable,
|
|
||||||
DerivingStrategies,
|
|
||||||
DerivingVia,
|
DerivingVia,
|
||||||
DuplicateRecordFields,
|
DuplicateRecordFields,
|
||||||
FlexibleContexts,
|
|
||||||
FlexibleInstances,
|
|
||||||
GeneralizedNewtypeDeriving,
|
|
||||||
LambdaCase,
|
|
||||||
NamedFieldPuns,
|
|
||||||
OverloadedLabels,
|
OverloadedLabels,
|
||||||
OverloadedLists,
|
OverloadedLists,
|
||||||
OverloadedStrings,
|
OverloadedStrings,
|
||||||
PatternSynonyms,
|
|
||||||
QuasiQuotes,
|
QuasiQuotes,
|
||||||
RankNTypes,
|
|
||||||
ScopedTypeVariables,
|
|
||||||
StandaloneDeriving,
|
|
||||||
TupleSections,
|
|
||||||
TypeSynonymInstances,
|
TypeSynonymInstances,
|
||||||
ViewPatterns
|
ViewPatterns
|
||||||
other-extensions:
|
other-extensions:
|
||||||
|
PatternSynonyms,
|
||||||
CPP,
|
CPP,
|
||||||
ImplicitParams,
|
ImplicitParams,
|
||||||
MultiParamTypeClasses,
|
TemplateHaskell
|
||||||
ScopedTypeVariables,
|
|
||||||
TemplateHaskell,
|
|
||||||
TransformListComp,
|
|
||||||
TypeApplications
|
|
||||||
build-depends:
|
build-depends:
|
||||||
base ^>= 4.16.4,
|
base >= 4.16.4 && < 4.21,
|
||||||
bytestring ^>= 0.11.3.1,
|
bytestring >= 0.11.3.1 && < 0.14,
|
||||||
containers ^>= 0.6.0.1,
|
containers >= 0.6.0.1 && < 0.8,
|
||||||
filemanip ^>= 0.3.6.3,
|
filemanip ^>= 0.3.6.3,
|
||||||
filepath ^>= 1.4.2.1,
|
filepath >= 1.4.2.1 && < 1.6,
|
||||||
hashable ^>= 1.3.0.0,
|
hashable >= 1.3.0.0 && < 1.5,
|
||||||
HsYAML ^>= 0.2.1.0,
|
HsYAML ^>= 0.2.1.0,
|
||||||
optparse-applicative ^>= 0.15.1.0,
|
optparse-applicative ^>= 0.15.1.0,
|
||||||
process ^>= 1.6.8.2,
|
process ^>= 1.6.8.2,
|
||||||
template-haskell ^>= 2.18.0.0,
|
template-haskell >= 2.18.0.0 && < 2.23,
|
||||||
text ^>= 1.2.3.1,
|
text >= 1.2.3.1 && < 2.2,
|
||||||
time >= 1.8.0.2 && < 1.10,
|
time >= 1.8.0.2 && < 1.13,
|
||||||
unordered-containers ^>= 0.2.11.0
|
unordered-containers ^>= 0.2.11.0
|
||||||
ghc-options:
|
ghc-options:
|
||||||
-Wall -threaded -rtsopts -with-rtsopts=-N -O
|
-Wall -threaded -rtsopts -with-rtsopts=-N -O
|
||||||
|
|
|
@ -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
224
script/gallery.ts
Normal 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 {};
|
|
@ -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
28
script/nsfw-warning.ts
Normal 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 {};
|
110
script/single.js
110
script/single.js
|
@ -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
109
script/single.ts
Normal 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 {};
|
|
@ -17,12 +17,11 @@
|
||||||
/* bright colours from yummy.cricket bg */
|
/* bright colours from yummy.cricket bg */
|
||||||
--gradient:
|
--gradient:
|
||||||
linear-gradient(135deg,
|
linear-gradient(135deg,
|
||||||
hsl(42deg, 92%, 70%),
|
oklch(93% 0.16 86),
|
||||||
hsl(348deg, 92%, 70%),
|
oklch(84% 0.17 15),
|
||||||
hsl(334deg, 100%, 80%),
|
oklch(78% 0.18 304),
|
||||||
hsl(234deg, 100%, 76%),
|
oklch(78% 0.18 233),
|
||||||
hsl(195deg, 100%, 67%),
|
oklch(78% 0.18 162)
|
||||||
hsl(155deg, 70%, 62%)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
--text-col: white;
|
--text-col: white;
|
||||||
|
@ -45,7 +44,6 @@
|
||||||
--button-radius: 1000px;
|
--button-radius: 1000px;
|
||||||
|
|
||||||
font-family: Muller;
|
font-family: Muller;
|
||||||
font-size: x-large;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
||||||
background: var(--gradient) fixed;
|
background: var(--gradient) fixed;
|
||||||
|
@ -241,7 +239,7 @@ del {
|
||||||
|
|
||||||
.threecol {
|
.threecol {
|
||||||
columns: 3;
|
columns: 3;
|
||||||
column-gap: 2em;
|
grid-column-gap: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.threecol dd {
|
.threecol dd {
|
||||||
|
@ -257,12 +255,11 @@ del {
|
||||||
:root {
|
:root {
|
||||||
--gradient:
|
--gradient:
|
||||||
linear-gradient(135deg,
|
linear-gradient(135deg,
|
||||||
hsl(42deg, 37%, 20%),
|
/* oklch(33% 0.16 86), */
|
||||||
hsl(348deg, 37%, 20%),
|
oklch(30% 0.17 15),
|
||||||
hsl(334deg, 42%, 20%),
|
oklch(30% 0.18 304),
|
||||||
hsl(234deg, 67%, 18%),
|
oklch(30% 0.18 233),
|
||||||
hsl(195deg, 37%, 15%),
|
oklch(30% 0.18 162)
|
||||||
hsl(155deg, 32%, 15%)
|
|
||||||
);
|
);
|
||||||
--text-col: hsl(55deg, 60%, 90%);
|
--text-col: hsl(55deg, 60%, 90%);
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 15% auto;
|
grid-template-columns: 15% auto;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: 0.75em;
|
grid-gap: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#filters h3 {
|
#filters h3 {
|
||||||
|
@ -42,15 +42,22 @@
|
||||||
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
gap: 0.5em;
|
grid-column-gap: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filterlist input {
|
.filterlist input {
|
||||||
appearance: none;
|
/* i can't believe it's not appearance: none */
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
clip-path: inset(100%);
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filterlist li:not([hidden]) {
|
.filterlist li {
|
||||||
display: block;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filterlist li:focus-within {
|
.filterlist li:focus-within {
|
||||||
|
@ -101,7 +108,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
gap: 2em;
|
grid-gap: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#filterstuff li {
|
#filterstuff li {
|
||||||
|
@ -138,7 +145,7 @@
|
||||||
#filterstuff {
|
#filterstuff {
|
||||||
grid-area: unset;
|
grid-area: unset;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
gap: 0.2em;
|
grid-column-gap: 0.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#filterstuff li {
|
#filterstuff li {
|
||||||
|
@ -151,7 +158,7 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid: auto-flow / repeat(auto-fit, var(--image-size));
|
grid: auto-flow / repeat(auto-fit, var(--image-size));
|
||||||
gap: var(--gap);
|
grid-gap: var(--gap);
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,8 +174,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.item:not(.year-marker) {
|
.item:not(.year-marker) {
|
||||||
box-shadow: var(--text-shadow);
|
|
||||||
outline: var(--border-thickness) solid var(--border-col);
|
|
||||||
background: hsl(0, 0%, 0%, 50%);
|
background: hsl(0, 0%, 0%, 50%);
|
||||||
clip-path: polygon(5% 0, 95% 10%, 95% 100%, 5% 90%);
|
clip-path: polygon(5% 0, 95% 10%, 95% 100%, 5% 90%);
|
||||||
}
|
}
|
||||||
|
@ -190,7 +195,7 @@
|
||||||
--gap: 0.2em;
|
--gap: 0.2em;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, calc(50% - 3 * var(--gap)));
|
grid-template-columns: repeat(2, calc(50% - 3 * var(--gap)));
|
||||||
gap: var(--gap);
|
grid-gap: var(--gap);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -214,6 +219,8 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
mix-blend-mode: hard-light;
|
mix-blend-mode: hard-light;
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item.nsfw::before {
|
.item.nsfw::before {
|
||||||
|
|
|
@ -62,12 +62,12 @@
|
||||||
|
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
grid-template-columns: repeat(2, auto);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
gap: 1.5em;
|
grid-gap: 1.5em;
|
||||||
|
|
||||||
font-size: 175%;
|
font-size: 175%;
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,7 @@ main {
|
||||||
.list {
|
.list {
|
||||||
font-size: 300%;
|
font-size: 300%;
|
||||||
grid-template-columns: 100%;
|
grid-template-columns: 100%;
|
||||||
gap: 1em;
|
grid-gap: 1em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
"icon text"
|
"icon text"
|
||||||
"buttons buttons"
|
"buttons buttons"
|
||||||
/ 1fr 3fr;
|
/ 1fr 3fr;
|
||||||
gap: 0.5em;
|
grid-gap: 0.5em;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
min-height: 20vh;
|
min-height: 20vh;
|
||||||
|
|
|
@ -64,6 +64,7 @@ body {
|
||||||
padding: 0.25em 0.5em;
|
padding: 0.25em 0.5em;
|
||||||
border-radius: 0.5em;
|
border-radius: 0.5em;
|
||||||
}
|
}
|
||||||
|
#cw-text b { font-weight: 900; }
|
||||||
#cw-text::before {
|
#cw-text::before {
|
||||||
content: 'cw: ';
|
content: 'cw: ';
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
@ -91,7 +92,7 @@ body {
|
||||||
#info {
|
#info {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: min-content auto;
|
grid-template-columns: min-content auto;
|
||||||
column-gap: 1em;
|
grid-column-gap: 1em;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,7 +145,7 @@ body {
|
||||||
#updates dl {
|
#updates dl {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: min-content auto;
|
grid-template-columns: min-content auto;
|
||||||
gap: 0.5em;
|
grid-gap: 0.5em;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,7 +184,7 @@ footer {
|
||||||
margin: 1.5em 0;
|
margin: 1.5em 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.5em;
|
grid-gap: 0.5em;
|
||||||
grid-template-columns: minmax(auto, 10em) auto minmax(auto, 10em);
|
grid-template-columns: minmax(auto, 10em) auto minmax(auto, 10em);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,11 +215,64 @@ footer {
|
||||||
grid-area: 1 / 3 / auto / auto;
|
grid-area: 1 / 3 / auto / auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(#tags, #links) ul {
|
#tags ul, #links ul {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: wrap row;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(#tags, #links) li {
|
#tags li, #links li {
|
||||||
display: inline;
|
display: inline;
|
||||||
margin-right: 0.75em;
|
margin-right: 0.75em;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.chost, .toot {
|
||||||
|
--avatar-size: 4em;
|
||||||
|
display: grid;
|
||||||
|
grid-template: 'avatar user' 1lh
|
||||||
|
'avatar post' auto / var(--avatar-size) 1fr;
|
||||||
|
grid-gap: 5px 15px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.chost, .toot) .user {
|
||||||
|
grid-area: user;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.chost, .toot) :is(.user a, a.user) {
|
||||||
|
font-weight: 800;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.chost, .toot) .username {
|
||||||
|
margin-left: 1em;
|
||||||
|
font-size: 90%;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.chost, .toot) .post { grid-area: post; }
|
||||||
|
:is(.chost, .toot) .avatar { grid-area: avatar; }
|
||||||
|
|
||||||
|
:is(.chost, .toot) :is(.avatar img, img.avatar) {
|
||||||
|
height: var(--avatar-size); width: var(--avatar-size);
|
||||||
|
border-radius: 1000em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chost .squircle {
|
||||||
|
mask-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTAwIDBDMjAgMCAwIDIwIDAgMTAwczIwIDEwMCAxMDAgMTAwIDEwMC0yMCAxMDAtMTAwUzE4MCAwIDEwMCAweiIvPjwvc3ZnPg==);
|
||||||
|
mask-size: contain;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.chost, .toot) h1 {
|
||||||
|
margin: 0 0 0.5em;
|
||||||
|
font-size: 150%;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.chost, .toot) + :is(.chost, .toot) { margin-top: 1.5em; }
|
||||||
|
|
8
tsconfig.json
Normal file
8
tsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"noEmitOnError": true,
|
||||||
|
"lib": ["ES2021", "dom"],
|
||||||
|
"target": "ES2015"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue