From ae689cc8e40090548f6c344d8054290dc6d8bb6a Mon Sep 17 00:00:00 2001 From: Ivan Mahonin Date: Aug 18 2025 13:53:34 +0000 Subject: initial commit --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9606ee5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/__pycache__ +/data/* +config.py +tplerr.txt diff --git a/config.py.example b/config.py.example new file mode 100644 index 0000000..d170f4d --- /dev/null +++ b/config.py.example @@ -0,0 +1,19 @@ + +salt = '123' +title = 'My site' + +src = 'data/src' +dst = 'data/dst' +hidedst = 'data/hidedst' +mangle = 'm/' + +url = 'https://my.site/files' +hideurl = 'https://my.site/private' + +thumbw = 120 +thumbh = 90 +thumbcolor = 0xffffff + +ffprobe = '/usr/bin/ffprobe' +ffmpeg = '/usr/bin/ffmpeg' +magick = '/usr/bin/convert' diff --git a/indexer.py b/indexer.py new file mode 100755 index 0000000..ccd5c14 --- /dev/null +++ b/indexer.py @@ -0,0 +1,240 @@ +#!/usr/bin/python3 + +# path.thumb.png - user thumb for 'path' +# path/.thumb.png - user thumb for 'path' +# path/.thumb..png - default thumb for unknown extension +# path/.thumb...png - default thumb for subdirectories +# path/.thumb.ext.png - default thumb for 'ext' +# path/.thumb/.png - default thumb for unknown extension +# path/.thumb/..png - default thumb for subdirectories +# path/.thumb/ext.png - default thumb for 'ext' + + +import os +import sys +import time +import hashlib + +import config +import thumb +import template + + +thumbext = [ 'png', 'svg', 'jpg' ] +skipname = [ 'index.html', 'indexd.html' ] + + +def join(*args): return '/'.join(a for a in args if a) +def isdir(path): return not os.path.islink(path) and os.path.isdir(path) +def isfile(path): return not os.path.islink(path) and os.path.isfile(path) + +def nameext(name): + name = os.path.basename(name) + i = name.rfind('.') + return (name[:i], name[i+1:].lower()) if i >= 0 else (name, '') +def remove(path): + if os.path.isfile(path) or os.path.islink(path): + print('remove file:', path) + os.remove(path) + if os.path.isdir(path): + for f in os.listdir(path): remove(join(path, f)) + print('remove dir:', path) + os.rmdir(path) +def mkdirfor(path): + os.makedirs(os.path.dirname(path), exist_ok = True) +def touch(path, msg = True): + if msg: print('create file:', path) + assert(not path in files) + mkdirfor(path) + files.add(path) + return path +def link(src, dst): + touch(dst, False) + if not force and isfile(dst) and os.path.samefile(src, dst): return + remove(dst) + print('create link:', dst, '(' + src + ')') + os.link(src, dst, follow_symlinks = False) +def clean(path, root = True): + if os.path.islink(path) or (os.path.isfile(path) and not path in files): + print('remove file:', path) + os.remove(path) + elif os.path.isdir(path): + for f in os.listdir(path): clean(join(path, f), False) + if not root and not os.listdir(path): + print('remove dir:', path) + os.rmdir(path) + + + +class Node: + def __init__(self, parent, src, name, hide = False): + #print('node:', src) + self.parent = parent + self.src = src + self.name = name + self.title = name if parent else config.title + self.isdir = isdir(src) + self.ext = '' if self.isdir else nameext(name)[1] + self.path = join(parent.path, name) if parent else name + self.date = os.path.getmtime(src) + self.mangle = config.mangle + hashlib.md5((config.salt + ':' + self.path).encode()).hexdigest() + '.' + self.ext + self.hide = hide or (parent and parent.hide) + self.url = join(config.url, self.path) + self.mangleurl = join(config.url, self.mangle) + self.hideurl = join(config.hideurl, self.path) if self.isdir else (self.mangleurl if self.hide else self.url) + self.thumburl = None + self.thumbs = parent.thumbs if parent else {} + self.subs = {} + if parent: parent.touchdate(self.date); parent.subs[name] = self + + def touchdate(self, date): + if self.date >= date: return + self.date = date + if self.parent: self.parent.touchdate(date) + + def scan(self): + if not self.isdir: return + + td = join(self.src, '.thumb') + if isdir(td): + for f in os.listdir(td): + n, e = nameext(f); fn = join(td, f) + if e in thumbext and isfile(fn): self.thumbs[None if n == '.' else n] = fn + + for f in os.listdir(self.src): + fn = join(self.src, f) + if f[:1] != '.' or not isfile(fn): continue + n, e = nameext(f[1:]) + if not e in thumbext: continue + n, e = nameext(n) + if n == 'thumb': self.thumbs[e] = fn + if n == 'thumb.': self.thumbs[None] = fn + + for f in os.listdir(self.src): self.sub(join(self.src, f), f, False) + hd = join(self.src, '.hide') + if isdir(hd): + for f in os.listdir(hd): self.sub(join(hd, f), f, True) + + def sub(self, src, name, hide): + n, e = nameext(name) + if ( self.isdir + and name + and name not in self.subs + and name[0] != '.' + and not name.lower() in skipname + and (self.parent or not name.lower().startswith(config.mangle)) + and nameext(n)[1] != 'thumb' + and (isfile(src) or isdir(src)) ): + Node(self, src, name, hide).scan() + + def makefile(self): + if self.isdir: return + link(self.src, join(config.dst, self.mangle)) + if not self.hide: link(self.src, join(config.dst, self.path)) + + def makethumb(self): + ts = None + td = None + dst = config.hidedst if self.hide else config.dst + url = config.hideurl if self.hide else config.url + if self.isdir: + dst = join(dst, self.path, '.thumb.') + url = join(url, self.path, '.thumb.') + else: + dst = join(dst, self.path + '.thumb.') + url = join(url, self.path + '.thumb.') + + for e in reversed(thumbext): + if isfile(join(self.src, '.thumb.' + e)): ts = join(self.src, '.thumb.' + e) + for e in reversed(thumbext): + if isfile(self.src + '.thumb.' + e): ts = self.src + '.thumb.' + e + + if not ts and not self.isdir: + self.path + '.thumb.' + if not force: + for e in reversed(thumbext): + if isfile(dst + e) and os.path.getmtime(dst + e) > self.date: td = dst + e + if td: touch(td, False) + else: + mkdirfor(dst + thumbext[0]) + td = thumb.make(self.src, dst + thumbext[0]); + if td: touch(td) + + if not ts and not td: + if self.isdir: ts = self.thumbs.get(None) + else: ts = self.thumbs.get(self.ext, self.thumbs.get('')) + if not td and ts: + td = dst + nameext(ts)[1] + link(ts, td) + if td: self.thumburl = url + nameext(td)[1] + + def makeinfo(self): + return { + 'name': self.name, + 'title': self.title, + 'date': time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(self.date)), + 'isdir': self.isdir, + 'hide': self.hide, + 'url': self.url, + 'hideurl': self.hideurl, + 'mangleurl': self.mangleurl, + 'thumburl': self.thumburl } + + def makepath(self): + r = []; p = ''; n = self + while n: r.insert(0, {'name': n.name, 'title': n.title, 'url': p}); p = join(p, '..'); n = n.parent + return r + + def makeinfoex(self, bydate): + r = self.makeinfo() + r['bydate'] = bydate + r['path'] = self.makepath() + r['tw'] = config.thumbw + r['th'] = config.thumbh + return r + + def makeindex(self, bydate): + if not self.isdir or self.hide: return + fn = join(config.dst, self.path, ('indexd.html' if bydate else 'index.html')) + if not force and isfile(fn) and os.path.getmtime(fn) > self.date: touch(fn, False); return + r = self.makeinfoex(bydate) + r['subs'] = [] + for n in sorted(self.subs.values(), key=lambda n: (not n.isdir, (-n.date if bydate else n.name))): + if not n.hide: r['subs'].append(n.makeinfo()) + with open(touch(fn), 'w') as f: tpl.write(f, r) + + def makehideindex(self, bydate): + if not self.isdir: return + fn = join(config.hidedst, self.path, ('indexd.html' if bydate else 'index.html')) + if not force and isfile(fn) and os.path.getmtime(fn) > self.date: touch(fn, False); return + r = self.makeinfoex(bydate) + r['subs'] = [ n.makeinfo() for n in sorted(self.subs.values(), key=lambda n: (not n.isdir, (-n.date if bydate else n.name), not n.hide)) ] + with open(touch(fn), 'w') as f: hidetpl.write(f, r) + + def make(self): + #print('make:', self.path) + for n in self.subs.values(): n.make() + self.makefile() + self.makethumb() + self.makeindex(False) + self.makeindex(True) + self.makehideindex(False) + self.makehideindex(True) + + +#os.umask(0o0027) +force = len(sys.argv) > 1 and sys.argv[1] == 'force' +files = set() + +tpl = template.TplLoader.load("tpl/index.tpl") +hidetpl = template.TplLoader.load("tpl/hide.tpl") + +n = Node(None, config.src, '') +n.scan() +n.make() + +clean(config.dst) +clean(config.hidedst) + +print('done') + diff --git a/template.py b/template.py new file mode 100644 index 0000000..5e58f69 --- /dev/null +++ b/template.py @@ -0,0 +1,436 @@ + +import os +import html +#import markdown +#os.path.dirname(os.path.abspath(__file__)) + + +def dictGet(d, path): + for k in str(path).split('.'): + if type(d) is list or type(d) is tuple: + d = d[int(k)] + continue + elif type(d) is dict: + if (not k in d) and ('ex' in d): + print("query ex for field: %s (%s)" % (path, k)) + d = d['ex'](d) + if k in d: + d = d[k] + continue + print("variable not found: %s (%s)" % (path, k)) + return None + return d + +def tostr(s): + return '' if s is None else str(s) +def text2html(t): + return html.escape(tostr(t)).replace("\n", "
") +def md2html(t): + return markdown.markdown(tostr(t)) + + +tabsym = ' -- ' + + +class TemplateText: + def __init__(self, text = ''): + self.text = text + def write(self, writer, context): + writer.write(self.text) + def optimize(self): + return self if self.text else None + def dbgPrint(self, tab): + print("%stext(%s)" % (tab, self.text.replace("\n", "\\n"))) + +class TemplateVarHtml: + def __init__(self, varpath = None): + self.varpath = varpath + def write(self, writer, context): + writer.write(tostr(dictGet( context, self.varpath ))) + def optimize(self): + return self + def dbgPrint(self, tab): + print("%svarHtml(%s)" % (tab, self.varpath)) + +class TemplateVarMd: + def __init__(self, varpath = None): + self.varpath = varpath + def write(self, writer, context): + writer.write(md2html(dictGet( context, self.varpath ))) + def optimize(self): + return self + def dbgPrint(self, tab): + print("%svarMd(%s)" % (tab, self.varpath)) + +class TemplateVarText: + def __init__(self, varpath = None): + self.varpath = varpath + def write(self, writer, context): + writer.write(text2html(dictGet( context, self.varpath ))) + def optimize(self): + return self + def dbgPrint(self, tab): + print("%svarText(%s)" % (tab, self.varpath)) + +class TemplateVarTextMultiline: + def __init__(self, varpath = None): + self.varpath = varpath + def write(self, writer, context): + writer.write(text2html(dictGet( context, self.varpath )).replace("\n", "
")) + def optimize(self): + return self + def dbgPrint(self, tab): + print("%svarTextMultiline(%s)" % (tab, self.varpath)) + +class TemplateVarCount: + def __init__(self, varpath = None): + self.varpath = varpath + def write(self, writer, context): + v = dictGet( context, self.varpath ) + writer.write(str(len(v) if hasattr(v, '__len__') else 0)) + def optimize(self): + return self + def dbgPrint(self, tab): + print("%svarCount(%s)" % (tab, self.varpath)) + +class TemplateVarField: + def __init__(self, varpath = None): + self.varpath = varpath + def write(self, writer, context): + f = self.varpath.split('.')[-1] + writer.write("

%s: %s

" % ( + text2html( f ), + text2html( dictGet(context, self.varpath) ) )) + def optimize(self): + return self + def dbgPrint(self, tab): + print("%svarField(%s)" % (tab, self.varpath)) + +class TemplateIf: + def __init__(self, varpath = None, sub = None, alt = None): + self.varpath = varpath + self.sub = sub + self.alt = alt + def write(self, writer, context): + if dictGet(context, self.varpath): + if self.sub: self.sub.write(writer, context) + else: + if self.alt: self.alt.write(writer, context) + def optimize(self): + self.sub = self.sub.optimize() if self.sub else None + self.alt = self.alt.optimize() if self.alt else None + return self if self.sub or self.alt else None + def dbgPrint(self, tab): + print("%sif(%s):" % (tab, self.varpath)) + if self.sub: + self.sub.dbgPrint(tab + tabsym) + if self.alt: + print("%selse:" % tab) + self.alt.dbgPrint(tab + tabsym) + + +class TemplateWith: + def __init__(self, varpath = None, varname = None, sub = None): + self.varpath = varpath + self.varname = varname + self.sub = sub + def write(self, writer, context): + if self.sub: + context = dict(context) + context[self.varname] = dictGet(context, self.varpath) + self.sub.write(writer, context) + def optimize(self): + self.sub = self.sub.optimize() if self.sub else None + return self if self.sub else None + def dbgPrint(self, tab): + print("%swith(%s:%s):" % (tab, self.varpath, self.varname)) + if self.sub: self.sub.dbgPrint(tab + tabsym) + + +class TemplateComment: + def __init__(self, text = None): + self.text = text + def write(self, writer, context): + pass + def optimize(self): + return None + def dbgPrint(self, tab): + print("%scomment(%s)" % (texttab, self.text)) + + +class TemplateLoop: + def __init__(self, varpath = None, keyvarname = None, varname = None, sub = None, sep = None): + self.varpath = varpath + self.keyvarname = keyvarname + self.varname = varname + self.sub = sub + self.sep = sep + def writeItem(self, writer, context, first, k, v): + if self.sep and not first: + self.sep.write(writer, context) + if self.sub: + ctx = dict(context) + ctx[self.keyvarname] = k + ctx[self.varname] = v + self.sub.write(writer, ctx) + def write(self, writer, context): + if self.sep or self.sub: + vv = dictGet(context, self.varpath) + if type(vv) is dict: + f = True + keys = list(vv.keys()) + keys.sort() + for k in keys: + self.writeItem(writer, context, f, k, vv[k]) + f = False + elif hasattr(vv, '__iter__'): + idx = 0 + for v in vv: + self.writeItem(writer, context, not idx, idx, v) + idx = idx + 1 + def optimize(self): + self.sub = self.sub.optimize() if self.sub else None + self.sep = self.sep.optimize() if self.sep else None + return self if self.sub or self.sep else None + def dbgPrint(self, tab): + print("%sloop(%s:%s:%s):" % (tab, self.varpath, self.keyvarname, self.varname)) + if self.sub: + self.sub.dbgPrint(tab + tabsym) + if self.sep: + print("%ssep:" % tab) + self.sep.dbgPrint(tab + tabsym) + +class Template: + def __init__(self): + self.items = [] + def write(self, writer, context): + for i in self.items: + i.write(writer, context) + def optimize(self): + it = [] + for i in self.items: + ii = i.optimize() + if ii: + if it and type(ii) is TemplateText and type(it[-1]) is TemplateText: + it[-1].text = it[-1].text + ii.text + else: + it.append(ii) + self.items = it + if len(self.items) == 1: return self.items[0] + return self if self.items else None + def dbgPrint(self, tab): + print("%stemplate:" % tab) + for i in self.items: + i.dbgPrint(tab + tabsym) + + +class TplLoader: + @staticmethod + def loadtext(filename): + filename = os.path.abspath(filename) + path = os.path.dirname(filename) + res = '' + text = '' + with open(filename, 'r') as f: text = f.read() + ii = 0 + i = 0 + while i < len(text): + if text[i:i+9] == '{include:': + res = res + text[ii:i] + a = i+9 + b = text.find('}', a) + if b<0: raise Exception("include parse error: %s:%d" % (filename, a)) + fn = text[a:b] + res = res + TplLoader.loadtext(path +'/' + fn) + i = b+1 + ii = i + else: + i = i + 1 + return res + text[ii:i] + + def __init__(self, filename): + self.filename = os.path.abspath(filename) + self.text = TplLoader.loadtext(self.filename) + self.ptr = 0 + + def error(self): + with open('tplerr.txt', 'w') as f: f.write(self.text) + raise Exception("parse error: %s:%d" % (self.filename, self.ptr)) + + def readKey(self, k): + if self.text[self.ptr:self.ptr + len(k)] == k: + self.ptr = self.ptr + len(k) + return True + return False + + def readVarname(self): + i = self.ptr + while i < len(self.text) and (self.text[i].isalnum() or self.text[i] in '_'): + i = i + 1 + if i == self.ptr: return None + i, self.ptr = self.ptr, i + return self.text[i:self.ptr] + + def readVarpath(self): + i = self.ptr + varpath = None + while True: + varname = self.readVarname() + if not varname: + if varpath: self.error() + break + varpath = varpath + '.' + varname if varpath else varname + if not self.readKey('.'): break + if not varpath: + self.ptr = i + return varpath + + def loadVarText(self): + if not self.readKey("{:"): return None + varpath = self.readVarpath() + if not varpath or not self.readKey("}"): self.error() + return TemplateVarText(varpath) + + def loadVarTextMultiline(self): + if not self.readKey("{t:"): return None + varpath = self.readVarpath() + if not varpath or not self.readKey("}"): self.error() + return TemplateVarTextMultiline(varpath) + + def loadVarHtml(self): + if not self.readKey("{html:"): return None + varpath = self.readVarpath() + if not varpath or not self.readKey("}"): self.error() + return TemplateVarHtml(varpath) + + def loadVarMd(self): + return None # disabled + if not self.readKey("{md:"): return None + varpath = self.readVarpath() + if not varpath or not self.readKey("}"): self.error() + return TemplateVarMd(varpath) + + def loadVarCount(self): + if not self.readKey("{count:"): return None + varpath = self.readVarpath() + if not varpath or not self.readKey("}"): self.error() + return TemplateVarCount(varpath) + + def loadVarField(self): + if not self.readKey("{f:"): return None + varpath = self.readVarpath() + if not varpath or not self.readKey("}"): self.error() + return TemplateVarField(varpath) + + def loadIf(self): + if not self.readKey("{if:"): return None + varpath = self.readVarpath() + if not varpath or not self.readKey("}"): self.error() + res = TemplateIf(varpath, Template(), Template()) + e = False + while True: + if self.readKey("{endif}"): break + if self.readKey("{else}"): + if e: self.error() + e = True + continue + item = self.loadTemplate() + if not item: self.error() + if e: + res.alt.items.append(item) + else: + res.sub.items.append(item) + return res + + def loadWith(self): + if not self.readKey("{with:"): return None + varpath = self.readVarpath() + if not varpath or not self.readKey(":"): self.error() + varname = self.readVarname() + if not varname or not self.readKey("}"): self.error() + res = TemplateWith(varpath, varname, Template()) + while True: + if self.readKey("{endwith}"): break + item = self.loadTemplate() + if not item: self.error() + res.sub.items.append(item) + return res + + def loadComment(self): + if not self.readKey("{comment}"): return None + res = TemplateComment() + i = self.text.find('{endcomment}', self.ptr) + if i < 0: + i = len(self.text) + res = TemplateComment(self.text[self.ptr:i]) + self.ptr = i + if not self.readKey("{endcomment}"): self.error() + return res + + def loadLoop(self): + if not self.readKey("{loop:"): return None + varpath = self.readVarpath() + if not varpath or not self.readKey(":"): self.error() + keyvarname = self.readVarname() + if not self.readKey(":"): self.error() + varname = self.readVarname() + if not varname or not self.readKey("}"): self.error() + res = TemplateLoop(varpath, keyvarname, varname, Template(), Template()) + s = False + while True: + if self.readKey("{endloop}"): break + if self.readKey("{sep}"): + if s: self.error() + s = True + continue + item = self.loadTemplate() + if not item: self.error() + if s: + res.sep.items.append(item) + else: + res.sub.items.append(item) + return res + + def loadTemplate(self): + t = self.loadVarText() + if t: return t + t = self.loadVarTextMultiline() + if t: return t + t = self.loadVarHtml() + if t: return t + t = self.loadVarMd() + if t: return t + t = self.loadVarCount() + if t: return t + t = self.loadVarField() + if t: return t + t = self.loadIf() + if t: return t + t = self.loadWith() + if t: return t + t = self.loadComment() + if t: return t + t = self.loadLoop() + if t: return t + + i = self.text.find('{', self.ptr) + if i == self.ptr: + i = self.text.find('{', self.ptr+1) + if i < 0: + i = len(self.text) + if i <= self.ptr: + return None + i, self.ptr = self.ptr, i + return TemplateText(self.text[i:self.ptr]) + + @staticmethod + def load(filename): + l = TplLoader(filename) + t = Template() + while True: + tt = l.loadTemplate() + if not tt: break + t.items.append(tt) + if l.ptr < len(l.text): l.error() + t = t.optimize() + return t if t else TemplateText() diff --git a/thumb.py b/thumb.py new file mode 100644 index 0000000..046a1fe --- /dev/null +++ b/thumb.py @@ -0,0 +1,61 @@ + +import os +import subprocess + +import config + + +env = dict(os.environ) +env['LANG']='C' + + +def printerr(r): + print('process returned with error:', r.returncode) + print('command:', r.args) + print('output:', r.stdout) + print('error:', r.stderr) + return + + +def ffmpeg(src, dst): + r = subprocess.run( + [ config.ffprobe, '-v', 'error', '-show_entries', 'format=duration:stream=codec_type', '-of', 'default=noprint_wrappers=1', src ], + capture_output = True, text = True, env = env ) + if r.returncode: return #printerr(r) + video = False; audio = False; duration = 0 + for l in r.stdout.split(): + f = l.split('=', 1) + if len(f) != 2: continue + if l == 'codec_type=video': video = True + if l == 'codec_type=audio': audio = True + if l.startswith('duration='): + try: duration = float(l[9:]) + except ValueError: pass + if not duration: return + if video: + filters = "select='gte(t,%f)',thumbnail,scale=%d:%d:force_original_aspect_ratio=decrease:force_divisible_by=2" % (duration/4, config.thumbw, config.thumbh) + r = subprocess.run( + [ config.ffmpeg, '-i', src, '-vf', filters, '-frames:v', '1', '-y', dst ], + capture_output = True, text = True, env = env ) + #if r.returncode: printerr(r) + if not r.returncode and os.path.isfile(dst): return dst + if audio: + filters = "showwavespic=s=%dx%d:scale=sqrt:colors=%06x" % (config.thumbw, config.thumbh, config.thumbcolor) + r = subprocess.run( + [ config.ffmpeg, '-i', src, '-filter_complex', filters, '-frames:v', '1', '-y', dst ], + capture_output = True, text = True, env = env ) + #if r.returncode: printerr(r) + if not r.returncode and os.path.isfile(dst): return dst + + +def magick(src, dst): + r = subprocess.run( + [ config.magick, src, '-resize', '%dx%d>' % (config.thumbw, config.thumbh), dst ], + capture_output = True, text = True, env = env ) + if r.returncode: return #printerr(r) + if not r.returncode and os.path.isfile(dst): return dst + + +def make(src, dst): + return ffmpeg(src, dst) or magick(src, dst) + diff --git a/tpl/css.tpl b/tpl/css.tpl new file mode 100644 index 0000000..29c1687 --- /dev/null +++ b/tpl/css.tpl @@ -0,0 +1,63 @@ + +body { + font-family: sans; + text-align: center; + font-size: 16px; + background: black; + color: white; +} + +a { font-weight: bold; color: #88f; } +a:visited { color: #66d; } +a:hover { color: #44c; } + +body>div { + text-align: left; + margin: 32px; + border-radius: 16px; + border: 1px solid gray; + display: inline-block; + background: #111; +} +#path { margin: 10px 10px 10px calc({:tw}px + 90px); } + +#head div:first-child { + margin-left: 70px; + width: {:tw}px; + height: {:th}px; + text-align: right; +} +#head div { + font-size: 32px; + font-weight: bold; + vertical-align: middle; + display: inline-block; + margin: 10px; +} +#head img { + display: block; + max-width: {:tw}px; + max-height: {:th}px; + margin: auto; +} + +table { + counter-set: row -1; + border: 0; + border-spacing: 0; + border-radius: 0 0 16px 16px; + overflow: hidden; +} +tr { counter-increment: row; height: calc({:th}px + 10px); } +tr:nth-child(odd) { background-color: #222; } +tr:nth-child(1) { background-color: #333; height: 50px; } +td:first-child::before { content: counter(row); } +th:first-child { padding-left: 20px; padding-right: 10px; width: 30px; text-align: right; } +th:nth-child(3) { padding-left: 10px; } +th:last-child { padding-right: 20px; } +td { padding: 5px; font-size: 16px; } +td:first-child { padding-left: 20px; padding-right: 10px; width: 30px; text-align: right; } +td:nth-child(2) { width: calc({:tw}px + 10px); } +td:nth-child(3) { padding-left: 10px; } +td:last-child { padding-right: 20px; } +td img { max-width: {:tw}px; max-height: {:th}px; display: block; margin: auto; } diff --git a/tpl/hide.tpl b/tpl/hide.tpl new file mode 100644 index 0000000..1e73bc6 --- /dev/null +++ b/tpl/hide.tpl @@ -0,0 +1,25 @@ + + + {:title} + + +
+
{loop:path::p}{:p.title}{sep} / {endloop}
+ + + + + + + + + + {loop:subs::n} + + + + + + {endloop} +
#{if:bydate}name{else}name{endif}{if:bydate}date{else}date{endif}
{:n.title}mangled{:n.date}
+
diff --git a/tpl/hidecss.tpl b/tpl/hidecss.tpl new file mode 100644 index 0000000..4022841 --- /dev/null +++ b/tpl/hidecss.tpl @@ -0,0 +1,3 @@ +.hide { --strip: calc(({:th}px + 10px)*0.707106781/4); } +.hide:nth-child(odd) { background: repeating-linear-gradient(45deg, #222, #222 var(--strip), #1c1c1c var(--strip), #1c1c1c calc(var(--strip)*2)); } +.hide:nth-child(even) { background: repeating-linear-gradient(45deg, #151515, #151515 var(--strip), #111 var(--strip), #111 calc(var(--strip)*2)); } diff --git a/tpl/index.tpl b/tpl/index.tpl new file mode 100644 index 0000000..75d26c9 --- /dev/null +++ b/tpl/index.tpl @@ -0,0 +1,25 @@ + + + {:title} + + +
+
{loop:path::p}{:p.title}{sep} / {endloop}
+ + + + + + + + + + {loop:subs::n} + + + + + + {endloop} +
#{if:bydate}name{else}name{endif}{if:bydate}date{else}date{endif}
{:n.title}mangled{:n.date}
+