Compare commits

..

3 commits

11 changed files with 185 additions and 178 deletions

View file

@ -50,7 +50,7 @@ $(BUILDDIR)/%: $(TMPDIR)/%
$(TMPDIR)/%.js: %.ts $(TMPDIR)/%.js: %.ts
echo "[tsc] "$@ echo "[tsc] "$@
tsc --strict --noEmitOnError \ tsc --strict --noUncheckedIndexedAccess --noEmitOnError \
--lib dom,es2021 --target es2015 \ --lib dom,es2021 --target es2015 \
--outDir $(dir $@) $^ --outDir $(dir $@) $^

View file

@ -34,7 +34,7 @@ make' root (GalleryInfo {title, desc, prefix, filters, hidden}) infos = [b|@0
<meta property=og:title content="$title"> <meta property=og:title content="$title">
<meta property=og:site_name content="$title"> <meta property=og:site_name content="$title">
<meta property=og:description content="$desc"> <meta property=og:description content="$desc">
<meta property=og:image content="$url/$imagepath0"> <meta property=og:image content="$url/style/card.png">
<meta property=og:url content="$url"> <meta property=og:url content="$url">
<meta name=twitter:site content=@2_gecs> <meta name=twitter:site content=@2_gecs>
<meta name=twitter:card content=summary> <meta name=twitter:card content=summary>
@ -108,9 +108,6 @@ make' root (GalleryInfo {title, desc, prefix, filters, hidden}) infos = [b|@0
nsfw = filters.nsfw /= NoNsfw nsfw = filters.nsfw /= NoNsfw
url = [b|$root/$prefix|] url = [b|$root/$prefix|]
imagepath0
| (_, (p, i) : _) : _ <- infosByYear = getThumb (takeDirectory p) i
| otherwise = "/style/card.png"
nsfw' = NsfwWarning.Gallery <$ guard nsfw nsfw' = NsfwWarning.Gallery <$ guard nsfw
nsfwScript = NsfwWarning.script nsfw' nsfwScript = NsfwWarning.script nsfw'

View file

