Skip to content

This website

https://materialdrain.senko.tools has two parts: the home page at /, with the downloads, and this documentation at /docs/. Both are made from the website branch, which shares no history with the app's branches.

Where things are

Path (on website) What it is
website/ The home page: index.html, style.css, releases.js and 404.html. Plain HTML, CSS and JavaScript, no build step. Also robots.txt and the icons (below)
docs/ These pages, in Markdown, built with MkDocs Material
mkdocs.yml The documentation's settings: the theme, the Markdown extensions and the navigation
docs/requirements.txt The exact versions of MkDocs and Material the site is built with
docs/stylesheets/extra.css The documentation's colours, the same palette as website/style.css
hooks/page_info.py Adds to every page as it's built: the line at its end (when it was last changed, and the app's version then), the Nightly labels, and where a name in a table may break over lines on a phone
overrides/ The parts of the theme the site changes: partials/source-file.html, which shows that line
docs/server-setup/ How the server serves the site, and the files it needs. Not published
.github/workflows/site.yml Builds the site on every push to website, into the site branch
preview.ps1 Builds the whole site as the workflow does and serves it locally (see Previewing it)

Why a branch of its own: the documentation changes far more often than the app, and a change to it should never start an app build or appear in the app's history. The app's branches (main, dev) hold only the app; see Development.

How it's published

flowchart LR
    push[Push to website] --> build[site.yml builds it]
    build -->|force-push, one commit| site[site branch]
    server[The server, every 5 minutes] -->|git fetch| site
    server -->|the releases| releases["/releases.json"]
    server --> nginx[nginx serves /var/www/materialdrain]
  1. A push to website runs the Website workflow (.github/workflows/site.yml). Run workflow on the Actions tab runs it without a change. A newer push cancels a build that's still running.
  2. The build installs the pinned requirements, then:

    mkdocs build --strict --site-dir _site/docs
    cp -r website/. _site/
    

    The documentation goes to _site/docs, and the home page's files to the root of _site. 3. The result is force-pushed to site as a single new commit, "Site built from <commit>". The branch is replaced every time, so it never grows. The workflow only needs permission to write to the repository, no secrets. 4. The server pulls site. A systemd timer (materialdrain-site-sync.timer) runs a small script every 5 minutes (and a minute after booting). It fetches the branch; when the commit is a new one, it resets /var/www/materialdrain to it and drops the old builds so the folder doesn't grow. nginx serves that folder. The same script saves the app's releases for the download page (see The downloads).

So a change shows on the site within about 5 minutes of its build finishing.

The server only ever reads from GitHub: nothing on GitHub can reach it, and the sync runs in a systemd sandbox that can only write the site's folder and the releases' one. Setting up a server is described in docs/server-setup/README.md.

Never edit site by hand

site is output. The next build replaces it entirely, so a change made there is lost within minutes. Change website instead.

Previewing it

You need Python 3 (the workflow uses 3.12). The simplest way, on Windows or anywhere with PowerShell, is the script at the root of the branch:

./preview.ps1

It builds the whole site as the workflow does, into _site, with --strict, and serves it at http://127.0.0.1:8000: the home page at /, the documentation at /docs/, as on the real site. It also reads the releases from GitHub into _site/releases.json, as the server's sync does. Run it again after a change. The first time, it makes a virtual environment in .venv with the versions in docs/requirements.txt (again whenever that file changes). -Port 8080 serves on another port, -NoServe only builds, and -Offline leaves out the releases, so the home page asks GitHub itself.

What it can't show is what only nginx does: the 404 pages, and the headers for caching and security.

By hand, from the root of a checkout of website:

python3 -m venv .venv
source .venv/bin/activate
pip install -r docs/requirements.txt
mkdocs serve

mkdocs serve shows the documentation at http://127.0.0.1:8000 and reloads on every change you save. Before pushing, build it the way the workflow does, so a mistake fails here and not there:

mkdocs build --strict

The home page asks for its files from the root of the site (/style.css, /releases.js), so serve its folder rather than opening the file:

python3 -m http.server --directory website 8001

Its Documentation link (/docs/) only works on the real site, or if you run the workflow's two build commands and serve _site instead, which preview.ps1 does. site/, _site/ and .venv/ are in .gitignore.

The versions in docs/requirements.txt are pinned, so a new release of MkDocs or Material can't change or break the site without anyone noticing. Raise them on purpose, and check the build when you do.

