Initial commit.
authorMark Wooding <mdw@distorted.org.uk>
Wed, 15 Dec 2021 12:35:30 +0000 (12:35 +0000)
committerMark Wooding <mdw@distorted.org.uk>
Wed, 15 Dec 2021 14:31:15 +0000 (14:31 +0000)
.skelrc [new file with mode: 0644]
mason/.perl-lib/TrivGal.pm [new file with mode: 0644]
mason/dhandler [new file with mode: 0755]
static/agpl.png [new file with mode: 0644]
static/folder.svg [new file with mode: 0644]
static/tgal.css [new file with mode: 0644]
static/tgal.js [new file with mode: 0644]

diff --git a/.skelrc b/.skelrc
new file mode 100644 (file)
index 0000000..c25a61c
--- /dev/null
+++ b/.skelrc
@@ -0,0 +1,8 @@
+;;; -*-emacs-lisp-*-
+
+(setq skel-alist
+      (append
+       '((author . "Mark Wooding")
+        (licence-text . "[[agpl-3]]")
+        (full-title . "Trivial Gallery"))
+       skel-alist))
diff --git a/mason/.perl-lib/TrivGal.pm b/mason/.perl-lib/TrivGal.pm
new file mode 100644 (file)
index 0000000..5bfc930
--- /dev/null
@@ -0,0 +1,328 @@
+### -*-cperl-*-
+###
+### Main output for Trivial Gallery.
+###
+### (c) 2021 Mark Wooding
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This file is part of Trivial Gallery.
+###
+### Trivial Gallery is free software: you can redistribute it and/or modify
+### it under the terms of the GNU Affero General Public License as
+### published by the Free Software Foundation; either version 3 of the
+### License, or (at your option) any later version.
+###
+### Trivial Gallery is distributed in the hope that it will be useful, but
+### WITHOUT ANY WARRANTY; without even the implied warranty of
+### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+### Affero General Public License for more details.
+###
+### You should have received a copy of the GNU Affero General Public
+### License along with Trivial Gallery.  If not, see
+### <https://www.gnu.org/licenses/>.
+
+package TrivGal;
+
+use autodie qw{:all};
+
+use Errno;
+use Exporter qw{import};
+use File::Path qw{make_path};
+use File::stat;
+use Image::Imlib2;
+use User::pwent;
+use POSIX;
+
+our @EXPORT;
+sub export (@) { push @EXPORT, @_; }
+
+###--------------------------------------------------------------------------
+### Internal utilities.
+
+sub read_or_set ($\$@) {
+  my ($me, $ref, @args) = @_;
+  if (@args == 0) { return $$ref; }
+  elsif (@args == 1) { $$ref = $args[0]; return $me; }
+  elsif (@args > 1) { die "too many arguments"; }
+}
+
+###--------------------------------------------------------------------------
+### Random utilities.
+
+export qw{join_paths};
+sub join_paths (@) {
+  my @p = @_;
+  my $p = "";
+  ELT: for my $e (@p) {
+    $e =~ s:^/{2,}:/:;
+    $e =~ s,([^/])/+$,$1,;
+    if ($e eq "") { next ELT; }
+    elsif ($p eq "" || $e =~ m,^/,) { $p = $e; }
+    else { $p = "$p/$e"; }
+  }
+  return $p;
+}
+
+export qw{split_path};
+sub split_path ($) {
+  my ($path) = @_;
+
+  my ($dir, $base, $ext) = $path =~ m,^(?:(.*)/)?(?:([^/]*)\.)?([^./]*)$,;
+  if (defined $base) { $ext = ".$ext"; }
+  else { $base = $ext; $ext = ""; }
+  return ($dir, $base, $ext);
+}
+
+export qw{urlencode};
+sub urlencode ($) {
+  my ($u) = @_;
+  $u =~ s:([^0-9a-zA-Z_./~-]):sprintf "%%%02x", ord $1:eg;
+  return $u;
+}
+
+export qw{urldecode};
+sub urldecode ($) {
+  my ($u) = @_;
+  $u =~ s:\%([0-9a-fA-F]{2}):chr hex $1:eg;
+  return $u;
+}
+
+###--------------------------------------------------------------------------
+### Image types.
+
+our %TYPE;
+
+package TrivGal::ImageType {
+  sub new ($$) {
+    my ($cls, $ext) = @_;
+    return $TYPE{$ext} = bless { ext => $ext }, $cls;
+  }
+  sub ext ($) {
+    my ($me, @args) = @_;
+    return $me->{ext};
+  }
+  sub mimetype ($@) {
+    my ($me, @args) = @_;
+    return TrivGal::read_or_set $me, $me->{mimetype}, @args;
+  }
+  sub imlibfmt ($@) {
+    my ($me, @args) = @_;
+    return TrivGal::read_or_set $me, $me->{imlibfmt}, @args;
+  }
+};
+
+TrivGal::ImageType->new(".jpg")->mimetype("image/jpeg")->imlibfmt("jpeg");
+TrivGal::ImageType->new(".png")->mimetype("image/png")->imlibfmt("png");
+
+###--------------------------------------------------------------------------
+### Configuration.
+
+export qw{$SCOPE $SUFFIX};
+our $SCOPE //= $::SCOPE;
+our $SUFFIX //= $::SUFFIX;
+
+export qw{$IMGROOT $CACHE $TMP};
+our $IMGROOT //= "$ENV{HOME}/publish/$SCOPE-html$SUFFIX/tgal-images";
+our $CACHE //=
+  ($ENV{XDG_CACHE_HOME} // "$ENV{HOME}/.cache") .
+  "/tgal/$SCOPE$SUFFIX";
+our $TMP //= "$CACHE/tmp";
+
+export qw{$ROOTURL $IMGURL $CACHEURL $STATICURL $SCRIPTURL};
+my $user = getpwuid($>)->name;
+our $ROOTURL //= "/~$user";
+our $IMGURL //= "$ROOTURL/tgal-images";
+our $CACHEURL //= "$ROOTURL/tgal-cache";
+our $STATICURL //= "$ROOTURL/tgal-static";
+our $SCRIPTURL;
+
+export qw{%SIZE};
+our %SIZE = (thumb => 228, view => 1200);
+
+export qw{init};
+my $initp = 0;
+sub init () {
+  my $m = HTML::Mason::Request->instance;
+  my $r = $m->cgi_request;
+
+  $m->interp->set_escape(u => sub { my ($r) = @_; $$r = urlencode $$r; });
+
+  return unless !$initp;
+
+  $SCRIPTURL //= $r->subprocess_env("SCRIPT_NAME");
+  $initp = 1;
+}
+
+###--------------------------------------------------------------------------
+### Temporary files.
+
+export qw{clean_temp_files};
+sub clean_temp_files () {
+  my $d;
+
+  eval { opendir $d, $TMP; };
+  if ($@) {
+    if ($@->isa("autodie::exception") && $@->errno == ENOENT) { return; }
+    else { die $@; }
+  }
+  my $now = time;
+  FILE: while (my $name = readdir $d) {
+    next FILE unless $name =~ /^t(\d+)\-/;
+    my $pid = $1;
+    next FILE if kill 0, $pid;
+    my $f = "$TMP/$name";
+    my $st = stat $name;
+    next FILE if $now - $st->mtime() < 300;
+    unlink $f;
+  }
+  closedir $d;
+}
+
+###--------------------------------------------------------------------------
+### Scaled images.
+
+export qw{scaled};
+sub scaled ($$) {
+  my ($scale, $path) = @_;
+
+  my $sz = $SIZE{$scale} or die "unknown scale `$scale'";
+  my $imgpath = "$IMGROOT/$path";
+  my $ist = stat $imgpath or die "no image `$path'";
+  my $thumb = "$CACHE/scaled.$scale/$path";
+  my $thumburl = "$CACHEURL/scaled.$scale/$path";
+  my $tst = stat $thumb;
+  if (defined $tst && $tst->mtime > $ist->mtime) { return $thumburl; }
+  my ($dir, $base, $ext) = split_path $thumb;
+  my $ty = $TYPE{lc $ext} or die "unknown type `$ext'";
+
+  my $img = Image::Imlib2->load($imgpath);
+  my ($wd, $ht) = ($img->width, $img->height);
+  my $max = $wd > $ht ? $wd : $ht;
+  if ($max <= $sz) { return "$IMGURL/$path"; }
+  my $sc = $sz/$max;
+  my $scaled = $img->create_scaled_image($sc*$wd, $sc*$ht);
+
+  $scaled->image_set_format($ty->imlibfmt);
+  $scaled->set_quality(90);
+  my $new = "$TMP/t${$}$ext";
+  make_path $TMP;
+  $scaled->save($new);
+  make_path $dir;
+  rename $new, $thumb;
+  return $thumburl;
+}
+
+###--------------------------------------------------------------------------
+### Directory listings.
+
+package TrivGal::Item {
+  sub new ($$) {
+    my ($cls, $name) = @_;
+    return bless { name => $name }, $cls;
+  }
+  sub name ($@) {
+    my ($me, @args) = @_;
+    return TrivGal::read_or_set $me, $me->{name}, @args;
+  }
+  sub comment ($@) {
+    my ($me, @args) = @_;
+    return TrivGal::read_or_set $me, $me->{comment}, @args;
+  }
+};
+
+export qw{listdir};
+sub listdir ($) {
+  my ($path) = @_;
+  my (@d, @f);
+  my $ix = undef;
+
+  if (-f "$path/.tgal.index") {
+    open my $f, "<", "$path/.tgal.index";
+    my $item = undef;
+    my $comment = undef;
+    LINE: while (<$f>) {
+      chomp;
+      next LINE if /^\s*(\#|$)/;
+      if (s/^\s+//) {
+       die "no item" unless $item;
+       $comment = defined $comment ? $comment . "\n" . $_ : $_;
+      } else {
+       if ($item && $comment) { $item->comment($comment); }
+       my ($indexp, $name, $c) = /(!\s+)?(\S+)\s*(\S|\S.*\S)?\s*$/;
+       $name = urldecode $name;
+       my $list;
+       if ($name =~ m!/$!) {
+         $list = \@d;
+         die "can't index a folder" if $indexp;
+       } else {
+         $list = \@f;
+         my ($dir, $base, $ext) = TrivGal::split_path $name;
+         die "unknown image type" unless $TYPE{lc $ext};
+         if ($indexp) {
+           die "two index images" if defined $ix;
+           $ix = $item;
+         }
+       }
+       $item = TrivGal::Item->new($name);
+       $comment = $c;
+       push @$list, $item;
+      }
+    }
+    if ($item && $comment) { $item->comment($comment); }
+    close $f;
+  } else {
+    opendir $d, $path;
+    my @e = readdir $d;
+    closedir $d;
+
+    ENT: for my $e (sort @e) {
+      my ($dir, $base, $ext) = split_path $e;
+      my $dotp = $e =~ /^\./;
+      my $st = stat "$path/$e";
+      my $list = undef;
+      if ($dotp || !($st->mode&0004)) { }
+      elsif (-d $st) { $list = \@d; }
+      elsif ($TYPE{lc $ext} && -f $st) { $list = \@f; }
+      $list and push @$list, TrivGal::Item->new($e);
+    }
+    $ix = $f[0] if @f;
+  }
+
+  return (\@d, \@f, $ix);
+}
+
+export qw{contents};
+sub contents ($) {
+  my ($file) = @_;
+  my $contents = "";
+  my $f;
+  local $@;
+  eval { open $f, "<", "$file"; };
+  if ($@) {
+    if ($@->isa("autodie::exception") && $@->errno == ENOENT)
+      { return undef; }
+    die $@;
+  }
+  while (sysread $f, $buf, 16384) { $contents .= $buf; }
+  close $f;
+  return $contents;
+}
+
+export qw{find_covering_file};
+sub find_covering_file ($$$) {
+  my ($top, $path, $name) = @_;
+  for (;;) {
+    my $stuff = contents "$top/$path/$name"; return $stuff if defined $stuff;
+    if ($path eq "") { return undef; }
+    if ($path =~ m!^(.*)/[^/]+/?!) { $path = $1; }
+    else { $path = ""; }
+  }
+}
+
+###----- That's all, folks --------------------------------------------------
+
+clean_temp_files;
+
+1;
diff --git a/mason/dhandler b/mason/dhandler
new file mode 100755 (executable)
index 0000000..4355b7a
--- /dev/null
@@ -0,0 +1,317 @@
+%### -*-html-*-
+%###
+%### Main output for Trivial Gallery.
+%###
+%### (c) 2021 Mark Wooding
+%###
+%
+%###----- Licensing notice --------------------------------------------------
+%###
+%### This file is part of Trivial Gallery.
+%###
+%### Trivial Gallery is free software: you can redistribute it and/or modify
+%### it under the terms of the GNU Affero General Public License as
+%### published by the Free Software Foundation; either version 3 of the
+%### License, or (at your option) any later version.
+%###
+%### Trivial Gallery is distributed in the hope that it will be useful, but
+%### WITHOUT ANY WARRANTY; without even the implied warranty of
+%### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%### Affero General Public License for more details.
+%###
+%### You should have received a copy of the GNU Affero General Public
+%### License along with Trivial Gallery.  If not, see
+%### <https://www.gnu.org/licenses/>.
+%
+%###-------------------------------------------------------------------------
+<%def .html>\
+% $r->content_type("text/html; charset=\"utf-8\"");
+<!DOCTYPE html>
+<!--
+Trivial Gallery, copyright © 2021 Mark Wooding.
+Free software: you can redistribute it and/or modify it under the terms
+of the GNU Affero General Public License.
+-->
+<html>
+<head>
+  <meta name=viewport content="width=device-width initial-scale=1.0">
+  <script type="text/javascript" src="<% "$STATICURL/tgal.js" |u %>" defer></script>
+  <link rel=stylesheet type=text/css href="<% "$STATICURL/tgal.css" |u %>">
+<% $head %>\
+  <title><% $title %></title>
+</head>
+<body>
+<% $m->content %>
+</body>
+</html>\
+%
+<%args>
+       $title
+       $head => ""
+</%args>
+</%def>
+%
+%###-------------------------------------------------------------------------
+<%def .not-found>\
+<&| .html, title => "Not found" &>
+<h1>Not found</h1>
+Failed to find &lsquo;<% $path |h %>&rsquo;.
+</&>
+% return 404;
+%
+<%args>
+       $path
+</%args>
+</%def>
+%
+%###-------------------------------------------------------------------------
+<%def .contact>\
+<%perl>
+       unless ($r->path_info =~ m!/$!) {
+         $m->redirect(join_paths($SCRIPTURL, $path) . "/");
+       }
+       my $real = join_paths $IMGROOT, $path;
+       my $url = join_paths $SCRIPTURL, $path;
+       my ($dd, $ff, $ii) = listdir $real;
+       my $links = "";
+       my $uplink;
+       if ($path eq "" || $path eq "/") { $uplink = undef; }
+       else {
+         ($uplink = $path) =~ s![^/]*/$!!;
+         $links .= sprintf "  <link rel=up href=\"%s\">\n",
+           urlencode "$SCRIPTURL/$uplink";
+       }
+       (my $nosl = $path) =~ s!/$!!;
+</%perl>
+%
+<&| .html, title => "Folder " . $m->interp->apply_escapes($nosl || "[top]", "h"),
+          head => $links &>
+<& .breadcrumbs, what => "Folder", path => $path &>
+%
+% my $note = contents "$IMGROOT/$path/.tgal-note.html";
+% if (defined $note) {
+<div class=note>
+<% $note %>
+</div>
+% }
+%
+% if (@$dd) {
+<h2>Subfolders</h2>
+<div class=gallery>
+%   for my $d (@$dd) {
+%     my ($ddd, $fff, $iii) = listdir $real . "/" . $d->name;
+%     my $tn;
+%     if ($iii) { $tn = join_paths $path, $d->name . "/" . $iii->name; }
+%     else { $tn = undef; }
+  <& .thumbnail, target => $d->name . "/", img => $tn,
+                caption => $m->interp->apply_escapes($d->name, "h"),
+                comment => $d->comment &>\
+%   }
+</div>
+% }
+%
+% if (@$ff) {
+<h2>Images</h2>
+<div class=gallery>
+%   for my $f (@$ff) {
+  <& .thumbnail, target => $f->name, img => $path . $f->name,
+                caption => $m->interp->apply_escapes($f->name, "h"),
+                comment => $f->comment &>\
+%   }
+</div>
+% }
+%
+<div class=fill></div>
+<& .footer, path => $path &>
+</&>
+%
+<%args>
+       $path
+</%args>
+</%def>
+%
+%###-------------------------------------------------------------------------
+<%def .image>\
+<%perl>
+       my ($dir, $base, $ext) = split_path $path;
+       my $real = join_paths $IMGROOT, $path;
+       my $url = join_paths $IMGURL, $path;
+       my $realdir = join_paths $IMGROOT, $dir;
+       my $urldir = join_paths $SCRIPTURL, $dir;
+       my ($dd, $ff, $ii) = listdir $realdir;
+       my $vw = scaled "view", $path;
+
+       my $fi = undef;
+       FILE: for (my $i = 0; $i < @$ff; $i++)
+         { if ($ff->[$i]->name eq "$base$ext") { $fi = $i; last FILE; } }
+       defined $fi or die "image not found in its folder?";
+       my $this = $ff->[$fi];
+
+       my %link;
+       $link{up} = "";
+       if ($fi != 0) {
+         $link{first} = $ff->[0]->name;
+         $link{prev} = $ff->[$fi - 1]->name;
+       }
+       if ($fi != @$ff - 1) {
+         $link{last} = $ff->[-1]->name;
+         $link{next} = $ff->[$fi + 1]->name;
+       }
+
+       my $links = "";
+       my $pre =
+         urlencode join_paths $SCRIPTURL, $dir;
+       for my $rel (qw{up first prev next last}) {
+         exists $link{$rel} and 
+           $links .= sprintf "  <link rel=%s href=\"%s\">\n",
+             $rel, urlencode "$pre/$link{$rel}";
+       }
+</%perl>
+%
+<&| .html, title => "Image " . $m->interp->apply_escapes($path, "h"),
+          head => $links &>
+<& .breadcrumbs, what => "Image", path => $path &>
+% if ($this->comment) {
+  <div class=comment>
+    <p><% $this->comment %>
+  </div>
+% }
+%
+<div class=viewnav>
+% if ($link{prev}) {
+  <div class=prev><a class=prev href="<% "$pre/$link{prev}" |u %>">&lsaquo;</a></div>
+% }
+  <a class=view href="<% $url |h %>">
+    <img src="<% $vw |h %>">
+  </a>
+% if ($link{next}) {
+  <div class=next><a class=next href="<% "$pre/$link{next}" |u %>">&rsaquo;</a></div>
+% }
+</div>
+%
+<div class=thumbstrip>
+% for my $f (@$ff) {
+  <& .thumbnail, target => $f->name, img => $dir . "/" . $f->name,
+                caption => $m->interp->apply_escapes($f->name, "h"),
+                focus => $f->name eq "$base$ext" &>\
+% }
+</div>
+<& .footer, path => $dir &>
+</&>
+%
+<%args>
+       $path
+</%args>
+</%def>
+%
+%###-------------------------------------------------------------------------
+<%def .breadcrumbs>\
+% $path =~ s!/$!!;
+% my @p = split m!/!, $path;
+% my $pp = "";
+% my $prev = undef;
+<h1><% $what %> \
+% if (!@p) {
+[top]
+% } else {
+<a href="<% $SCRIPTURL |u %>/">[top]</a>&thinsp;/&thinsp;\
+%   STEP: for my $p (@p) {
+%     if (defined $prev) {
+%       $pp .= "$prev/";
+<a href="<% join_paths($SCRIPTURL, $pp) |u %>/">\
+<% $prev %></a>&thinsp;/&thinsp;\
+%     }
+%     $prev = $p;
+%   }
+<% $prev %>\
+% }
+</h1>
+<%args>
+       $what
+       $path
+</%args>
+</%def>
+%
+%###-------------------------------------------------------------------------
+<%def .thumbnail>\
+% my $tn;
+% if (defined $img) { $tn = scaled "thumb", $img; }
+% else { $tn = "$STATICURL/folder.svg"; }
+% if ($focus) {
+  <div class=pic id=focusthumb>
+    <img class=thumb src="<% $tn |u %>">
+    <div class=caption><span class=name><% $caption %></span></div>
+% } else {
+  <div class=pic>
+    <a class=pic href="<% $target |u %>">
+      <img class=thumb src="<% $tn |u %>">
+      <div class=caption>
+       <span class=name><% $caption %></span>
+% if (defined $comment) {
+       <span class=comment><% $comment %></span>
+% }
+      </div>
+    </a>
+% }
+  </div>
+%
+<%args>
+       $target
+       $img
+       $caption
+       $comment => undef
+       $focus => 0
+</%args>
+</%def>
+%
+%###-------------------------------------------------------------------------
+<%def .footer>\
+<%perl>
+</%perl>
+<div class=footer>
+  <div class=footitem>
+    <a href="https://www.gnu.org/licenses/agpl-3.0.en.html"><img class=licence src="<% "$STATICURL/agpl.png" |u %>"></a>
+    Trivial Gallery, copyright &copy; 2021 Mark Wooding.
+    Free software: you can modify it and/or redistribute it under the
+    terms of the
+    <a href="https://www.gnu.org/licenses/agpl-3.0.en.html">GNU Affero
+    General Public License version 3</a>.
+    Browse or download
+    the <a href="https://git.distorted.org.uk/~mdw/tgal/">source code</a>.
+  </div>
+% my $user =
+%   find_covering_file $IMGROOT, $path, ".tgal-footer.html";
+% if (defined $user) {
+  <div class=footitem>
+<% $user %>
+  </div>
+% }
+</div>
+<%args>
+       $path
+</%args>
+</%def>
+%
+%###-------------------------------------------------------------------------
+<%once>
+       use autodie;
+use Data::Dumper;
+       use File::stat;
+
+       use TrivGal;
+</%once>
+%
+<%init>
+       TrivGal->init;
+
+       my $path = $m->dhandler_arg;
+       my $st = stat "$IMGROOT/$path";
+       my $comp;
+       if (!$st) { $comp = ".not-found"; }
+       elsif (-d $st) { $comp = ".contact"; }
+       elsif (-f $st) { $comp = ".image"; }
+       else { $comp = ".not-found"; }
+       $m->comp($comp, path => $path);
+</%init>
+%
+%###----- That's all, folks -------------------------------------------------
diff --git a/static/agpl.png b/static/agpl.png
new file mode 100644 (file)
index 0000000..ff8c3b7
Binary files /dev/null and b/static/agpl.png differ
diff --git a/static/folder.svg b/static/folder.svg
new file mode 100644 (file)
index 0000000..cb50205
--- /dev/null
@@ -0,0 +1,454 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="240"
+   height="240"
+   id="svg97"
+   sodipodi:version="0.32"
+   inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
+   sodipodi:docname="folder.svg"
+   inkscape:export-filename="/home/jimmac/Desktop/horlander-style3.png"
+   inkscape:export-xdpi="90.000000"
+   inkscape:export-ydpi="90.000000"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape"
+   version="1.1">
+  <defs
+     id="defs3">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 24 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="48 : 24 : 1"
+       inkscape:persp3d-origin="24 : 16 : 1"
+       id="perspective68" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient5060"
+       id="radialGradient6719"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(-2.774389,0,0,1.969706,112.7623,-872.8854)"
+       cx="605.71429"
+       cy="486.64789"
+       fx="605.71429"
+       fy="486.64789"
+       r="117.14286" />
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient5060">
+      <stop
+         style="stop-color:black;stop-opacity:1;"
+         offset="0"
+         id="stop5062" />
+      <stop
+         style="stop-color:black;stop-opacity:0;"
+         offset="1"
+         id="stop5064" />
+    </linearGradient>
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient5060"
+       id="radialGradient6717"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(2.774389,0,0,1.969706,-1891.633,-872.8854)"
+       cx="605.71429"
+       cy="486.64789"
+       fx="605.71429"
+       fy="486.64789"
+       r="117.14286" />
+    <linearGradient
+       id="linearGradient5048">
+      <stop
+         style="stop-color:black;stop-opacity:0;"
+         offset="0"
+         id="stop5050" />
+      <stop
+         id="stop5056"
+         offset="0.5"
+         style="stop-color:black;stop-opacity:1;" />
+      <stop
+         style="stop-color:black;stop-opacity:0;"
+         offset="1"
+         id="stop5052" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient5048"
+       id="linearGradient6715"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(2.774389,0,0,1.969706,-1892.179,-872.8854)"
+       x1="302.85715"
+       y1="366.64789"
+       x2="302.85715"
+       y2="609.50507" />
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient9806">
+      <stop
+         style="stop-color:#000000;stop-opacity:1;"
+         offset="0"
+         id="stop9808" />
+      <stop
+         style="stop-color:#000000;stop-opacity:0;"
+         offset="1"
+         id="stop9810" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient9766">
+      <stop
+         style="stop-color:#6194cb;stop-opacity:1;"
+         offset="0"
+         id="stop9768" />
+      <stop
+         style="stop-color:#729fcf;stop-opacity:1;"
+         offset="1"
+         id="stop9770" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3096">
+      <stop
+         id="stop3098"
+         offset="0"
+         style="stop-color:#424242;stop-opacity:1;" />
+      <stop
+         id="stop3100"
+         offset="1.0000000"
+         style="stop-color:#777777;stop-opacity:1.0000000;" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient319"
+       inkscape:collect="always">
+      <stop
+         id="stop320"
+         offset="0"
+         style="stop-color:#ffffff;stop-opacity:1;" />
+      <stop
+         id="stop321"
+         offset="1"
+         style="stop-color:#ffffff;stop-opacity:0;" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient1789">
+      <stop
+         style="stop-color:#202020;stop-opacity:1.0000000;"
+         offset="0.0000000"
+         id="stop1790" />
+      <stop
+         style="stop-color:#b9b9b9;stop-opacity:1.0000000;"
+         offset="1.0000000"
+         id="stop1791" />
+    </linearGradient>
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient1789"
+       id="radialGradient238"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.055022,-0.02734504,0.177703,1.190929,-3.572177,-7.125301)"
+       cx="20.706017"
+       cy="37.517986"
+       fx="20.706017"
+       fy="37.517986"
+       r="30.905205" />
+    <linearGradient
+       id="linearGradient3983">
+      <stop
+         style="stop-color:#ffffff;stop-opacity:0.87628865;"
+         offset="0.0000000"
+         id="stop3984" />
+      <stop
+         style="stop-color:#fffffe;stop-opacity:0.0000000;"
+         offset="1.0000000"
+         id="stop3985" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3983"
+       id="linearGradient491"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.516844,0,0,0.708978,-0.879573,-1.318166)"
+       x1="6.2297964"
+       y1="13.773066"
+       x2="9.8980894"
+       y2="66.834053" />
+    <linearGradient
+       gradientUnits="userSpaceOnUse"
+       y2="46.689312"
+       x2="12.853771"
+       y1="32.567184"
+       x1="13.035696"
+       gradientTransform="matrix(1.317489,0,0,0.816256,-0.879573,-1.318166)"
+       id="linearGradient322"
+       xlink:href="#linearGradient319"
+       inkscape:collect="always" />
+    <linearGradient
+       gradientUnits="userSpaceOnUse"
+       y2="6.1802502"
+       x2="15.514889"
+       y1="31.36775"
+       x1="18.112709"
+       id="linearGradient3104"
+       xlink:href="#linearGradient3096"
+       inkscape:collect="always" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient9766"
+       id="linearGradient9772"
+       x1="22.175976"
+       y1="36.987999"
+       x2="22.065331"
+       y2="32.050499"
+       gradientUnits="userSpaceOnUse" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient9806"
+       id="radialGradient9812"
+       cx="24.35099"
+       cy="41.591846"
+       fx="24.35099"
+       fy="41.591846"
+       r="19.136078"
+       gradientTransform="matrix(1,0,0,0.242494,0,31.50606)"
+       gradientUnits="userSpaceOnUse" />
+  </defs>
+  <sodipodi:namedview
+     fill="#729fcf"
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="0.10196078"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="144.11879"
+     inkscape:cy="101.28997"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:grid-bbox="true"
+     inkscape:document-units="px"
+     inkscape:window-width="1026"
+     inkscape:window-height="818"
+     inkscape:window-x="169"
+     inkscape:window-y="30"
+     inkscape:showpageshadow="false"
+     stroke="#3465a4" />
+  <metadata
+     id="metadata4">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+        <dc:date />
+        <dc:creator>
+          <cc:Agent>
+            <dc:title>Jakub Steiner</dc:title>
+          </cc:Agent>
+        </dc:creator>
+        <cc:license
+           rdf:resource="http://creativecommons.org/publicdomain/zero/1.0/" />
+        <dc:source>http://jimmac.musichall.cz</dc:source>
+        <dc:subject>
+          <rdf:Bag>
+            <rdf:li>folder</rdf:li>
+            <rdf:li>directory</rdf:li>
+          </rdf:Bag>
+        </dc:subject>
+      </cc:Work>
+      <cc:License
+         rdf:about="http://creativecommons.org/publicdomain/zero/1.0/">
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#Reproduction" />
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#Distribution" />
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
+      </cc:License>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="layer1"
+     inkscape:label="Folder"
+     inkscape:groupmode="layer"
+     transform="translate(0,192)">
+    <g
+       id="g95"
+       transform="matrix(5.1567691,0,0,5.1567691,0.04843989,-196.52972)">
+      <g
+         id="g6707"
+         transform="matrix(0.02262383,0,0,0.02086758,43.38343,36.36962)"
+         style="display:inline">
+        <rect
+           y="-150.69685"
+           x="-1559.2523"
+           height="478.35718"
+           width="1339.6335"
+           id="rect6709"
+           style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.40206185;fill:url(#linearGradient6715);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none" />
+        <path
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccc"
+           id="path6711"
+           d="m -219.61876,-150.68038 c 0,0 0,478.33079 0,478.33079 142.874166,0.90045 345.40022,-107.16966 345.40014,-239.196175 0,-132.026537 -159.436816,-239.134595 -345.40014,-239.134615 z"
+           style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.40206185;fill:url(#radialGradient6717);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none" />
+        <path
+           inkscape:connector-curvature="0"
+           style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.40206185;fill:url(#radialGradient6719);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none"
+           d="m -1559.2523,-150.68038 c 0,0 0,478.33079 0,478.33079 -142.8742,0.90045 -345.4002,-107.16966 -345.4002,-239.196175 0,-132.026537 159.4368,-239.134595 345.4002,-239.134615 z"
+           id="path6713"
+           sodipodi:nodetypes="cccc" />
+      </g>
+      <path
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="ccccccssssccc"
+         style="fill:url(#radialGradient238);fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient3104);stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="path216"
+         d="m 4.5217805,38.687417 c 0.021796,0.416304 0.4599049,0.832609 0.8762095,0.832609 h 31.327021 c 0.416302,0 0.810812,-0.416305 0.789016,-0.832609 L 36.577584,11.460682 c -0.0218,-0.416303 -0.459897,-0.832616 -0.876201,-0.832616 H 22.43051 c -0.485057,0 -1.234473,-0.315589 -1.401644,-1.1066322 L 20.417475,6.6283628 C 20.262006,5.8926895 19.535261,5.5904766 19.118957,5.5904766 H 4.3400975 c -0.4163128,0 -0.8108208,0.4163041 -0.7890249,0.8326083 z" />
+      <path
+         inkscape:connector-curvature="0"
+         style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.11363633;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none"
+         d="M 5.2265927,22.5625 H 35.492173"
+         id="path9788"
+         sodipodi:nodetypes="cc" />
+      <path
+         inkscape:connector-curvature="0"
+         style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.11363633;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000036;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none"
+         d="M 5.0421736,18.5625 H 35.489104"
+         id="path9784"
+         sodipodi:nodetypes="cc" />
+      <path
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="cc"
+         id="path9778"
+         d="M 4.9806965,12.5625 H 35.488057"
+         style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.11363633;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none" />
+      <path
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="cc"
+         id="path9798"
+         d="M 5.3861577,32.5625 H 35.494881"
+         style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.11363633;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000036;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none" />
+      <path
+         inkscape:connector-curvature="0"
+         style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.11363633;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none"
+         d="M 5.5091398,34.5625 H 35.496893"
+         id="path9800"
+         sodipodi:nodetypes="cc" />
+      <path
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="cc"
+         id="path9782"
+         d="M 5.0421736,16.5625 H 35.489104"
+         style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.11363633;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000036;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none" />
+      <path
+         inkscape:connector-curvature="0"
+         style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.11363633;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none"
+         d="M 5.0114345,14.5625 H 35.48858"
+         id="path9780"
+         sodipodi:nodetypes="cc" />
+      <path
+         inkscape:connector-curvature="0"
+         style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.11363633;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none"
+         d="M 4.9220969,10.5625 H 20.202912"
+         id="path9776"
+         sodipodi:nodetypes="cc" />
+      <path
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="cc"
+         id="path9774"
+         d="M 4.8737534,8.5624999 H 19.657487"
+         style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.11363633;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.99999982;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none" />
+      <path
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="cc"
+         id="path9794"
+         d="M 5.3246666,28.5625 H 35.493876"
+         style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.11363633;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000036;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none" />
+      <path
+         inkscape:connector-curvature="0"
+         style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.11363633;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none"
+         d="M 5.2880638,26.5625 H 35.493184"
+         id="path9792"
+         sodipodi:nodetypes="cc" />
+      <path
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="cc"
+         id="path9790"
+         d="M 5.2265927,24.5625 H 35.492173"
+         style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.11363633;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none" />
+      <path
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="cc"
+         id="path9786"
+         d="M 5.1958537,20.5625 H 35.491649"
+         style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.11363633;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none" />
+      <path
+         inkscape:connector-curvature="0"
+         style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.11363633;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000036;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none"
+         d="M 5.3246666,30.5625 H 35.493876"
+         id="path9796"
+         sodipodi:nodetypes="cc" />
+      <path
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="cc"
+         id="path9802"
+         d="M 5.5091398,36.5625 H 35.496893"
+         style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.11363633;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none" />
+      <path
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="cccccccccscccccc"
+         id="path219"
+         d="m 6.068343,38.864023 c 0.016343,0.312228 -0.1809113,0.520379 -0.4985848,0.416303 v 0 C 5.2520766,39.176251 5.033027,38.968099 5.0166756,38.65587 L 4.068956,6.5913839 C 4.0526131,6.2791558 4.2341418,6.0906134 4.5463699,6.0906134 L 18.96842,6.0429196 c 0.312228,0 0.931943,0.3004727 1.132936,1.3221818 l 0.573489,2.8155346 C 20.247791,9.715379 20.255652,9.7010175 20.037287,9.0239299 L 19.631192,7.7647478 C 19.412142,7.0371009 18.932991,6.9328477 18.620763,6.9328477 H 5.7329889 c -0.3122276,0 -0.5094814,0.2081522 -0.4931306,0.5203887 L 6.1778636,38.968099 Z"
+         style="color:#000000;display:block;overflow:visible;visibility:visible;opacity:0.45142858;fill:url(#linearGradient491);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.21380496;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none" />
+      <g
+         inkscape:export-ydpi="74.800003"
+         inkscape:export-xdpi="74.800003"
+         inkscape:export-filename="/home/jimmac/ximian_art/icons/nautilus/suse93/gnome-fs-directory.png"
+         transform="matrix(1.040764,0,0.05449252,1.040764,-8.670199,2.670594)"
+         id="g220"
+         style="fill:#ffffff;fill-opacity:0.75706213;fill-rule:nonzero;stroke:none;stroke-width:0.99946535;stroke-miterlimit:4">
+        <path
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cscscs"
+           id="path221"
+           d="m 42.417183,8.5151772 c 0.0051,-0.097113 -0.128161,-0.2469882 -0.235117,-0.2470056 l -13.031401,-0.00212 c 0,0 0.911714,0.5879545 2.201812,0.5962436 l 11.053497,0.07102 c 0.01109,-0.2117278 0.0027,-0.2560322 0.01121,-0.4181395 z"
+           style="fill:#ffffff;fill-opacity:0.50847461" />
+      </g>
+      <path
+         inkscape:connector-curvature="0"
+         inkscape:export-ydpi="74.800003"
+         inkscape:export-xdpi="74.800003"
+         inkscape:export-filename="/home/jimmac/ximian_art/icons/nautilus/suse93/gnome-fs-directory.png"
+         sodipodi:nodetypes="cscccscc"
+         id="path233"
+         d="m 39.783532,39.51062 c 1.143894,-0.04406 1.963076,-1.096299 2.047035,-2.321005 0.791787,-11.548687 1.65936,-21.231949 1.65936,-21.231949 0.07215,-0.247484 -0.167911,-0.494967 -0.48014,-0.494967 H 8.6386304 c 0,0 -1.8503191,21.866892 -1.8503191,21.866892 -0.1145551,0.982066 -0.4660075,1.804718 -1.5498358,2.183713 z"
+         style="color:#000000;display:block;visibility:visible;fill:url(#linearGradient9772);fill-opacity:1;fill-rule:nonzero;stroke:#3465a4;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none" />
+      <path
+         inkscape:connector-curvature="0"
+         style="opacity:0.46590911;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient322);stroke-width:0.9999997px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1"
+         d="m 9.6202444,16.463921 32.7910986,0.06481 -1.574046,20.001979 c -0.08432,1.071511 -0.450678,1.428215 -1.872656,1.428215 -1.871502,0 -28.677968,-0.03241 -31.394742,-0.03241 0.2335983,-0.320811 0.3337557,-0.988623 0.3350963,-1.004612 z"
+         id="path304"
+         sodipodi:nodetypes="ccsscsc" />
+      <path
+         inkscape:connector-curvature="0"
+         style="fill:#ffffff;fill-opacity:0.0892857;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+         d="M 9.6202481,16.223182 8.4536014,31.866453 c 0,0 8.2961546,-4.148078 18.6663476,-4.148078 10.370193,0 15.55529,-11.495193 15.55529,-11.495193 z"
+         id="path323"
+         sodipodi:nodetypes="ccccc" />
+    </g>
+  </g>
+  <g
+     inkscape:groupmode="layer"
+     id="layer2"
+     inkscape:label="pattern"
+     transform="translate(0,192)" />
+</svg>
diff --git a/static/tgal.css b/static/tgal.css
new file mode 100644 (file)
index 0000000..d5f5807
--- /dev/null
@@ -0,0 +1,154 @@
+/* -*-css-*-
+ *
+ * Style sheet for Trivial Gallery.
+ *
+ * (c) 2021 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of Trivial Gallery.
+ *
+ * Trivial Gallery is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Trivial Gallery is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with Trivial Gallery.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+html { height: 100%; }
+body {
+       height: calc(100% - 2ex);
+       display: flex; flex-direction: column;
+       margin-top: 0; margin-bottom: 0;
+       background-color: white;
+       color: black;
+       margin: 1ex 2em;
+}
+
+a { text-decoration: none; }
+a:link { color: blue; }
+a:link:active, a:visited { color: darkblue; }
+a:link:hover, a:visited:hover { background: #ccc; }
+
+h1 {
+       padding: 0.47ex 0;
+       border-bottom: thick black solid;
+       margin-top: 0.5ex; margin-bottom: 1.41ex;
+       font-weight: bold;
+       font-size: 200%;
+}
+h1 + h2, div.toc + h2, h1 + div > h2 {
+       border-top: none;
+       padding-top: 1ex;
+       margin-top: 0;
+}
+h2 { border-top: thin black solid; padding-top: 1ex; }
+h2, h3 { margin-top: 3ex; }
+h2 { font-size: x-large; }
+h3 { font-size: large; }
+h4, h5, h6 { display: run-in; }
+h1, h2, h3, h4, h5, h6 { font-family: sans-serif; font-weight bold; }
+
+hr { width: calc(100% - 4em); }
+div.fill { flex-grow: 1; }
+
+div.footer {
+       border-top: medium black solid;  
+       margin-top: 3.43ex;
+       padding-top: 1ex;
+       font-size: small;
+       font-style: italic;
+       text-align: right;
+}
+div.footer img.licence { float: left; margin: 1ex; }
+div.footitem {
+       margin-top: 1ex; margin-bottom: 1ex;
+       clear: both;
+}
+
+div.gallery {
+       display: block;
+       text-align: center;
+}
+
+div.pic {
+       display: inline-block;
+       vertical-align: top;
+       width: 228px;
+       margin: 1em;
+}
+
+div.pic a:link { display: inline-block; }
+
+img.thumb {
+       width: 228px; height: 228px;
+       object-fit: contain;
+}
+
+div.comment {
+       border: thin black solid;
+       max-width: 40em;
+       align-self: center;
+       background-color: #ccc;
+       padding-left: 1em; padding-right: 1em;
+       margin-top: 2ex; margin-bottom: 2ex;
+}
+
+div.caption {
+       display: block;
+       width: 228px;
+       white-space: normal;
+}
+div.caption span.name { font-family: sans-serif; }
+div.caption span.comment { font-style: italic; margin-inline-start: 1em; }
+
+div.viewnav {
+       flex-grow: 1; flex-basis: 0;
+       display: flex; flex-direction: row;
+       position: relative;
+}
+div.prev, div.next {
+       position: absolute;
+       height: 100%;
+       display: flex; flex-direction: row; align-items: center;
+}
+div.prev { left: 0%; }
+div.next { right: 0%; }
+a.prev, a.next {
+       font-size: 400%;
+       font-weight: bold;
+       background-color: #0006;
+       min-width: 1em;
+       text-align: center;
+       min-height: 3ex;
+}
+a.view {
+       flex-grow: 1; flex-basis: 0;
+       display: flex; flex-direction: column;
+}
+a:link:hover.view { background: inherit; }
+
+a.view img {
+       min-width: 0; min-height: 0;
+       max-width: 100%; max-height: 100%;
+       flex-grow: 1; flex-basis: 0;
+       object-fit: contain;
+}
+
+div.thumbstrip {
+       width: 100%;
+       overflow-x: auto;
+       text-align: center;
+       white-space: nowrap;
+}
+
+/*----- That's all, folks -------------------------------------------------*/
diff --git a/static/tgal.js b/static/tgal.js
new file mode 100644 (file)
index 0000000..de05593
--- /dev/null
@@ -0,0 +1,56 @@
+/* -*-javascript-*-
+ *
+ * Interactive features for Trivial Gallery.
+ *
+ * (c) 2021 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of Trivial Gallery.
+ *
+ * Trivial Gallery is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Trivial Gallery is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with Trivial Gallery.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+/* Handle keyboard interaction. */
+addEventListener("keydown", function (ev) {
+  var dir;
+  if (ev.key === " " || ev.key === "ArrowRight") dir = "next";
+  else if (ev.key === " " || ev.key === "ArrowRight") dir = "next";
+  else if (ev.key === "Backspace" || ev.key === "ArrowLeft") dir = "prev";
+  else if (ev.key === "^") dir = "up";
+  else if (ev.key === "<") dir = "first";
+  else if (ev.key === ">") dir = "last";
+  else return;
+  var elt = document.querySelector("link[rel=" + dir + "]");
+  if (!elt) return;
+  location = elt.getAttribute("href");
+  ev.stopPropagation();
+}, true);
+
+/* Scroll the thumbnail strip so that the current image is in the middle. */
+(function () {
+  var strip = document.querySelector("div.thumbstrip");
+  var focus = document.querySelector("#focusthumb");
+  if (strip && focus) {
+    var stripbox = strip.getBoundingClientRect();
+    var focusbox = focus.getBoundingClientRect();
+    strip.scrollLeft +=
+      (focusbox.x - stripbox.x) -
+      (stripbox.width - focusbox.width)/2;
+  }
+})();
+
+/*----- That's all, folks -------------------------------------------------*/