@ -13,6 +13,7 @@ module Info
CompareKey (..), compareKeyFor, compareFor, sortFor, CompareKey (..), compareKeyFor, compareFor, sortFor,
Artist (..), Images' (..), Images, Image (..), Desc (..), DescField (..), Artist (..), Images' (..), Images, Image (..), Desc (..), DescField (..),
PreviewImage (..), previewImage,
Link (..), Update (..), Bg (..), Link (..), Update (..), Bg (..),
@ -117,6 +118,13 @@ data Images' a =
type Images = Images' Image type Images = Images' Image
data PreviewImage = PFull Image | PThumb FilePath
previewImage :: Info -> Maybe PreviewImage
previewImage info
| Just img <- find (.sfw) $ allImages info.images = Just $ PFull img
| otherwise = PThumb <$> info.thumb'
data Link = data Link =
Link { Link {
@ -261,8 +269,7 @@ newtype NoThumb = NoThumb FilePath
instance Show NoThumb where show (NoThumb dir) = "no thumbnail for " ++ dir instance Show NoThumb where show (NoThumb dir) = "no thumbnail for " ++ dir
getThumb :: FilePath -> Info -> FilePath getThumb :: FilePath -> Info -> FilePath
getThumb dir = getThumb dir = maybe (throw $ NoThumb dir) (\t -> dir </> thumbFile t) . thumb
maybe (throw $ NoThumb dir) (\t -> dir </> thumbFile t) . thumb
thumbFile :: FilePath -> FilePath thumbFile :: FilePath -> FilePath
thumbFile = addSuffix "_small" thumbFile = addSuffix "_small"

View file

@ -8,6 +8,7 @@ import Data.List (intersperse)
import qualified Data.List as List import qualified Data.List as List
import Data.Text.Lazy (Text) import Data.Text.Lazy (Text)
import Data.Text.Lazy.Builder (toLazyText) import Data.Text.Lazy.Builder (toLazyText)
import qualified Data.Text as Strict
import qualified Data.Text.Lazy.IO as Text import qualified Data.Text.Lazy.IO as Text
import qualified Data.YAML as YAML import qualified Data.YAML as YAML
import System.FilePath (makeRelative, takeDirectory, takeFileName) import System.FilePath (makeRelative, takeDirectory, takeFileName)
@ -56,7 +57,7 @@ main2 (SinglePage {root, file, prefix, index, dataDir, nsfw, output}) = do
writeOutput output page writeOutput output page
main2 (GalleryPage {root, files, prefix, index, output, dataDir}) = do main2 (GalleryPage {root, files, prefix, index, output, dataDir}) = do
ginfo <- galleryFromIndex index prefix (_, ginfo) <- galleryFromIndex index prefix
printV $ "gallery_info" := ginfo printV $ "gallery_info" := ginfo
infos <- mapM (infoYAML dataDir) files infos <- mapM (infoYAML dataDir) files
printV $ "infos" := infos printV $ "infos" := infos
@ -70,12 +71,12 @@ main2 (IndexPage {root, file, output}) = do
writeOutput output page writeOutput output page
main2 (RSS {files, root, index, prefix, output, dataDir}) = do main2 (RSS {files, root, index, prefix, output, dataDir}) = do
ginfo <- galleryFromIndex index prefix (name, ginfo) <- galleryFromIndex index prefix
printV $ "gallery_info" := ginfo printV $ "gallery_info" := ginfo
infos <- mapM (infoYAML dataDir) files infos <- mapM (infoYAML dataDir) files
printV $ "infos" := infos printV $ "infos" := infos
let output' = takeFileName <$> output let output' = takeFileName <$> output
let rss = RSS.make root ginfo output' infos let rss = RSS.make root name ginfo output' infos
writeOutput output rss writeOutput output rss
main2 (DependSingle {index, file, nsfw, output, prefix, buildDir, dataDir}) = do main2 (DependSingle {index, file, nsfw, output, prefix, buildDir, dataDir}) = do
@ -117,10 +118,10 @@ findInfos dataDir infoName =
readYAML :: YAML.FromYAML a => FilePath -> IO a readYAML :: YAML.FromYAML a => FilePath -> IO a
readYAML file = ByteString.readFile file >>= decode1Must file readYAML file = ByteString.readFile file >>= decode1Must file
galleryFromIndex :: FilePath -> FilePath -> IO GalleryInfo galleryFromIndex :: FilePath -> FilePath -> IO (Strict.Text, GalleryInfo)
galleryFromIndex file prefix = do galleryFromIndex file prefix = do
IndexInfo {galleries} <- readYAML file IndexInfo {title, galleries} <- readYAML file
maybe (fail $ "no gallery with prefix " ++ prefix) pure $ maybe (fail $ "no gallery with prefix " ++ prefix) (pure . (title,)) $
List.find (\g -> g.prefix == prefix) galleries List.find (\g -> g.prefix == prefix) galleries
decode1Must :: YAML.FromYAML a => FilePath -> ByteString -> IO a decode1Must :: YAML.FromYAML a => FilePath -> ByteString -> IO a

View file

@ -17,27 +17,15 @@ script (Just _) = [b|<script src=/script/nsfw-warning.js type=module></script>|]
dialog :: Maybe What -> Builder dialog :: Maybe What -> Builder
dialog Nothing = "" dialog Nothing = ""
dialog (Just what) = [b|@0 dialog (Just what) = [b|@0
<div class=dialog id=nsfw-dialog> <dialog id=nsfw-dialog>
<div class=dialog-inner>
<h1>cw: lewd art</h1> <h1>cw: lewd art</h1>
<img src=/style/stop_hand.svg>
<img class=dialog-icon src=/style/stop_hand.svg> <div> you must be an adult to view $what. no minors! </div>
<form>
<div class=dialog-message>
<p>
$what contains pornographic content that is
<strong>not suitable for minors</strong>.
<p>
by continuing, you are confirming that you are at least
<strong>eighteen years old</strong>.
</div>
<div class=dialog-buttons>
<button id=nsfw-yes class=yes>i am an adult</button> <button id=nsfw-yes class=yes>i am an adult</button>
<a href=//crouton.net referrerpolicy=no-referrer> <a href=//crouton.net referrerpolicy=no-referrer>
<button id=nsfw-no class=no>i am not</button> <button id=nsfw-no class=no>i am not</button>
</a> </a>
</div> </form>
</div> </dialog>
</div>
|] |]

View file

@ -4,8 +4,8 @@ import Date
import Info import Info
import BuilderQQ import BuilderQQ
import Data.List (sortBy) import Data.List (sortBy, intersperse)
import Data.Maybe (isJust) import Data.Maybe (catMaybes)
import Data.Function (on) import Data.Function (on)
import qualified Data.Text as Strict import qualified Data.Text as Strict
import qualified Data.Text.Lazy as Lazy import qualified Data.Text.Lazy as Lazy
@ -13,20 +13,21 @@ import System.FilePath (takeDirectory)
make :: Strict.Text -- ^ website root e.g. @https://gallery.niss.website@ make :: Strict.Text -- ^ website root e.g. @https://gallery.niss.website@
-> Strict.Text -- ^ website name e.g. @nissart@
-> GalleryInfo -> GalleryInfo
-> Maybe FilePath -- ^ output filename for self link -> Maybe FilePath -- ^ output filename for self link
-> [(FilePath, Info)] -> [(FilePath, Info)]
-> Lazy.Text -> Lazy.Text
make root ginfo output infos = make root name ginfo output infos =
toLazyText $ make' root ginfo output infos toLazyText $ make' root name ginfo output infos
make' :: Strict.Text -> GalleryInfo make' :: Strict.Text -> Strict.Text -> GalleryInfo
-> Maybe FilePath -> [(FilePath, Info)] -> Builder -> Maybe FilePath -> [(FilePath, Info)] -> Builder
make' root ginfo@(GalleryInfo {title, desc, prefix}) output infos = [b|@0 make' root name ginfo@(GalleryInfo {title, desc, prefix}) output infos = [b|@0
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> <rss version="2.0">
<channel> <channel>
<title>$title</title> <title>$name$title</title>
<link>$link</link> <link>$link</link>
<description>$desc</description> <description>$desc</description>
$selflink $selflink
@ -43,37 +44,54 @@ make' root ginfo@(GalleryInfo {title, desc, prefix}) output infos = [b|@0
filter (not . (.unlisted) . snd) infos filter (not . (.unlisted) . snd) infos
selflink = case output of selflink = case output of
Nothing -> "" Nothing -> ""
Just o -> [b|<atom:link href="$link/$o" rel="self" />|] Just o -> [b|<link href="$link/$o" rel="self" />|]
makeItem :: Strict.Text -> FilePath -> Bool -> FilePath -> Info -> Builder makeItem :: Strict.Text -> FilePath -> Bool -> FilePath -> Info -> Builder
makeItem root prefix nsfw path i@(Info {title, artist}) = [b|@4 makeItem root prefix nsfw path i@(Info {title, artist}) = [b|@4
<item> <item>
<title>$title$up</title> <title>$title$suf</title>
<link>$link</link> <link>$link</link>
<guid>$link</guid> <guid>$link</guid>
$descArtist' $body
<pubDate>$date</pubDate> <pubDate>$date</pubDate>
</item> </item>
|] |]
where where
up = if hasUpdatesFor nsfw i then [b| (updated)|] else "" suf = let parts = catMaybes [o18, cnt, up] in
if null parts then ""
else " (" <> mconcat (intersperse ", " parts) <> ")"
up = if hasUpdatesFor nsfw i then Just "updated" else Nothing
o18 = if nsfw && anyNsfw i then Just "🔞" else Nothing
cnt = let len = maybe 0 length $ allImages <$> imagesFor nsfw i in
if len == 1 then Nothing else Just [b|$len images|]
dir = takeDirectory path dir = takeDirectory path
link = [b|$root/$prefix/$dir|] link = [b|$root/$prefix/$dir|]
date = formatRSS $ latestDateFor nsfw i
artist' = ifJust artist \case artist' = ifJust artist \case
Artist {name, url = Nothing} -> [b|<p>by $name|] Artist {name, url = Nothing} -> [b|<p>by $name|]
Artist {name, url = Just url} -> [b|<p>by <a href=$url>$name</a>|] Artist {name, url = Just url} -> [b|<p>by <a href="$url">$name</a>|]
desc = descFor nsfw i desc = descFor nsfw i
desc' = makeDesc desc desc' = makeDesc desc
descArtist' = if desc.exists || isJust artist then [b|@6
<description> body = [b|@6
<![CDATA[ <description> <![CDATA[
$10.artist' $8.image
$10.desc' $8.artist'
]]> $8.desc'
</description> ]]> </description>
|]
image = case previewImage i of
Just (PFull img) -> go $ pageFile img
Just (PThumb th) -> go $ thumbFile th
Nothing -> ""
where go p = [b|@0
<figure>
<a href="$link"><img src="$link/$p"></a>
</figure>
|] |]
else ""
date = formatRSS $ latestDateFor nsfw i
makeDesc :: Desc -> Builder makeDesc :: Desc -> Builder
makeDesc NoDesc = "" makeDesc NoDesc = ""

View file

@ -9,7 +9,7 @@ import qualified NsfwWarning
import Control.Exception import Control.Exception
import Control.Monad import Control.Monad
import Data.List (sort, intersperse) import Data.List (sort, intersperse)
import Data.Maybe (fromMaybe, isNothing, isJust) import Data.Maybe (fromMaybe, isJust)
import qualified Data.Text as Strict import qualified Data.Text as Strict
import qualified Data.Text.Lazy as Lazy import qualified Data.Text.Lazy as Lazy
import System.FilePath (joinPath, splitPath) import System.FilePath (joinPath, splitPath)
@ -94,7 +94,6 @@ make' root siteName prefix nsfw _dataDir dir
let desc = case artist of let desc = case artist of
Just (Artist {name}) -> [b|by $name|] Just (Artist {name}) -> [b|by $name|]
Nothing -> "by niss" Nothing -> "by niss"
let thumbnail = getThumb "" info
let updateDate = ifJust (last' updates) \(d, _) -> let updateDate = ifJust (last' updates) \(d, _) ->
let updated = formatLong d in let updated = formatLong d in
@ -104,15 +103,17 @@ make' root siteName prefix nsfw _dataDir dir
let nsfwScript = NsfwWarning.script nsfw' let nsfwScript = NsfwWarning.script nsfw'
let nsfwDialog = NsfwWarning.dialog nsfw' let nsfwDialog = NsfwWarning.dialog nsfw'
let imageMeta = let imageMeta = case previewImage info of
if image0.sfw && isNothing image0.warning then [b|@0 Just (PFull (Image {path})) -> [b|@0
<meta property=og:image content="$url/$path0'"> <meta property=og:image content="$url/$path">
<meta name=twitter:card content=summary_large_image> <meta name=twitter:card content=summary_large_image>
<meta name=twitter:image content="$url/$path0'"> <meta name=twitter:image content="$url/$path">
|] else [b|@0 |]
<meta property=og:image content="$url/$thumbnail"> Just (PThumb path) -> [b|@0
<meta property=og:image content="$url/$path">
<meta name=twitter:card content=summary> <meta name=twitter:card content=summary>
|] |]
Nothing -> throw $ NoThumb dir
pure [b|@0 pure [b|@0
<!DOCTYPE html> <!DOCTYPE html>

View file

@ -106,23 +106,30 @@ function makeFragment() {
} }
} }
type Shortcuts = { [short: string]: Set<string> };
const shortcuts: Shortcuts = {
'summary': new Set('require_artsummary'),
'colourexamples': new Set('require_colourexample'),
'flatexamples': new Set('require_flatexample'),
'sketchexamples': new Set('require_sketchexample'),
'iconexamples': new Set('require_iconexample'),
'curated': new Set('require_curated')
};
function useFragment() { function useFragment() {
const frag = decodeURIComponent(location.hash).replace(/^#/, ''); const frag = decodeURIComponent(location.hash).replace(/^#/, '');
const details = document.getElementById('filters-details') as HTMLDetailsElement; const details = document.getElementById('filters-details') as HTMLDetailsElement;
const fromShortcut = shortcuts[frag];
if (!frag) { if (!frag) {
clearForm(); clearForm();
} else if (frag == 'all') { } else if (frag == 'all') {
allBoxes.forEach(b => b.checked = false); allBoxes.forEach(b => b.checked = false);
details.open = false; details.open = false;
} else if (fromShortcut) {
allBoxes.forEach(b => b.checked = fromShortcut.has(b.id));
} else { } else {
const pieces = const pieces = frag.split(';');
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 set = new Set(pieces);
const re = /^(require|exclude)_|hide_filters/; const re = /^(require|exclude)_|hide_filters/;
if (pieces.every(x => re.test(x))) { if (pieces.every(x => re.test(x))) {
@ -154,7 +161,7 @@ function sortFilters(cmp: (a: Node, b: Node) => number) {
function sortFiltersAlpha(e: Event) { function sortFiltersAlpha(e: Event) {
function getName(node: Node): string { function getName(node: Node): string {
if (node instanceof Element) { if (node instanceof Element) {
return node.getElementsByTagName('input')[0].value; return node.getElementsByTagName('input')[0]?.value ?? '';
} else { } else {
return ''; return '';
} }
@ -164,9 +171,9 @@ function sortFiltersAlpha(e: Event) {
} }
function sortFiltersUses(e: Event) { function sortFiltersUses(e: Event) {
function getUses(node: Node) { function getUses(node: Node): number {
if (node instanceof Element) { if (node instanceof Element) {
const countStr = node.getElementsByTagName('label')[0].dataset.count; const countStr = node.getElementsByTagName('label')[0]?.dataset.count;
return countStr ? +countStr : 0; return countStr ? +countStr : 0;
} else { } else {
return 0; return 0;

View file

@ -1,21 +1,17 @@
const nsfwOk = 'nsfw-ok'; const nsfwOk = 'nsfw-ok';
const dialog = document.getElementById('nsfw-dialog')! as HTMLDialogElement;
function dismiss() {
const dialog = document.getElementById('nsfw-dialog')!;
dialog.parentElement!.removeChild(dialog);
}
function yes() { function yes() {
localStorage.setItem(nsfwOk, '1'); localStorage.setItem(nsfwOk, '1');
dismiss(); dialog.close();
} }
function setup() { function setup() {
if (localStorage.getItem(nsfwOk)) { if (!localStorage.getItem(nsfwOk)) {
dismiss(); (dialog.querySelector('#nsfw-yes') as HTMLElement).onclick = yes;
} else {
document.getElementById('nsfw-yes')!.onclick = yes;
// nsfw-no is a normal link // nsfw-no is a normal link
dialog.showModal();
} }
} }

View file

@ -1,30 +1,22 @@
.dialog { dialog {
position: fixed; &[open] {
top: 0; left: 0; &::backdrop {
width: 100vw; height: 100vh;
margin: 0;
z-index: 1000;
background: hsl(340deg, 35%, 15%, 90%); background: hsl(340deg, 35%, 15%, 90%);
color: var(--text-col); mix-blend-mode: multiply;
display: grid; }
} ~ .page { filter: blur(15px); }
.dialog ~ .page {
filter: blur(15px);
}
.dialog-inner {
place-self: center; place-self: center;
display: grid; display: grid;
grid-template: grid-template:
"header header" "icon header"
"icon text" "icon text"
"buttons buttons" "icon buttons"
/ 1fr 3fr; / 1fr 3fr;
grid-gap: 0.5em; gap: 0.5em 1.5em;
align-items: center; place-items: center;
place-content: center;
min-height: 20vh; min-height: 20vh;
max-width: 30em; max-width: 30em;
@ -33,42 +25,40 @@
background: var(--background); background: var(--background);
border: var(--border); border: var(--border);
border-radius: var(--border-radius); border-radius: var(--border-radius);
} box-shadow: 0 0 50px #fff6;
}
.dialog h1 { color: var(--text-col);
h1 {
margin: 0; margin: 0;
grid-area: header; grid-area: header;
justify-self: center; }
}
.dialog-icon { img {
height: 3em;
width: 3em;
grid-area: icon; grid-area: icon;
justify-self: center; }
}
.dialog-message { div {
grid-area: text; grid-area: text;
justify-self: start; text-align: center;
font-size: 125%; font-size: 125%;
} }
.dialog-buttons { form {
grid-area: buttons; grid-area: buttons;
justify-self: center;
display: flex; display: flex;
justify-content: center; justify-content: center;
margin-top: 1em; margin-top: 1em;
}
.dialog-buttons > * { > * {
margin: 0 1em; margin: 0 1em;
} }
}
.dialog button { button {
border: none; border: none;
border-radius: 0.75em; border-radius: 0.75em;
padding: 0.5em 1em; padding: 0.5em 1em;
@ -81,30 +71,31 @@
position: relative; position: relative;
cursor: pointer; cursor: pointer;
}
.dialog button a { a {
inset: 0; inset: 0;
width: max-content; width: max-content;
text-decoration: none; text-decoration: none;
} }
}
.dialog .yes { .yes {
background: hsl(100deg, 70%, 80%); background: hsl(100deg, 70%, 80%);
} }
.dialog .no { .no {
background: hsl(5deg, 70%, 80%); background: hsl(5deg, 70%, 80%);
} }
.dialog p { p {
text-align: left; text-align: left;
-ms-hyphens: none; -ms-hyphens: none;
hyphens: none; hyphens: none;
} }
.dialog strong { strong {
font-weight: 800; font-weight: 800;
}
} }
@media (pointer: coarse) { @media (pointer: coarse) {

View file

@ -1,6 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"strict": true, "strict": true,
"noUncheckedIndexedAccess": true,
"noEmitOnError": true, "noEmitOnError": true,
"lib": ["ES2021", "dom"], "lib": ["ES2021", "dom"],
"target": "ES2015" "target": "ES2015"