Adding a page

  1. Create the Markdown file under docs/, in the folder of its section, e.g. docs/development/my-topic.md. Start it with a single # title.
  2. Add it to nav in mkdocs.yml, where it should appear:

    nav:
      - Development:
          - development/index.md
          - My topic: development/my-topic.md
    

    A section's index.md is listed without a title: it becomes the section's own page (navigation.indexes). 3. Link to it from the pages it belongs with. Links are relative paths to the Markdown files, with an optional anchor: [Conventions](conventions.md#tests), [Hosts](../hosts/index.md).

The build is strict: a link to a page that doesn't exist, or a nav entry for a missing file, fails it, and nothing is published until it's fixed. A page that's in docs/ but deliberately not in the navigation goes under not_in_nav in mkdocs.yml (like the old provider-configuration.md); files that aren't pages at all go under exclude_docs (like server-setup/).

Writing a page

Match the pages around it: plain words, short sentences, and the reason behind things, not just the steps. Tables for reference, a diagram only where it shows something words don't, and code blocks with their language (```kotlin, ```json, ```bash).

These Markdown extensions are turned on in mkdocs.yml:

Extension For Written as
admonition A boxed note, tip or warning !!! note "Title", then the text indented by four spaces
pymdownx.details A box that opens on a click ??? note "Title" (closed) or ???+ note (open)
pymdownx.tabbed Tabs, for alternatives === "First tab", then the content indented
pymdownx.superfences Code blocks inside lists, tabs and notes, and Mermaid diagrams ```mermaid
pymdownx.highlight, pymdownx.inlinehilite Coloured code, also inline ```kotlin, `#!kotlin val x = 1`
pymdownx.keys Keys to press ++ctrl+c++
tables Tables The usual pipes
def_list A term and its meaning Term on one line, : Meaning on the next
attr_list, md_in_html Classes on Markdown, Markdown inside HTML [Download](…){ .md-button }, <div class="grid cards" markdown>
toc The table of contents on the right, and a link on every heading Headings (##, ###)

A few of them, written out:

!!! note "Pixeldrain itself"
    The built-in Pixeldrain host isn't a config, so `screens` doesn't apply to it.

Use them sparingly: a page full of boxes has nothing that stands out. note, tip, info and warning are the usual kinds.

=== "WebDAV"

    Text about WebDAV.

=== "S3"

    Text about S3.
??? info "Why it works this way"
    Hidden until it's clicked.
```mermaid
flowchart LR
    app --> api[provider-api]
```

flowchart and sequenceDiagram are the ones used so far.

Press ++ctrl+c++ to copy.

Renders as: Press Ctrl+C to copy.

Stable and Nightly

The pages describe the app as it is on dev, the Nightly build. Whatever it has that the newest stable release doesn't yet is marked with <span class="nightly" title="Only in the Nightly build so far, not in a stable release yet">Nightly</span>, written where the label should be, which hooks/page_info.py turns into a small yellow Nightly with a tooltip:

| **Compact player** <span class="nightly" title="Only in the Nightly build so far, not in a stable release yet">Nightly</span> | The player opens as a card a third of the screen high |

Mark the feature, not every mention of it: a whole table under a marked heading needs no label on each row. When a stable release has those changes, take the labels out (grep -rn "<span class="nightly" title="Only in the Nightly build so far, not in a stable release yet">Nightly</span>" docs), and the sentences that say how a stable release differs ("In a stable release, …").

The line at the end of a page

Every page ends with when it was last changed and the newest stable release then: Last updated 23 October 2026 · For Materialdrain 1.5.20261014.1558, and and the Nightly build where it says so on a page with Nightly labels. hooks/page_info.py works both out from git, at build time:

  • The date is the page's last commit in this branch (git log -1 --follow), so it changes only when the page does. A page with changes not committed yet says so, with today's date, as in a local preview.
  • The version is the newest of the app's release tags (v…) whose commit is no newer than that date. The tags are on the commits the releases were built from, which share this repository, so nothing is asked of GitHub. A page last changed before a release still names the older one, which is the point: it says which version it was written for.

The workflow checks out the whole history and the tags for it (fetch-depth: 0); a shallow checkout would give every page the date of the last commit and no version. overrides/partials/source-file.html replaces Material's own line with this one.

The theme's features (mkdocs.yml, theme.features) add instant page loads, tabs for the top-level sections, "back to top", search suggestions, a copy button on every code block, and tabs that switch together across a page when they have the same title (content.tabs.link).

The colours

The whole site is dark, in one palette: a dark rose. It's defined twice, in the same CSS variables with the same values:

  • website/style.css for the home page.
  • docs/stylesheets/extra.css for the documentation, which maps them onto Material's own variables (--md-default-bg-color, --md-typeset-a-color, the code colours and so on). mkdocs.yml sets the scheme to slate (dark) with custom primary and accent colours, so these are the only colours used.
Variable Value Used for
--bg-main #1a1617 The page
--bg-surface #251f21 The header, cards, code blocks, notes
--accent-primary #d4a5b2 Accents, keywords in code
--accent-highlight #eec1c8 The site's name in the header
--link-color #e2b2be Links
--text-main #f2ecee Text
--text-muted #b0a2a6 Secondary text

The rest (borders, the deeper and muted accents, the blue, red, green and yellow) are in both files. To change a colour, change it in both, so the home page and the documentation stay one site.

The downloads

The home page's download cards are filled in by website/releases.js, in the visitor's browser, from the repository's GitHub releases. The app's build workflow publishes those (see Development); the website only lists them, and every download comes straight from GitHub.

  1. The server reads the list. Every 5 minutes the sync script asks https://api.github.com/repos/fidesosu/Materialdrain/releases?per_page=30 and saves the answer as /var/www/materialdrain-releases/releases.json, which nginx serves as /releases.json. It's written on every check, so its Last-Modified is when the server last asked GitHub; a failed check keeps the last good list and its time. Visitors never ask GitHub themselves, so its limit of 60 requests an hour for each visitor doesn't matter. The file is apart from the site's checkout, which git resets and cleans on every update.
  2. The page reads /releases.json, and again every minute while it's open and whenever it's shown again after being in the background. The cards are only drawn again when the server has checked since. Where there's no /releases.json (a local preview, or a server not set up for it yet) the page asks GitHub once, itself.
  3. Drafts and releases without an .apk are left out.
  4. Stable is the newest release that isn't a pre-release and isn't tagged dev-build. Nightly is the release tagged dev-build, the development build (it's rebuilt with every push to dev, not once a night). Up to 4 older stable releases are listed under Older versions ("None yet." when there are none).
  5. Each card links straight to its APK ("Download APK" at the end of its title's line), and shows the version, the date and the size, and a line on what the build is, which stays after the date comes in. The version is read from the APK's name (Materialdrain-1.4.20261009.1530.apk gives 1.4.20261009.1530); releases from before the build workflow named their APKs differently, so for those it's the tag without its v. The date is when the APK was uploaded, not when the release was published: the dev-build release is published once and then only edited, so its own date stays the first development build's, while each push to dev uploads a new APK.
  6. Under each card: What's new (the release's page on GitHub, with its notes) and the APK's SHA-256, shortened, with a button that copies all of it, to check a download against. GitHub records the checksum (digest) of uploads since mid-2025; for older ones the line has only the notes. The older versions have What's new beside their version, and their checksum in the version's tooltip.
  7. Next check in … beside the Download heading counts down, by the second, to the server's next read of the list from GitHub: 5 minutes after its last, which is when releases.json was written. Its tooltip has the time of the last one. Once that's due it says Checking GitHub…, and the page asks for the list every 10 seconds until the server's is newer (its timer can be up to 30 seconds late). When the page had to ask GitHub itself, there's no next check, and it says so. From 20 minutes without a check the server's checks are failing (journalctl -u materialdrain-site-sync.service says why): the line says Couldn't check GitHub lately, and under the cards the page says a newer build may be missing. When the list couldn't be loaded it stays empty.

If neither the server's list nor GitHub can be read, the page says so and the cards keep their plain links to the releases page, which are in the HTML from the start. So the page works, if more plainly, without the script.

A card for a build that isn't published yet is dimmed and has no link, so it can't be clicked or reached with the keyboard.

  • website/icon.svg is the app's launcher icon, drawn from the app's own pixeldrain_launcher.xml on its background colour (#97B57C), cut to the part a launcher shows. It's the favicon of both the home page and the documentation (theme.favicon: /icon.svg in mkdocs.yml), and the logo before the title.
  • favicon.ico (16, 32 and 48 px), apple-touch-icon.png (180 px) and og-image.png (1200 × 630, the picture a shared link shows in a chat) are made from it, with rsvg-convert. Change the icon, make them again.
  • The og: and twitter: tags in index.html are what Discord, Telegram and the like show for a link to the site.
  • A Content-Security-Policy, as a <meta> tag in index.html and 404.html, lets the pages load only their own files, and releases.js talk only to the site and api.github.com. A new script, style or picture from elsewhere has to be added to it, or the browser blocks it. The documentation has none: Material has inline scripts.
  • robots.txt points search engines at the documentation's sitemap.xml.

The home page's layout

On a computer the home page fits one screen, without a sidebar: main is up to 1200 px wide, at least as tall as the window, and centres its content top to bottom.

  • The top: the icon and title, its line, and a few things the app does as one line of plain text (.features, taken from What it can do), with the Documentation and Source code links level with the title (.top, .top-links). The links go under the title only when there's no room for both.
  • Download: the Stable and Nightly cards side by side, as wide as the page, each with its notes and checksum under it (.build), then a line on installing, with links to how and to What you're installing. The notes-and-checksum line keeps its height while it's empty, so nothing moves when the list comes in.
  • Older versions: a grid of tiles (.versions), four to a row from 960 px wide, so the 4 shown make one full row; narrower, as many as fit. "All releases on GitHub" is at the end of the heading's line (.section-head).

All of it fits 1280 × 720, even with the warning about the server's checks. Under 640 px wide (a phone) the page starts at the top, everything stacks, and it scrolls. Showing more than 4 older versions (OLDER_SHOWN in releases.js) adds rows; 8 still fit 1280 × 720.