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