Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ffc1c1ead | ||
|
|
1bcb868897 | ||
|
|
e833647768 |
@@ -6,4 +6,4 @@ paths:
|
|||||||
# Website
|
# Website
|
||||||
|
|
||||||
## website/ is the live site, uploaded by hand
|
## website/ is the live site, uploaded by hand
|
||||||
website/ is a faithful copy of sealshare.nonameweb.ch (METANET hosting), hand-written HTML/CSS with no build step, uploaded wholesale when it changes. Colours in css/theme.css are copied from the indigo profile (the JSON's top-level light/dark) in resources/css/material-scheme.json — copy them again if indigo is regenerated differently; the site does not follow the admin's colour profile. The comparison tables are dated and every competitor value has a source from the product's own site, docs or repo; an unsourced value is "—", never a guess. Never call SealShare's encryption end-to-end (it encrypts at rest on the server). Nothing may load from another host except plausible.io. tests/Feature/WebsiteTest.php guards all of this.
|
website/ is a faithful copy of sealshare.nonameweb.ch (METANET hosting), hand-written HTML/CSS with no build step, uploaded wholesale when it changes. Colours in css/theme.css are copied from the indigo profile in resources/css/material-scheme.json: the standard light and dark values (profiles.indigo.light/dark) and the high-contrast light and dark values (profiles.indigo.contrast.high) — copy them again if indigo is regenerated differently; the site does not follow the admin's colour profile. Light or dark is <html data-theme>, written before the first paint by each page's inline head script from the nav's Light/Dark/System toggle (localStorage sealshare-website-theme); the high-contrast values apply under prefers-contrast: more. The comparison tables are dated and every competitor value has a source from the product's own site, docs or repo; an unsourced value is "—", never a guess. Never call SealShare's encryption end-to-end (files are encrypted in the browser with a key the server issues, and the server decrypts them for downloads). Nothing may load from another host except plausible.io. tests/Feature/WebsiteTest.php guards all of this.
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ A simple, self-hosted file sharing solution built with Laravel. Upload files, ge
|
|||||||
|-------|-----------|
|
|-------|-----------|
|
||||||
| **Framework** | Laravel 13 |
|
| **Framework** | Laravel 13 |
|
||||||
| **Application Server** | FrankenPHP (via Laravel Octane) |
|
| **Application Server** | FrankenPHP (via Laravel Octane) |
|
||||||
| **Frontend** | Livewire 4, Tailwind CSS 4, [Livewire Material](https://gitea.nonameweb.ch/noNameWEB/livewire-material) (Material 3 Expressive) |
|
| **Frontend** | Livewire 4, [Livewire Material](https://gitea.nonameweb.ch/noNameWEB/livewire-material) (Material 3 Expressive) |
|
||||||
| **Authentication** | Laravel Fortify |
|
| **Authentication** | Laravel Fortify |
|
||||||
| **Encryption** | Chunked AES-256-GCM (WebCrypto in the browser), keys wrapped with Argon2id |
|
| **Encryption** | Chunked AES-256-GCM (WebCrypto in the browser), keys wrapped with Argon2id |
|
||||||
| **ZIP Downloads** | [ZipStream-PHP](https://packagist.org/packages/maennchen/zipstream-php) |
|
| **ZIP Downloads** | [ZipStream-PHP](https://packagist.org/packages/maennchen/zipstream-php) |
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ final class DemoData
|
|||||||
|
|
||||||
public const PROTECTED_PASSWORD = 'autumn-harbour-42';
|
public const PROTECTED_PASSWORD = 'autumn-harbour-42';
|
||||||
|
|
||||||
|
/** The share the desktop upload creates, so its link and QR code read the same on every run. */
|
||||||
|
public const CREATED_TOKEN = 'Tf8gH2jK4mN6pQ3r';
|
||||||
|
|
||||||
public static function admin(): User
|
public static function admin(): User
|
||||||
{
|
{
|
||||||
return User::factory()->admin()->create([
|
return User::factory()->admin()->create([
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use App\Services\QrCodeService;
|
|||||||
use App\Services\ShareService;
|
use App\Services\ShareService;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Tests\Screenshots\DemoData;
|
use Tests\Screenshots\DemoData;
|
||||||
use Tests\Screenshots\Publisher;
|
use Tests\Screenshots\Publisher;
|
||||||
|
|
||||||
@@ -24,6 +25,10 @@ beforeEach(function () {
|
|||||||
DemoData::shares();
|
DemoData::shares();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
Str::createRandomStringsNormally();
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A visited page on a device in a theme, once it can be used. Tests call `visit()` themselves, on a
|
* A visited page on a device in a theme, once it can be used. Tests call `visit()` themselves, on a
|
||||||
* line of its own: Pest starts its browser only for tests under tests/Browser or whose body calls
|
* line of its own: Pest starts its browser only for tests under tests/Browser or whose body calls
|
||||||
@@ -45,10 +50,10 @@ function shotPage(mixed $visit, string $device, string $theme): mixed
|
|||||||
* every run: the page is shown as it reads on an installation at https://files.example.com, the
|
* every run: the page is shown as it reads on an installation at https://files.example.com, the
|
||||||
* server's origin replaced in text and fields, and the QR code drawn for that address.
|
* server's origin replaced in text and fields, and the QR code drawn for that address.
|
||||||
*/
|
*/
|
||||||
function shoot(mixed $page, string $device, string $theme, string $name): void
|
function shoot(mixed $page, string $device, string $theme, string $name, string $token = DemoData::DELIVERY_TOKEN): void
|
||||||
{
|
{
|
||||||
$origin = 'https://files.example.com';
|
$origin = 'https://files.example.com';
|
||||||
$qrCode = app(QrCodeService::class)->svg($origin.'/s/'.DemoData::DELIVERY_TOKEN);
|
$qrCode = app(QrCodeService::class)->svg($origin.'/s/'.$token);
|
||||||
|
|
||||||
$page->script("document.head.insertAdjacentHTML('beforeend', '<style>*{caret-color:transparent!important}</style>')");
|
$page->script("document.head.insertAdjacentHTML('beforeend', '<style>*{caret-color:transparent!important}</style>')");
|
||||||
$page->script('(() => { const from = location.origin, to = '.json_encode($origin).'; document.querySelectorAll("input").forEach((input) => { input.value = input.value.replaceAll(from, to) }); const text = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); while (text.nextNode()) { text.currentNode.nodeValue = text.currentNode.nodeValue.replaceAll(from, to) } document.querySelectorAll("[data-qr-code]").forEach((panel) => { panel.innerHTML = '.json_encode($qrCode).' }) })()');
|
$page->script('(() => { const from = location.origin, to = '.json_encode($origin).'; document.querySelectorAll("input").forEach((input) => { input.value = input.value.replaceAll(from, to) }); const text = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); while (text.nextNode()) { text.currentNode.nodeValue = text.currentNode.nodeValue.replaceAll(from, to) } document.querySelectorAll("[data-qr-code]").forEach((panel) => { panel.innerHTML = '.json_encode($qrCode).' }) })()');
|
||||||
@@ -101,23 +106,37 @@ function selectFiles(mixed $page, array $files): void
|
|||||||
$files = ['Q3 Report.pdf' => 2400, 'Contract 2026.pdf' => 380, 'Product photos/hero-shot.jpg' => 4800, 'Product photos/detail.jpg' => 3900];
|
$files = ['Q3 Report.pdf' => 2400, 'Contract 2026.pdf' => 380, 'Product photos/hero-shot.jpg' => 4800, 'Product photos/detail.jpg' => 3900];
|
||||||
|
|
||||||
test('desktop', function (string $theme) use ($files) {
|
test('desktop', function (string $theme) use ($files) {
|
||||||
|
// The share this upload creates takes a fixed token (share tokens are the only 16-character random
|
||||||
|
// strings); sessions and everything else stay random.
|
||||||
|
Str::createRandomStringsUsing(fn (int $length): string => $length === 16
|
||||||
|
? DemoData::CREATED_TOKEN
|
||||||
|
: substr(bin2hex(random_bytes($length)), 0, $length));
|
||||||
|
|
||||||
$upload = visit('/upload');
|
$upload = visit('/upload');
|
||||||
$page = shotPage($upload, 'desktop', $theme);
|
$page = shotPage($upload, 'desktop', $theme);
|
||||||
selectFiles($page, $files);
|
selectFiles($page, $files);
|
||||||
$page->click('label:has-text("Password protect")')
|
$page->click('label:has-text("Password protect")')
|
||||||
->type('input[wire\:model="password"]', DemoData::PROTECTED_PASSWORD)
|
->click('[data-test="generate-password"]')
|
||||||
->type('input[wire\:model="maxDownloads"]', '5');
|
->wait(1)
|
||||||
|
->assertScript("document.querySelector('input[autocomplete=\"new-password\"]').value.length > 0");
|
||||||
|
$page->type('input[wire\:model="maxDownloads"]', '5');
|
||||||
// The drop zone, the files and the options fill the window; typing left the page wherever it scrolled.
|
// The drop zone, the files and the options fill the window; typing left the page wherever it scrolled.
|
||||||
$page->script("document.activeElement?.blur(); window.scrollTo(0, document.querySelector('[data-test=drop-zone]').getBoundingClientRect().top + window.scrollY - 24)");
|
$page->script("document.activeElement?.blur(); window.scrollTo(0, document.querySelector('[data-test=drop-zone]').getBoundingClientRect().top + window.scrollY - 24)");
|
||||||
shoot($page, 'desktop', $theme, '01-upload');
|
shoot($page, 'desktop', $theme, '01-upload');
|
||||||
|
|
||||||
$created = visit(route('share.created', DemoData::DELIVERY_TOKEN, false));
|
// Creating the share is what offers the password once more beside the link.
|
||||||
$page = shotPage($created, 'desktop', $theme);
|
$page->click('[data-test="create-share"]')
|
||||||
shoot($page, 'desktop', $theme, '02-share-created');
|
->waitForText('Share Created!');
|
||||||
|
$page->waitForEvent('networkidle')
|
||||||
|
->assertScript("document.querySelector('[data-test=\"share-password\"]') !== null");
|
||||||
|
shoot($page, 'desktop', $theme, '02-share-created', DemoData::CREATED_TOKEN);
|
||||||
|
|
||||||
$page->click('[data-test="show-qr-code"]')
|
$page->click('[data-test="show-qr-code"]')
|
||||||
->assertScript("document.querySelector('[data-test=\"qr-code-dialog\"]').open");
|
->assertScript("document.querySelector('[data-test=\"qr-code-dialog\"]').open");
|
||||||
shoot($page, 'desktop', $theme, '03-qr-code');
|
shoot($page, 'desktop', $theme, '03-qr-code', DemoData::CREATED_TOKEN);
|
||||||
|
|
||||||
|
// The dashboard shows the eight demo shares, as before.
|
||||||
|
app(ShareService::class)->deleteShare(Share::query()->where('token', DemoData::CREATED_TOKEN)->firstOrFail());
|
||||||
|
|
||||||
$download = visit(route('share.download', DemoData::DELIVERY_TOKEN, false));
|
$download = visit(route('share.download', DemoData::DELIVERY_TOKEN, false));
|
||||||
shoot(shotPage($download, 'desktop', $theme), 'desktop', $theme, '04-download');
|
shoot(shotPage($download, 'desktop', $theme), 'desktop', $theme, '04-download');
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
* Pro viewport). The screen keeps the capture's aspect ratio, so the frame holds its shape while an
|
* Pro viewport). The screen keeps the capture's aspect ratio, so the frame holds its shape while an
|
||||||
* image loads. The screenshots come from `composer screenshots` (tests/Screenshots/).
|
* image loads. The screenshots come from `composer screenshots` (tests/Screenshots/).
|
||||||
*
|
*
|
||||||
|
* The bezels and the deck are hardware, drawn in fixed tints of their own; what shows through the
|
||||||
|
* screen before an image paints and the shadow under a device are colour roles from theme.css, which
|
||||||
|
* must be loaded first. Dark follows <html data-theme>, and the system setting without JavaScript.
|
||||||
|
*
|
||||||
* <div class="device device--laptop">
|
* <div class="device device--laptop">
|
||||||
* <div class="device__screen"><picture>…<img …></picture></div>
|
* <div class="device__screen"><picture>…<img …></picture></div>
|
||||||
* </div>
|
* </div>
|
||||||
@@ -14,13 +18,21 @@
|
|||||||
--bezel-edge: #3a3355;
|
--bezel-edge: #3a3355;
|
||||||
--deck: #c9c2dd;
|
--deck: #c9c2dd;
|
||||||
--deck-shadow: #8f88a8;
|
--deck-shadow: #8f88a8;
|
||||||
|
--device-shadow: color-mix(in srgb, var(--md-sys-color-shadow) 40%, transparent);
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] .device {
|
||||||
|
--bezel: #05020f;
|
||||||
|
--bezel-edge: #2a2244;
|
||||||
|
--deck: #4b426a;
|
||||||
|
--deck-shadow: #231649;
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.device {
|
:root:not([data-theme]) .device {
|
||||||
--bezel: #05020f;
|
--bezel: #05020f;
|
||||||
--bezel-edge: #2a2244;
|
--bezel-edge: #2a2244;
|
||||||
--deck: #4b426a;
|
--deck: #4b426a;
|
||||||
@@ -31,7 +43,7 @@
|
|||||||
.device__screen {
|
.device__screen {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--surface-container-high, #e8deff);
|
background-color: var(--md-sys-color-surface-container-high);
|
||||||
}
|
}
|
||||||
|
|
||||||
.device__screen img {
|
.device__screen img {
|
||||||
@@ -54,7 +66,7 @@
|
|||||||
padding: 2.4% 2.4% 3%;
|
padding: 2.4% 2.4% 3%;
|
||||||
border-radius: 3.2% 3.2% 0 0 / 4.8% 4.8% 0 0;
|
border-radius: 3.2% 3.2% 0 0 / 4.8% 4.8% 0 0;
|
||||||
background: var(--bezel);
|
background: var(--bezel);
|
||||||
box-shadow: inset 0 0 0 1px var(--bezel-edge), 0 24px 48px -24px rgb(16 6 45 / 0.45);
|
box-shadow: inset 0 0 0 1px var(--bezel-edge), 0 24px 48px -24px var(--device-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.device--laptop .device__screen {
|
.device--laptop .device__screen {
|
||||||
@@ -79,8 +91,7 @@
|
|||||||
.device--laptop::after {
|
.device--laptop::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -4%;
|
inset-inline: -4%;
|
||||||
right: -4%;
|
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
height: 3.2%;
|
height: 3.2%;
|
||||||
border-radius: 0 0 40% 40% / 0 0 100% 100%;
|
border-radius: 0 0 40% 40% / 0 0 100% 100%;
|
||||||
@@ -95,7 +106,7 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: 14%;
|
width: 14%;
|
||||||
height: 1.4%;
|
height: 1.4%;
|
||||||
border-radius: 0 0 12px 12px;
|
border-radius: 0 0 var(--md-sys-shape-corner-md) var(--md-sys-shape-corner-md);
|
||||||
background: var(--deck-shadow);
|
background: var(--deck-shadow);
|
||||||
translate: -50% 0;
|
translate: -50% 0;
|
||||||
}
|
}
|
||||||
@@ -106,7 +117,7 @@
|
|||||||
padding: 3.4%;
|
padding: 3.4%;
|
||||||
border-radius: 15% / 7%;
|
border-radius: 15% / 7%;
|
||||||
background: var(--bezel);
|
background: var(--bezel);
|
||||||
box-shadow: inset 0 0 0 2px var(--bezel-edge), 0 24px 48px -20px rgb(16 6 45 / 0.5);
|
box-shadow: inset 0 0 0 2px var(--bezel-edge), 0 24px 48px -20px var(--device-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.device--phone .device__screen {
|
.device--phone .device__screen {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@@ -7,6 +7,83 @@
|
|||||||
<meta name="description" content="SealShare is self-hosted file sharing: your company's own upload platform to exchange files with customers, encrypted at rest, on your own server and domain. Open source, one Docker image.">
|
<meta name="description" content="SealShare is self-hosted file sharing: your company's own upload platform to exchange files with customers, encrypted at rest, on your own server and domain. Open source, one Docker image.">
|
||||||
<meta name="theme-color" content="#faf4ff" media="(prefers-color-scheme: light)">
|
<meta name="theme-color" content="#faf4ff" media="(prefers-color-scheme: light)">
|
||||||
<meta name="theme-color" content="#10062d" media="(prefers-color-scheme: dark)">
|
<meta name="theme-color" content="#10062d" media="(prefers-color-scheme: dark)">
|
||||||
|
<script>
|
||||||
|
// Light, Dark or System, decided before the first paint: the visitor's choice in the nav, kept in
|
||||||
|
// localStorage, else the system's. css/theme.css keys on <html data-theme>.
|
||||||
|
(function () {
|
||||||
|
var root = document.documentElement;
|
||||||
|
var key = 'sealshare-website-theme';
|
||||||
|
var choices = ['light', 'dark', 'system'];
|
||||||
|
var surfaces = { light: '#faf4ff', dark: '#10062d' };
|
||||||
|
var system = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
var choice = 'system';
|
||||||
|
|
||||||
|
try {
|
||||||
|
var stored = localStorage.getItem(key);
|
||||||
|
|
||||||
|
if (choices.indexOf(stored) !== -1) {
|
||||||
|
choice = stored;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Storage is blocked: follow the system.
|
||||||
|
}
|
||||||
|
|
||||||
|
// A <picture>'s dark source (data-theme-dark) follows the site's theme instead of the system's.
|
||||||
|
var sources = function () {
|
||||||
|
document.querySelectorAll('source[data-theme-dark]').forEach(function (source) {
|
||||||
|
source.media = root.dataset.theme === 'dark' ? 'all' : 'not all';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var apply = function () {
|
||||||
|
var theme = choice === 'system' ? (system.matches ? 'dark' : 'light') : choice;
|
||||||
|
|
||||||
|
root.dataset.themeChoice = choice;
|
||||||
|
root.dataset.theme = theme;
|
||||||
|
|
||||||
|
document.querySelectorAll('meta[name="theme-color"]').forEach(function (meta) {
|
||||||
|
meta.removeAttribute('media');
|
||||||
|
meta.setAttribute('content', surfaces[theme]);
|
||||||
|
});
|
||||||
|
|
||||||
|
sources();
|
||||||
|
document.dispatchEvent(new CustomEvent('sealshare:theme', { detail: { choice: choice, theme: theme } }));
|
||||||
|
};
|
||||||
|
|
||||||
|
window.sealshareTheme = {
|
||||||
|
sources: sources,
|
||||||
|
get choice() {
|
||||||
|
return choice;
|
||||||
|
},
|
||||||
|
get theme() {
|
||||||
|
return root.dataset.theme;
|
||||||
|
},
|
||||||
|
set: function (next) {
|
||||||
|
if (choices.indexOf(next) === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
choice = next;
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key, next);
|
||||||
|
} catch (error) {
|
||||||
|
// Storage is blocked: the page still switches, it just will not remember.
|
||||||
|
}
|
||||||
|
|
||||||
|
apply();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
system.addEventListener('change', function () {
|
||||||
|
if (choice === 'system') {
|
||||||
|
apply();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
apply();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:url" content="https://sealshare.nonameweb.ch/">
|
<meta property="og:url" content="https://sealshare.nonameweb.ch/">
|
||||||
<meta property="og:title" content="SealShare — your own secure upload platform">
|
<meta property="og:title" content="SealShare — your own secure upload platform">
|
||||||
@@ -25,7 +102,7 @@
|
|||||||
<link rel="stylesheet" href="css/device-frame.css">
|
<link rel="stylesheet" href="css/device-frame.css">
|
||||||
</head>
|
</head>
|
||||||
<body id="top">
|
<body id="top">
|
||||||
<a class="skip-link" href="#main">Skip to content</a>
|
<a class="skip-link btn btn--filled" href="#main">Skip to content</a>
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="sprite" aria-hidden="true">
|
<svg xmlns="http://www.w3.org/2000/svg" class="sprite" aria-hidden="true">
|
||||||
<symbol id="i-cloud-upload" viewBox="0 -960 960 960"><path d="m440-446-36 35q-11 11-27.5 11T348-412q-11-11-11-28t11-28l104-104q12-12 28-12t28 12l104 104q11 11 11.5 27.5T612-412q-11 11-27.5 11.5T556-411l-36-35v206h220q42 0 71-29t29-71q0-42-29-71t-71-29h-60v-80q0-83-58.5-141.5T480-720q-83 0-141.5 58.5T280-520h-20q-58 0-99 41t-41 99q0 58 41 99t99 41h60q17 0 28.5 11.5T360-200q0 17-11.5 28.5T320-160h-60q-91 0-155.5-63T40-377q0-78 47-139t123-78q25-92 100-149t170-57q117 0 198.5 81.5T760-520q69 8 114.5 59.5T920-340q0 75-52.5 127.5T740-160H520q-33 0-56.5-23.5T440-240v-206Zm40 6Z"/></symbol>
|
<symbol id="i-cloud-upload" viewBox="0 -960 960 960"><path d="m440-446-36 35q-11 11-27.5 11T348-412q-11-11-11-28t11-28l104-104q12-12 28-12t28 12l104 104q11 11 11.5 27.5T612-412q-11 11-27.5 11.5T556-411l-36-35v206h220q42 0 71-29t29-71q0-42-29-71t-71-29h-60v-80q0-83-58.5-141.5T480-720q-83 0-141.5 58.5T280-520h-20q-58 0-99 41t-41 99q0 58 41 99t99 41h60q17 0 28.5 11.5T360-200q0 17-11.5 28.5T320-160h-60q-91 0-155.5-63T40-377q0-78 47-139t123-78q25-92 100-149t170-57q117 0 198.5 81.5T760-520q69 8 114.5 59.5T920-340q0 75-52.5 127.5T740-160H520q-33 0-56.5-23.5T440-240v-206Zm40 6Z"/></symbol>
|
||||||
@@ -64,12 +141,15 @@
|
|||||||
<symbol id="s-sunny" viewBox="0 0 100 100"><path d="M98 50C98 51.7 97.4 53.5 96.2 54.9C93.7 57.8 91.3 60.7 88.8 63.6C87.8 64.8 87.2 66.3 87 67.9C86.7 71.6 86.4 75.4 86.1 79.2C85.8 82.9 82.9 85.8 79.2 86.1C75.4 86.4 71.6 86.7 67.9 87C66.3 87.2 64.8 87.8 63.6 88.8C60.7 91.3 57.8 93.7 54.9 96.2C52.1 98.6 47.9 98.6 45.1 96.2C42.2 93.7 39.3 91.3 36.4 88.8C35.2 87.8 33.7 87.2 32.1 87C28.4 86.7 24.6 86.4 20.8 86.1C17.1 85.8 14.2 82.9 13.9 79.2C13.6 75.4 13.3 71.6 13 67.9C12.8 66.3 12.2 64.8 11.2 63.6C8.7 60.7 6.3 57.8 3.8 54.9C1.4 52.1 1.4 47.9 3.8 45.1C6.3 42.2 8.7 39.3 11.2 36.4C12.2 35.2 12.8 33.7 13 32.1C13.3 28.4 13.6 24.6 13.9 20.8C14.2 17.1 17.1 14.2 20.8 13.9C24.6 13.6 28.4 13.3 32.1 13C33.7 12.8 35.2 12.2 36.4 11.2C39.3 8.7 42.2 6.3 45.1 3.8C47.9 1.4 52.1 1.4 54.9 3.8C57.8 6.3 60.7 8.7 63.6 11.2C64.8 12.2 66.3 12.8 67.9 13C71.6 13.3 75.4 13.6 79.2 13.9C82.9 14.2 85.8 17.1 86.1 20.8C86.4 24.6 86.7 28.4 87 32.1C87.2 33.7 87.8 35.2 88.8 36.4C91.3 39.3 93.7 42.2 96.2 45.1C97.4 46.5 98 48.3 98 50Z"/></symbol>
|
<symbol id="s-sunny" viewBox="0 0 100 100"><path d="M98 50C98 51.7 97.4 53.5 96.2 54.9C93.7 57.8 91.3 60.7 88.8 63.6C87.8 64.8 87.2 66.3 87 67.9C86.7 71.6 86.4 75.4 86.1 79.2C85.8 82.9 82.9 85.8 79.2 86.1C75.4 86.4 71.6 86.7 67.9 87C66.3 87.2 64.8 87.8 63.6 88.8C60.7 91.3 57.8 93.7 54.9 96.2C52.1 98.6 47.9 98.6 45.1 96.2C42.2 93.7 39.3 91.3 36.4 88.8C35.2 87.8 33.7 87.2 32.1 87C28.4 86.7 24.6 86.4 20.8 86.1C17.1 85.8 14.2 82.9 13.9 79.2C13.6 75.4 13.3 71.6 13 67.9C12.8 66.3 12.2 64.8 11.2 63.6C8.7 60.7 6.3 57.8 3.8 54.9C1.4 52.1 1.4 47.9 3.8 45.1C6.3 42.2 8.7 39.3 11.2 36.4C12.2 35.2 12.8 33.7 13 32.1C13.3 28.4 13.6 24.6 13.9 20.8C14.2 17.1 17.1 14.2 20.8 13.9C24.6 13.6 28.4 13.3 32.1 13C33.7 12.8 35.2 12.2 36.4 11.2C39.3 8.7 42.2 6.3 45.1 3.8C47.9 1.4 52.1 1.4 54.9 3.8C57.8 6.3 60.7 8.7 63.6 11.2C64.8 12.2 66.3 12.8 67.9 13C71.6 13.3 75.4 13.6 79.2 13.9C82.9 14.2 85.8 17.1 86.1 20.8C86.4 24.6 86.7 28.4 87 32.1C87.2 33.7 87.8 35.2 88.8 36.4C91.3 39.3 93.7 42.2 96.2 45.1C97.4 46.5 98 48.3 98 50Z"/></symbol>
|
||||||
<symbol id="s-cookie-6" viewBox="0 0 100 100"><path d="M71.8 87.5C70.1 88.5 68.5 89.7 67.1 91C67 91.1 67 91.1 66.9 91.2C62.2 95.7 56.1 98 50 98C43.9 98 37.8 95.7 33.1 91.2C30.3 88.4 26.8 86.4 23 85.3C22.9 85.3 22.9 85.3 22.8 85.3C10.2 81.6 2.7 68.7 5.9 55.9C6.9 52.1 6.9 48.1 5.9 44.3C5.9 44.2 5.9 44.1 5.9 44.1C2.7 31.3 10.2 18.4 22.8 14.8C26.6 13.7 30.1 11.7 32.9 9C33 8.9 33 8.9 33.1 8.8C37.8 4.3 43.9 2 50 2C56.1 2 62.2 4.3 66.9 8.8C69.7 11.6 73.2 13.6 77 14.7C77.1 14.7 77.1 14.7 77.2 14.7C89.8 18.4 97.3 31.3 94.1 44.1C93.1 47.9 93.1 51.9 94.1 55.7C94.1 55.8 94.1 55.9 94.1 55.9C97.3 68.7 89.8 81.6 77.2 85.2C75.3 85.8 73.5 86.5 71.8 87.5Z"/></symbol>
|
<symbol id="s-cookie-6" viewBox="0 0 100 100"><path d="M71.8 87.5C70.1 88.5 68.5 89.7 67.1 91C67 91.1 67 91.1 66.9 91.2C62.2 95.7 56.1 98 50 98C43.9 98 37.8 95.7 33.1 91.2C30.3 88.4 26.8 86.4 23 85.3C22.9 85.3 22.9 85.3 22.8 85.3C10.2 81.6 2.7 68.7 5.9 55.9C6.9 52.1 6.9 48.1 5.9 44.3C5.9 44.2 5.9 44.1 5.9 44.1C2.7 31.3 10.2 18.4 22.8 14.8C26.6 13.7 30.1 11.7 32.9 9C33 8.9 33 8.9 33.1 8.8C37.8 4.3 43.9 2 50 2C56.1 2 62.2 4.3 66.9 8.8C69.7 11.6 73.2 13.6 77 14.7C77.1 14.7 77.1 14.7 77.2 14.7C89.8 18.4 97.3 31.3 94.1 44.1C93.1 47.9 93.1 51.9 94.1 55.7C94.1 55.8 94.1 55.9 94.1 55.9C97.3 68.7 89.8 81.6 77.2 85.2C75.3 85.8 73.5 86.5 71.8 87.5Z"/></symbol>
|
||||||
<symbol id="s-gem" viewBox="0 0 100 100"><path d="M49.9 98C49.2 98 48.4 98 47.6 97.9C43.8 97.5 40.2 95.8 32.9 92.5C26.9 89.8 21 87 15.1 84.3C7 80.6 2.4 72.1 3.7 63.3C5 54.2 6.3 45.1 7.7 36C8.5 30.1 11.7 24.8 16.6 21.4C23.8 16.2 31 11 38.3 5.8C41.7 3.3 45.9 2 50.1 2C54.4 2 58.5 3.4 61.9 5.8C69.2 11.1 76.4 16.3 83.6 21.5C88.4 25 91.6 30.3 92.5 36.2C93.8 45.3 95.1 54.4 96.3 63.5C97.6 72.2 92.9 80.8 84.8 84.5C78.9 87.2 73 89.8 67 92.5C59.7 95.9 56.1 97.5 52.3 97.9C51.5 98 50.7 98 49.9 98Z"/></symbol>
|
<symbol id="s-gem" viewBox="0 0 100 100"><path d="M49.9 98C49.2 98 48.4 98 47.6 97.9C43.8 97.5 40.2 95.8 32.9 92.5C26.9 89.8 21 87 15.1 84.3C7 80.6 2.4 72.1 3.7 63.3C5 54.2 6.3 45.1 7.7 36C8.5 30.1 11.7 24.8 16.6 21.4C23.8 16.2 31 11 38.3 5.8C41.7 3.3 45.9 2 50.1 2C54.4 2 58.5 3.4 61.9 5.8C69.2 11.1 76.4 16.3 83.6 21.5C88.4 25 91.6 30.3 92.5 36.2C93.8 45.3 95.1 54.4 96.3 63.5C97.6 72.2 92.9 80.8 84.8 84.5C78.9 87.2 73 89.8 67 92.5C59.7 95.9 56.1 97.5 52.3 97.9C51.5 98 50.7 98 49.9 98Z"/></symbol>
|
||||||
|
<symbol id="i-light-mode" viewBox="0 -960 960 960"><path d="M480-360q50 0 85-35t35-85q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 50 35 85t85 35Zm0 80q-83 0-141.5-58.5T280-480q0-83 58.5-141.5T480-680q83 0 141.5 58.5T680-480q0 83-58.5 141.5T480-280ZM80-440q-17 0-28.5-11.5T40-480q0-17 11.5-28.5T80-520h80q17 0 28.5 11.5T200-480q0 17-11.5 28.5T160-440H80Zm720 0q-17 0-28.5-11.5T760-480q0-17 11.5-28.5T800-520h80q17 0 28.5 11.5T920-480q0 17-11.5 28.5T880-440h-80ZM480-760q-17 0-28.5-11.5T440-800v-80q0-17 11.5-28.5T480-920q17 0 28.5 11.5T520-880v80q0 17-11.5 28.5T480-760Zm0 720q-17 0-28.5-11.5T440-80v-80q0-17 11.5-28.5T480-200q17 0 28.5 11.5T520-160v80q0 17-11.5 28.5T480-40ZM226-678l-43-42q-12-11-11.5-28t11.5-29q12-12 29-12t28 12l42 43q11 12 11 28t-11 28q-11 12-27.5 11.5T226-678Zm494 495-42-43q-11-12-11-28.5t11-27.5q11-12 27.5-11.5T734-282l43 42q12 11 11.5 28T777-183q-12 12-29 12t-28-12Zm-42-495q-12-11-11.5-27.5T678-734l42-43q11-12 28-11.5t29 11.5q12 12 12 29t-12 28l-43 42q-12 11-28 11t-28-11ZM183-183q-12-12-12-29t12-28l43-42q12-11 28.5-11t27.5 11q12 11 11.5 27.5T282-226l-42 43q-11 12-28 11.5T183-183Zm297-297Z"/></symbol>
|
||||||
|
<symbol id="i-dark-mode" viewBox="0 -960 960 960"><path d="M480-120q-151 0-255.5-104.5T120-480q0-138 90-239.5T440-838q13-2 23 3.5t16 14.5q6 9 6.5 21t-7.5 23q-17 26-25.5 55t-8.5 61q0 90 63 153t153 63q31 0 61.5-9t54.5-25q11-7 22.5-6.5T819-479q10 5 15.5 15t3.5 24q-14 138-117.5 229T480-120Zm0-80q88 0 158-48.5T740-375q-20 5-40 8t-40 3q-123 0-209.5-86.5T364-660q0-20 3-40t8-40q-78 32-126.5 102T200-480q0 116 82 198t198 82Zm-10-270Z"/></symbol>
|
||||||
|
<symbol id="i-brightness-auto" viewBox="0 -960 960 960"><path d="M408-412h146l25 73q3 8 10.5 13.5T606-320q15 0 23.5-12.5T633-359L519-661q-3-9-11-14t-17-5h-22q-9 0-17 5t-11 14L327-360q-5 14 3.5 27t24.5 13q10 0 17.5-5.5T383-340l25-72Zm18-52 52-150h4l52 150H426Zm-80 304H240q-33 0-56.5-23.5T160-240v-106l-77-78q-11-12-17-26.5T60-480q0-15 6-29.5T83-536l77-78v-106q0-33 23.5-56.5T240-800h106l78-77q12-11 26.5-17t29.5-6q15 0 29.5 6t26.5 17l78 77h106q33 0 56.5 23.5T800-720v106l77 78q11 12 17 26.5t6 29.5q0 15-6 29.5T877-424l-77 78v106q0 33-23.5 56.5T720-160H614l-78 77q-12 11-26.5 17T480-60q-15 0-29.5-6T424-83l-78-77Zm34-80 100 100 100-100h140v-140l100-100-100-100v-140H580L480-820 380-720H240v140L140-480l100 100v140h140Zm100-240Z"/></symbol>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<header class="nav">
|
<header class="nav">
|
||||||
<div class="container nav__bar">
|
<div class="container nav__bar">
|
||||||
<a class="brand" href="#top"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2Z" stroke="currentColor" stroke-width="1.5"/><path d="M14 2v5a1 1 0 0 0 1 1h5" stroke="currentColor" stroke-width="1.5"/><path d="M12 17v-6m0 0-2.5 2.5M12 11l2.5 2.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg><span>SealShare</span></a>
|
<a class="brand" href="#top"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2Z" stroke="currentColor" stroke-width="1.5"/><path d="M14 2v5a1 1 0 0 0 1 1h5" stroke="currentColor" stroke-width="1.5"/><path d="M12 17v-6m0 0-2.5 2.5M12 11l2.5 2.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg><span>SealShare</span></a>
|
||||||
<button class="nav__toggle" id="navToggle" type="button" aria-expanded="false" aria-controls="navLinks" aria-label="Menu">
|
<button class="icon-btn nav__toggle" id="navToggle" type="button" aria-expanded="false" aria-controls="navLinks" aria-label="Menu">
|
||||||
<svg class="icon icon-menu" aria-hidden="true"><use href="#i-menu"/></svg>
|
<svg class="icon icon-menu" aria-hidden="true"><use href="#i-menu"/></svg>
|
||||||
<svg class="icon icon-close" aria-hidden="true"><use href="#i-close"/></svg>
|
<svg class="icon icon-close" aria-hidden="true"><use href="#i-close"/></svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -84,6 +164,11 @@
|
|||||||
<li><a class="btn btn--filled" href="https://gitea.nonameweb.ch/noNameWEB/SealShare"><svg class="icon" aria-hidden="true"><use href="#i-open-in-new"/></svg>Gitea</a></li>
|
<li><a class="btn btn--filled" href="https://gitea.nonameweb.ch/noNameWEB/SealShare"><svg class="icon" aria-hidden="true"><use href="#i-open-in-new"/></svg>Gitea</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
<button class="icon-btn theme-toggle" id="themeToggle" type="button" aria-label="Theme: System">
|
||||||
|
<svg class="icon icon-light-mode" aria-hidden="true"><use href="#i-light-mode"/></svg>
|
||||||
|
<svg class="icon icon-dark-mode" aria-hidden="true"><use href="#i-dark-mode"/></svg>
|
||||||
|
<svg class="icon icon-brightness-auto" aria-hidden="true"><use href="#i-brightness-auto"/></svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -112,10 +197,11 @@
|
|||||||
<div class="device__lid">
|
<div class="device__lid">
|
||||||
<div class="device__screen">
|
<div class="device__screen">
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="img/screenshots/desktop/dark/01-upload-1600.webp 1600w, img/screenshots/desktop/dark/01-upload-800.webp 800w" sizes="(max-width: 960px) 94vw, 58vw">
|
<source data-theme-dark media="(prefers-color-scheme: dark)" srcset="img/screenshots/desktop/dark/01-upload-1600.webp 1600w, img/screenshots/desktop/dark/01-upload-800.webp 800w" sizes="(max-width: 839px) 94vw, 58vw">
|
||||||
<img src="img/screenshots/desktop/light/01-upload-800.webp" srcset="img/screenshots/desktop/light/01-upload-1600.webp 1600w, img/screenshots/desktop/light/01-upload-800.webp 800w" sizes="(max-width: 960px) 94vw, 58vw"
|
<script>sealshareTheme.sources()</script>
|
||||||
|
<img src="img/screenshots/desktop/light/01-upload-800.webp" srcset="img/screenshots/desktop/light/01-upload-1600.webp 1600w, img/screenshots/desktop/light/01-upload-800.webp 800w" sizes="(max-width: 839px) 94vw, 58vw"
|
||||||
width="1512" height="982" loading="eager" decoding="async"
|
width="1512" height="982" loading="eager" decoding="async"
|
||||||
alt="SealShare's upload page with files selected and share options">
|
alt="SealShare's upload page with files selected and a generated password in the share options">
|
||||||
</picture>
|
</picture>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -123,8 +209,9 @@
|
|||||||
<div class="device device--phone">
|
<div class="device device--phone">
|
||||||
<div class="device__screen">
|
<div class="device__screen">
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="img/screenshots/phone/dark/03-download-1080.webp 1080w, img/screenshots/phone/dark/03-download-540.webp 540w" sizes="(max-width: 960px) 24vw, 15vw">
|
<source data-theme-dark media="(prefers-color-scheme: dark)" srcset="img/screenshots/phone/dark/03-download-1080.webp 1080w, img/screenshots/phone/dark/03-download-540.webp 540w" sizes="(max-width: 839px) 24vw, 15vw">
|
||||||
<img src="img/screenshots/phone/light/03-download-540.webp" srcset="img/screenshots/phone/light/03-download-1080.webp 1080w, img/screenshots/phone/light/03-download-540.webp 540w" sizes="(max-width: 960px) 24vw, 15vw"
|
<script>sealshareTheme.sources()</script>
|
||||||
|
<img src="img/screenshots/phone/light/03-download-540.webp" srcset="img/screenshots/phone/light/03-download-1080.webp 1080w, img/screenshots/phone/light/03-download-540.webp 540w" sizes="(max-width: 839px) 24vw, 15vw"
|
||||||
width="393" height="852" loading="eager" decoding="async"
|
width="393" height="852" loading="eager" decoding="async"
|
||||||
alt="The download page on a phone">
|
alt="The download page on a phone">
|
||||||
</picture>
|
</picture>
|
||||||
@@ -176,7 +263,7 @@
|
|||||||
<li class="step">
|
<li class="step">
|
||||||
<span class="badge-icon" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-9"/></svg><svg class="icon" aria-hidden="true"><use href="#i-cloud-upload"/></svg></span>
|
<span class="badge-icon" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-9"/></svg><svg class="icon" aria-hidden="true"><use href="#i-cloud-upload"/></svg></span>
|
||||||
<h3 class="title">Upload</h3>
|
<h3 class="title">Upload</h3>
|
||||||
<p>Drop files or whole folders. Add a password, an expiry and a download limit if you want.</p>
|
<p>Drop files or whole folders. Add a password, an expiry and a download limit if you want — SealShare can generate the password for you.</p>
|
||||||
</li>
|
</li>
|
||||||
<li class="step">
|
<li class="step">
|
||||||
<span class="badge-icon badge-icon--secondary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-soft-burst"/></svg><svg class="icon" aria-hidden="true"><use href="#i-qr-code-2"/></svg></span>
|
<span class="badge-icon badge-icon--secondary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-soft-burst"/></svg><svg class="icon" aria-hidden="true"><use href="#i-qr-code-2"/></svg></span>
|
||||||
@@ -197,13 +284,13 @@
|
|||||||
<div class="section__head">
|
<div class="section__head">
|
||||||
<span class="eyebrow"><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg>Features</span>
|
<span class="eyebrow"><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg>Features</span>
|
||||||
<h2 class="headline" id="features-title">Everything a file exchange with customers needs</h2>
|
<h2 class="headline" id="features-title">Everything a file exchange with customers needs</h2>
|
||||||
<p class="lede">And nothing that turns it into a project: SealShare runs from a single container and is set up in a few minutes. It follows your system's light or dark theme, on desktop and phone.</p>
|
<p class="lede">And nothing that turns it into a project: SealShare runs from a single container and is set up in a few minutes. It works on desktop and phone, in a Light, Dark or System theme, and in high contrast when your system asks for it.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="features">
|
<div class="features">
|
||||||
<article class="feature">
|
<article class="feature">
|
||||||
<span class="badge-icon" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-drive-folder-upload"/></svg></span>
|
<span class="badge-icon" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-drive-folder-upload"/></svg></span>
|
||||||
<h3>Files and folders</h3>
|
<h3>Files and folders</h3>
|
||||||
<p>Drag & drop or browse; folders keep their structure, with live upload progress.</p>
|
<p>Drag & drop or browse; folders keep their structure. Files are encrypted in the browser and sent in chunks, with progress per file and a failed chunk retried on its own.</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="feature">
|
<article class="feature">
|
||||||
<span class="badge-icon badge-icon--secondary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-link"/></svg></span>
|
<span class="badge-icon badge-icon--secondary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-link"/></svg></span>
|
||||||
@@ -218,12 +305,12 @@
|
|||||||
<article class="feature">
|
<article class="feature">
|
||||||
<span class="badge-icon" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-lock"/></svg></span>
|
<span class="badge-icon" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-lock"/></svg></span>
|
||||||
<h3>Password protection</h3>
|
<h3>Password protection</h3>
|
||||||
<p>Protect a share with a password, or generate one to copy; its encryption key is derived from it.</p>
|
<p>Type a password or generate one, as random characters or a passphrase. It is shown once more beside the new link, and it protects the share's own random key.</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="feature">
|
<article class="feature">
|
||||||
<span class="badge-icon badge-icon--secondary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-encrypted"/></svg></span>
|
<span class="badge-icon badge-icon--secondary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-encrypted"/></svg></span>
|
||||||
<h3>Encrypted at rest</h3>
|
<h3>Encrypted at rest</h3>
|
||||||
<p>Encrypted at rest with AES-256-GCM; with a share password the key is never stored.</p>
|
<p>Encrypted in the browser with AES-256-GCM before upload, and stored only encrypted. With a share password the key is never stored as it is.</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="feature">
|
<article class="feature">
|
||||||
<span class="badge-icon badge-icon--tertiary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-schedule"/></svg></span>
|
<span class="badge-icon badge-icon--tertiary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-schedule"/></svg></span>
|
||||||
@@ -243,7 +330,7 @@
|
|||||||
<article class="feature">
|
<article class="feature">
|
||||||
<span class="badge-icon badge-icon--tertiary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-delete-sweep"/></svg></span>
|
<span class="badge-icon badge-icon--tertiary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-delete-sweep"/></svg></span>
|
||||||
<h3>Automatic clean-up</h3>
|
<h3>Automatic clean-up</h3>
|
||||||
<p>Expired shares and their files are deleted every hour.</p>
|
<p>Expired shares and their files are deleted every hour, and unfinished uploads after 4 hours.</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="feature">
|
<article class="feature">
|
||||||
<span class="badge-icon" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-dashboard"/></svg></span>
|
<span class="badge-icon" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-dashboard"/></svg></span>
|
||||||
@@ -252,8 +339,8 @@
|
|||||||
</article>
|
</article>
|
||||||
<article class="feature">
|
<article class="feature">
|
||||||
<span class="badge-icon badge-icon--secondary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-admin-panel-settings"/></svg></span>
|
<span class="badge-icon badge-icon--secondary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-admin-panel-settings"/></svg></span>
|
||||||
<h3>Limits and quota</h3>
|
<h3>Limits and defaults</h3>
|
||||||
<p>Maximum file and share size, files per share, storage quota, default expiry.</p>
|
<p>Maximum file and share size, files per share, storage quota, default expiry, and a password generator that is off, on request or prefilled.</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="feature">
|
<article class="feature">
|
||||||
<span class="badge-icon badge-icon--tertiary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-palette"/></svg></span>
|
<span class="badge-icon badge-icon--tertiary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-palette"/></svg></span>
|
||||||
@@ -273,7 +360,7 @@
|
|||||||
<article class="feature">
|
<article class="feature">
|
||||||
<span class="badge-icon badge-icon--tertiary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-deployed-code"/></svg></span>
|
<span class="badge-icon badge-icon--tertiary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-deployed-code"/></svg></span>
|
||||||
<h3>One Docker image</h3>
|
<h3>One Docker image</h3>
|
||||||
<p>FrankenPHP with Laravel Octane, optional automatic TLS and SQLite — no separate database.</p>
|
<p>FrankenPHP with Laravel Octane and SQLite — no separate database. Uploads need HTTPS: turn on automatic TLS, or use your reverse proxy.</p>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -286,13 +373,13 @@
|
|||||||
<p class="lede">The same interface on a laptop and a phone, in light or dark.</p>
|
<p class="lede">The same interface on a laptop and a phone, in light or dark.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="gallery__controls">
|
<div class="gallery__controls">
|
||||||
<div class="segmented" role="group" aria-label="Device">
|
<div class="button-group" role="group" aria-label="Device">
|
||||||
<button type="button" data-gallery-device="desktop" aria-pressed="true"><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg>Desktop</button>
|
<button class="btn" type="button" data-gallery-device="desktop" aria-pressed="true"><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg>Desktop</button>
|
||||||
<button type="button" data-gallery-device="phone" aria-pressed="false"><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg>Phone</button>
|
<button class="btn" type="button" data-gallery-device="phone" aria-pressed="false"><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg>Phone</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="segmented" role="group" aria-label="Theme">
|
<div class="button-group" role="group" aria-label="Theme">
|
||||||
<button type="button" data-gallery-theme="light" aria-pressed="true"><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg>Light</button>
|
<button class="btn" type="button" data-gallery-theme="light" aria-pressed="true"><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg>Light</button>
|
||||||
<button type="button" data-gallery-theme="dark" aria-pressed="false"><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg>Dark</button>
|
<button class="btn" type="button" data-gallery-theme="dark" aria-pressed="false"><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg>Dark</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="gallery gallery--desktop" data-gallery="desktop">
|
<ul class="gallery gallery--desktop" data-gallery="desktop">
|
||||||
@@ -301,15 +388,15 @@
|
|||||||
<div class="device device--laptop">
|
<div class="device device--laptop">
|
||||||
<div class="device__lid">
|
<div class="device__lid">
|
||||||
<div class="device__screen">
|
<div class="device__screen">
|
||||||
<img src="img/screenshots/desktop/light/01-upload-800.webp" srcset="img/screenshots/desktop/light/01-upload-1600.webp 1600w, img/screenshots/desktop/light/01-upload-800.webp 800w" sizes="(max-width: 840px) 94vw, 568px"
|
<img src="img/screenshots/desktop/light/01-upload-800.webp" srcset="img/screenshots/desktop/light/01-upload-1600.webp 1600w, img/screenshots/desktop/light/01-upload-800.webp 800w" sizes="(max-width: 839px) 94vw, 568px"
|
||||||
data-light-src="img/screenshots/desktop/light/01-upload-800.webp" data-light-srcset="img/screenshots/desktop/light/01-upload-1600.webp 1600w, img/screenshots/desktop/light/01-upload-800.webp 800w"
|
data-light-src="img/screenshots/desktop/light/01-upload-800.webp" data-light-srcset="img/screenshots/desktop/light/01-upload-1600.webp 1600w, img/screenshots/desktop/light/01-upload-800.webp 800w"
|
||||||
data-dark-src="img/screenshots/desktop/dark/01-upload-800.webp" data-dark-srcset="img/screenshots/desktop/dark/01-upload-1600.webp 1600w, img/screenshots/desktop/dark/01-upload-800.webp 800w"
|
data-dark-src="img/screenshots/desktop/dark/01-upload-800.webp" data-dark-srcset="img/screenshots/desktop/dark/01-upload-1600.webp 1600w, img/screenshots/desktop/dark/01-upload-800.webp 800w"
|
||||||
width="1512" height="982" loading="lazy" decoding="async"
|
width="1512" height="982" loading="lazy" decoding="async"
|
||||||
alt="The upload page with two files selected and the share options filled in">
|
alt="The upload page with four files selected and a generated password in the share options">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<figcaption><strong>Upload files and folders</strong><span>Drag & drop, then set a password, an expiry and a download limit.</span></figcaption>
|
<figcaption><strong>Upload files and folders</strong><span>Drag & drop, then generate a password and set an expiry and a download limit.</span></figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@@ -317,15 +404,15 @@
|
|||||||
<div class="device device--laptop">
|
<div class="device device--laptop">
|
||||||
<div class="device__lid">
|
<div class="device__lid">
|
||||||
<div class="device__screen">
|
<div class="device__screen">
|
||||||
<img src="img/screenshots/desktop/light/02-share-created-800.webp" srcset="img/screenshots/desktop/light/02-share-created-1600.webp 1600w, img/screenshots/desktop/light/02-share-created-800.webp 800w" sizes="(max-width: 840px) 94vw, 568px"
|
<img src="img/screenshots/desktop/light/02-share-created-800.webp" srcset="img/screenshots/desktop/light/02-share-created-1600.webp 1600w, img/screenshots/desktop/light/02-share-created-800.webp 800w" sizes="(max-width: 839px) 94vw, 568px"
|
||||||
data-light-src="img/screenshots/desktop/light/02-share-created-800.webp" data-light-srcset="img/screenshots/desktop/light/02-share-created-1600.webp 1600w, img/screenshots/desktop/light/02-share-created-800.webp 800w"
|
data-light-src="img/screenshots/desktop/light/02-share-created-800.webp" data-light-srcset="img/screenshots/desktop/light/02-share-created-1600.webp 1600w, img/screenshots/desktop/light/02-share-created-800.webp 800w"
|
||||||
data-dark-src="img/screenshots/desktop/dark/02-share-created-800.webp" data-dark-srcset="img/screenshots/desktop/dark/02-share-created-1600.webp 1600w, img/screenshots/desktop/dark/02-share-created-800.webp 800w"
|
data-dark-src="img/screenshots/desktop/dark/02-share-created-800.webp" data-dark-srcset="img/screenshots/desktop/dark/02-share-created-1600.webp 1600w, img/screenshots/desktop/dark/02-share-created-800.webp 800w"
|
||||||
width="1512" height="982" loading="lazy" decoding="async"
|
width="1512" height="982" loading="lazy" decoding="async"
|
||||||
alt="The share created page with the share link, its details and the QR code button">
|
alt="The share created page with the link, the password beside it, the share's details and the QR code button">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<figcaption><strong>Share the link</strong><span>Copy it, show it as a QR code or send it from the share sheet.</span></figcaption>
|
<figcaption><strong>Share the link</strong><span>Copy it and its password, show it as a QR code or send it from the share sheet.</span></figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@@ -333,7 +420,7 @@
|
|||||||
<div class="device device--laptop">
|
<div class="device device--laptop">
|
||||||
<div class="device__lid">
|
<div class="device__lid">
|
||||||
<div class="device__screen">
|
<div class="device__screen">
|
||||||
<img src="img/screenshots/desktop/light/03-qr-code-800.webp" srcset="img/screenshots/desktop/light/03-qr-code-1600.webp 1600w, img/screenshots/desktop/light/03-qr-code-800.webp 800w" sizes="(max-width: 840px) 94vw, 568px"
|
<img src="img/screenshots/desktop/light/03-qr-code-800.webp" srcset="img/screenshots/desktop/light/03-qr-code-1600.webp 1600w, img/screenshots/desktop/light/03-qr-code-800.webp 800w" sizes="(max-width: 839px) 94vw, 568px"
|
||||||
data-light-src="img/screenshots/desktop/light/03-qr-code-800.webp" data-light-srcset="img/screenshots/desktop/light/03-qr-code-1600.webp 1600w, img/screenshots/desktop/light/03-qr-code-800.webp 800w"
|
data-light-src="img/screenshots/desktop/light/03-qr-code-800.webp" data-light-srcset="img/screenshots/desktop/light/03-qr-code-1600.webp 1600w, img/screenshots/desktop/light/03-qr-code-800.webp 800w"
|
||||||
data-dark-src="img/screenshots/desktop/dark/03-qr-code-800.webp" data-dark-srcset="img/screenshots/desktop/dark/03-qr-code-1600.webp 1600w, img/screenshots/desktop/dark/03-qr-code-800.webp 800w"
|
data-dark-src="img/screenshots/desktop/dark/03-qr-code-800.webp" data-dark-srcset="img/screenshots/desktop/dark/03-qr-code-1600.webp 1600w, img/screenshots/desktop/dark/03-qr-code-800.webp 800w"
|
||||||
width="1512" height="982" loading="lazy" decoding="async"
|
width="1512" height="982" loading="lazy" decoding="async"
|
||||||
@@ -349,7 +436,7 @@
|
|||||||
<div class="device device--laptop">
|
<div class="device device--laptop">
|
||||||
<div class="device__lid">
|
<div class="device__lid">
|
||||||
<div class="device__screen">
|
<div class="device__screen">
|
||||||
<img src="img/screenshots/desktop/light/04-download-800.webp" srcset="img/screenshots/desktop/light/04-download-1600.webp 1600w, img/screenshots/desktop/light/04-download-800.webp 800w" sizes="(max-width: 840px) 94vw, 568px"
|
<img src="img/screenshots/desktop/light/04-download-800.webp" srcset="img/screenshots/desktop/light/04-download-1600.webp 1600w, img/screenshots/desktop/light/04-download-800.webp 800w" sizes="(max-width: 839px) 94vw, 568px"
|
||||||
data-light-src="img/screenshots/desktop/light/04-download-800.webp" data-light-srcset="img/screenshots/desktop/light/04-download-1600.webp 1600w, img/screenshots/desktop/light/04-download-800.webp 800w"
|
data-light-src="img/screenshots/desktop/light/04-download-800.webp" data-light-srcset="img/screenshots/desktop/light/04-download-1600.webp 1600w, img/screenshots/desktop/light/04-download-800.webp 800w"
|
||||||
data-dark-src="img/screenshots/desktop/dark/04-download-800.webp" data-dark-srcset="img/screenshots/desktop/dark/04-download-1600.webp 1600w, img/screenshots/desktop/dark/04-download-800.webp 800w"
|
data-dark-src="img/screenshots/desktop/dark/04-download-800.webp" data-dark-srcset="img/screenshots/desktop/dark/04-download-1600.webp 1600w, img/screenshots/desktop/dark/04-download-800.webp 800w"
|
||||||
width="1512" height="982" loading="lazy" decoding="async"
|
width="1512" height="982" loading="lazy" decoding="async"
|
||||||
@@ -365,11 +452,11 @@
|
|||||||
<div class="device device--laptop">
|
<div class="device device--laptop">
|
||||||
<div class="device__lid">
|
<div class="device__lid">
|
||||||
<div class="device__screen">
|
<div class="device__screen">
|
||||||
<img src="img/screenshots/desktop/light/05-admin-dashboard-800.webp" srcset="img/screenshots/desktop/light/05-admin-dashboard-1600.webp 1600w, img/screenshots/desktop/light/05-admin-dashboard-800.webp 800w" sizes="(max-width: 840px) 94vw, 568px"
|
<img src="img/screenshots/desktop/light/05-admin-dashboard-800.webp" srcset="img/screenshots/desktop/light/05-admin-dashboard-1600.webp 1600w, img/screenshots/desktop/light/05-admin-dashboard-800.webp 800w" sizes="(max-width: 839px) 94vw, 568px"
|
||||||
data-light-src="img/screenshots/desktop/light/05-admin-dashboard-800.webp" data-light-srcset="img/screenshots/desktop/light/05-admin-dashboard-1600.webp 1600w, img/screenshots/desktop/light/05-admin-dashboard-800.webp 800w"
|
data-light-src="img/screenshots/desktop/light/05-admin-dashboard-800.webp" data-light-srcset="img/screenshots/desktop/light/05-admin-dashboard-1600.webp 1600w, img/screenshots/desktop/light/05-admin-dashboard-800.webp 800w"
|
||||||
data-dark-src="img/screenshots/desktop/dark/05-admin-dashboard-800.webp" data-dark-srcset="img/screenshots/desktop/dark/05-admin-dashboard-1600.webp 1600w, img/screenshots/desktop/dark/05-admin-dashboard-800.webp 800w"
|
data-dark-src="img/screenshots/desktop/dark/05-admin-dashboard-800.webp" data-dark-srcset="img/screenshots/desktop/dark/05-admin-dashboard-1600.webp 1600w, img/screenshots/desktop/dark/05-admin-dashboard-800.webp 800w"
|
||||||
width="1512" height="982" loading="lazy" decoding="async"
|
width="1512" height="982" loading="lazy" decoding="async"
|
||||||
alt="The admin dashboard with share statistics, disk usage and the table of shares">
|
alt="The admin dashboard with share and file counts, disk usage and the list of shares with Sort by">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -381,15 +468,15 @@
|
|||||||
<div class="device device--laptop">
|
<div class="device device--laptop">
|
||||||
<div class="device__lid">
|
<div class="device__lid">
|
||||||
<div class="device__screen">
|
<div class="device__screen">
|
||||||
<img src="img/screenshots/desktop/light/06-admin-settings-800.webp" srcset="img/screenshots/desktop/light/06-admin-settings-1600.webp 1600w, img/screenshots/desktop/light/06-admin-settings-800.webp 800w" sizes="(max-width: 840px) 94vw, 568px"
|
<img src="img/screenshots/desktop/light/06-admin-settings-800.webp" srcset="img/screenshots/desktop/light/06-admin-settings-1600.webp 1600w, img/screenshots/desktop/light/06-admin-settings-800.webp 800w" sizes="(max-width: 839px) 94vw, 568px"
|
||||||
data-light-src="img/screenshots/desktop/light/06-admin-settings-800.webp" data-light-srcset="img/screenshots/desktop/light/06-admin-settings-1600.webp 1600w, img/screenshots/desktop/light/06-admin-settings-800.webp 800w"
|
data-light-src="img/screenshots/desktop/light/06-admin-settings-800.webp" data-light-srcset="img/screenshots/desktop/light/06-admin-settings-1600.webp 1600w, img/screenshots/desktop/light/06-admin-settings-800.webp 800w"
|
||||||
data-dark-src="img/screenshots/desktop/dark/06-admin-settings-800.webp" data-dark-srcset="img/screenshots/desktop/dark/06-admin-settings-1600.webp 1600w, img/screenshots/desktop/dark/06-admin-settings-800.webp 800w"
|
data-dark-src="img/screenshots/desktop/dark/06-admin-settings-800.webp" data-dark-srcset="img/screenshots/desktop/dark/06-admin-settings-1600.webp 1600w, img/screenshots/desktop/dark/06-admin-settings-800.webp 800w"
|
||||||
width="1512" height="982" loading="lazy" decoding="async"
|
width="1512" height="982" loading="lazy" decoding="async"
|
||||||
alt="The admin settings page with branding and upload limits">
|
alt="The admin settings page with the colour profile, branding and upload protection">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<figcaption><strong>Admin settings</strong><span>Upload limits, storage quota, your logo and title.</span></figcaption>
|
<figcaption><strong>Admin settings</strong><span>Colour profile, your logo and title, upload protection and limits.</span></figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -398,7 +485,7 @@
|
|||||||
<figure class="shot">
|
<figure class="shot">
|
||||||
<div class="device device--phone">
|
<div class="device device--phone">
|
||||||
<div class="device__screen">
|
<div class="device__screen">
|
||||||
<img src="img/screenshots/phone/light/01-upload-540.webp" srcset="img/screenshots/phone/light/01-upload-1080.webp 1080w, img/screenshots/phone/light/01-upload-540.webp 540w" sizes="(max-width: 840px) 44vw, 232px"
|
<img src="img/screenshots/phone/light/01-upload-540.webp" srcset="img/screenshots/phone/light/01-upload-1080.webp 1080w, img/screenshots/phone/light/01-upload-540.webp 540w" sizes="(max-width: 839px) 44vw, 232px"
|
||||||
data-light-src="img/screenshots/phone/light/01-upload-540.webp" data-light-srcset="img/screenshots/phone/light/01-upload-1080.webp 1080w, img/screenshots/phone/light/01-upload-540.webp 540w"
|
data-light-src="img/screenshots/phone/light/01-upload-540.webp" data-light-srcset="img/screenshots/phone/light/01-upload-1080.webp 1080w, img/screenshots/phone/light/01-upload-540.webp 540w"
|
||||||
data-dark-src="img/screenshots/phone/dark/01-upload-540.webp" data-dark-srcset="img/screenshots/phone/dark/01-upload-1080.webp 1080w, img/screenshots/phone/dark/01-upload-540.webp 540w"
|
data-dark-src="img/screenshots/phone/dark/01-upload-540.webp" data-dark-srcset="img/screenshots/phone/dark/01-upload-1080.webp 1080w, img/screenshots/phone/dark/01-upload-540.webp 540w"
|
||||||
width="393" height="852" loading="lazy" decoding="async"
|
width="393" height="852" loading="lazy" decoding="async"
|
||||||
@@ -412,7 +499,7 @@
|
|||||||
<figure class="shot">
|
<figure class="shot">
|
||||||
<div class="device device--phone">
|
<div class="device device--phone">
|
||||||
<div class="device__screen">
|
<div class="device__screen">
|
||||||
<img src="img/screenshots/phone/light/02-password-540.webp" srcset="img/screenshots/phone/light/02-password-1080.webp 1080w, img/screenshots/phone/light/02-password-540.webp 540w" sizes="(max-width: 840px) 44vw, 232px"
|
<img src="img/screenshots/phone/light/02-password-540.webp" srcset="img/screenshots/phone/light/02-password-1080.webp 1080w, img/screenshots/phone/light/02-password-540.webp 540w" sizes="(max-width: 839px) 44vw, 232px"
|
||||||
data-light-src="img/screenshots/phone/light/02-password-540.webp" data-light-srcset="img/screenshots/phone/light/02-password-1080.webp 1080w, img/screenshots/phone/light/02-password-540.webp 540w"
|
data-light-src="img/screenshots/phone/light/02-password-540.webp" data-light-srcset="img/screenshots/phone/light/02-password-1080.webp 1080w, img/screenshots/phone/light/02-password-540.webp 540w"
|
||||||
data-dark-src="img/screenshots/phone/dark/02-password-540.webp" data-dark-srcset="img/screenshots/phone/dark/02-password-1080.webp 1080w, img/screenshots/phone/dark/02-password-540.webp 540w"
|
data-dark-src="img/screenshots/phone/dark/02-password-540.webp" data-dark-srcset="img/screenshots/phone/dark/02-password-1080.webp 1080w, img/screenshots/phone/dark/02-password-540.webp 540w"
|
||||||
width="393" height="852" loading="lazy" decoding="async"
|
width="393" height="852" loading="lazy" decoding="async"
|
||||||
@@ -426,7 +513,7 @@
|
|||||||
<figure class="shot">
|
<figure class="shot">
|
||||||
<div class="device device--phone">
|
<div class="device device--phone">
|
||||||
<div class="device__screen">
|
<div class="device__screen">
|
||||||
<img src="img/screenshots/phone/light/03-download-540.webp" srcset="img/screenshots/phone/light/03-download-1080.webp 1080w, img/screenshots/phone/light/03-download-540.webp 540w" sizes="(max-width: 840px) 44vw, 232px"
|
<img src="img/screenshots/phone/light/03-download-540.webp" srcset="img/screenshots/phone/light/03-download-1080.webp 1080w, img/screenshots/phone/light/03-download-540.webp 540w" sizes="(max-width: 839px) 44vw, 232px"
|
||||||
data-light-src="img/screenshots/phone/light/03-download-540.webp" data-light-srcset="img/screenshots/phone/light/03-download-1080.webp 1080w, img/screenshots/phone/light/03-download-540.webp 540w"
|
data-light-src="img/screenshots/phone/light/03-download-540.webp" data-light-srcset="img/screenshots/phone/light/03-download-1080.webp 1080w, img/screenshots/phone/light/03-download-540.webp 540w"
|
||||||
data-dark-src="img/screenshots/phone/dark/03-download-540.webp" data-dark-srcset="img/screenshots/phone/dark/03-download-1080.webp 1080w, img/screenshots/phone/dark/03-download-540.webp 540w"
|
data-dark-src="img/screenshots/phone/dark/03-download-540.webp" data-dark-srcset="img/screenshots/phone/dark/03-download-1080.webp 1080w, img/screenshots/phone/dark/03-download-540.webp 540w"
|
||||||
width="393" height="852" loading="lazy" decoding="async"
|
width="393" height="852" loading="lazy" decoding="async"
|
||||||
@@ -440,7 +527,7 @@
|
|||||||
<figure class="shot">
|
<figure class="shot">
|
||||||
<div class="device device--phone">
|
<div class="device device--phone">
|
||||||
<div class="device__screen">
|
<div class="device__screen">
|
||||||
<img src="img/screenshots/phone/light/04-qr-code-540.webp" srcset="img/screenshots/phone/light/04-qr-code-1080.webp 1080w, img/screenshots/phone/light/04-qr-code-540.webp 540w" sizes="(max-width: 840px) 44vw, 232px"
|
<img src="img/screenshots/phone/light/04-qr-code-540.webp" srcset="img/screenshots/phone/light/04-qr-code-1080.webp 1080w, img/screenshots/phone/light/04-qr-code-540.webp 540w" sizes="(max-width: 839px) 44vw, 232px"
|
||||||
data-light-src="img/screenshots/phone/light/04-qr-code-540.webp" data-light-srcset="img/screenshots/phone/light/04-qr-code-1080.webp 1080w, img/screenshots/phone/light/04-qr-code-540.webp 540w"
|
data-light-src="img/screenshots/phone/light/04-qr-code-540.webp" data-light-srcset="img/screenshots/phone/light/04-qr-code-1080.webp 1080w, img/screenshots/phone/light/04-qr-code-540.webp 540w"
|
||||||
data-dark-src="img/screenshots/phone/dark/04-qr-code-540.webp" data-dark-srcset="img/screenshots/phone/dark/04-qr-code-1080.webp 1080w, img/screenshots/phone/dark/04-qr-code-540.webp 540w"
|
data-dark-src="img/screenshots/phone/dark/04-qr-code-540.webp" data-dark-srcset="img/screenshots/phone/dark/04-qr-code-1080.webp 1080w, img/screenshots/phone/dark/04-qr-code-540.webp 540w"
|
||||||
width="393" height="852" loading="lazy" decoding="async"
|
width="393" height="852" loading="lazy" decoding="async"
|
||||||
@@ -480,7 +567,7 @@
|
|||||||
<tr><th scope="row">End-to-end encryption</th><td class="is-us" data-compare="sealshare-e2e">No</td><td>Enterprise add-on</td><td>No</td><td>No</td><td>Higher Workspace editions</td></tr>
|
<tr><th scope="row">End-to-end encryption</th><td class="is-us" data-compare="sealshare-e2e">No</td><td>Enterprise add-on</td><td>No</td><td>No</td><td>Higher Workspace editions</td></tr>
|
||||||
<tr><th scope="row">Folder upload</th><td class="is-us">Yes</td><td>No, folders are flattened</td><td>Yes</td><td>Yes</td><td>Yes</td></tr>
|
<tr><th scope="row">Folder upload</th><td class="is-us">Yes</td><td>No, folders are flattened</td><td>Yes</td><td>Yes</td><td>Yes</td></tr>
|
||||||
<tr><th scope="row">Custom branding</th><td class="is-us">Yes: logo, title, description</td><td>Ultimate plan and up</td><td>Through paid kDrive</td><td>Standard plan and up</td><td>—</td></tr>
|
<tr><th scope="row">Custom branding</th><td class="is-us">Yes: logo, title, description</td><td>Ultimate plan and up</td><td>Through paid kDrive</td><td>Standard plan and up</td><td>—</td></tr>
|
||||||
<tr><th scope="row">Maximum file size</th><td class="is-us">Set by you (server limits)</td><td>3 GB or 100 GB a month on Free<sup>1</sup></td><td>50 GB per transfer</td><td>2 GB on Basic (free)</td><td>15 GB of free storage</td></tr>
|
<tr><th scope="row">Maximum file size</th><td class="is-us">Set by you (no server cap)</td><td>3 GB or 100 GB a month on Free<sup>1</sup></td><td>50 GB per transfer</td><td>2 GB on Basic (free)</td><td>15 GB of free storage</td></tr>
|
||||||
<tr><th scope="row">Licence and cost</th><td class="is-us">MIT, free</td><td>Free (non-commercial use); Starter USD 8/month<sup>2</sup></td><td>Free</td><td>Free; Plus EUR 11.99/month<sup>2</sup></td><td>Free; 100 GB CHF 2/month<sup>2</sup></td></tr>
|
<tr><th scope="row">Licence and cost</th><td class="is-us">MIT, free</td><td>Free (non-commercial use); Starter USD 8/month<sup>2</sup></td><td>Free</td><td>Free; Plus EUR 11.99/month<sup>2</sup></td><td>Free; 100 GB CHF 2/month<sup>2</sup></td></tr>
|
||||||
<tr><th scope="row">QR code for the link</th><td class="is-us">Yes</td><td>—</td><td>—</td><td>—</td><td>—</td></tr>
|
<tr><th scope="row">QR code for the link</th><td class="is-us">Yes</td><td>—</td><td>—</td><td>—</td><td>—</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -505,14 +592,14 @@
|
|||||||
<tr><th scope="row">End-to-end encryption</th><td class="is-us" data-compare="sealshare-e2e">No</td><td>No</td><td>No</td><td>Optional</td><td>No</td></tr>
|
<tr><th scope="row">End-to-end encryption</th><td class="is-us" data-compare="sealshare-e2e">No</td><td>No</td><td>No</td><td>Optional</td><td>No</td></tr>
|
||||||
<tr><th scope="row">Folder upload</th><td class="is-us">Yes</td><td>Yes</td><td>No</td><td>No, files are listed singly<sup>3</sup></td><td>Yes, keeps structure</td></tr>
|
<tr><th scope="row">Folder upload</th><td class="is-us">Yes</td><td>Yes</td><td>No</td><td>No, files are listed singly<sup>3</sup></td><td>Yes, keeps structure</td></tr>
|
||||||
<tr><th scope="row">Custom branding</th><td class="is-us">Yes: logo, title, description</td><td>Yes: logo, name, colours, CSS</td><td>Yes, by editing templates</td><td>Yes: logo, CSS, JavaScript</td><td>Yes: logo, colours, themes</td></tr>
|
<tr><th scope="row">Custom branding</th><td class="is-us">Yes: logo, title, description</td><td>Yes: logo, name, colours, CSS</td><td>Yes, by editing templates</td><td>Yes: logo, CSS, JavaScript</td><td>Yes: logo, colours, themes</td></tr>
|
||||||
<tr><th scope="row">Maximum file size</th><td class="is-us">Set by you (server limits)</td><td>Set by you (1 GB per share by default)</td><td>Set by you (no limit by default)</td><td>Set by you (100 GB by default)</td><td>Set by you (2 GB per share by default)</td></tr>
|
<tr><th scope="row">Maximum file size</th><td class="is-us">Set by you (no server cap)</td><td>Set by you (1 GB per share by default)</td><td>Set by you (no limit by default)</td><td>Set by you (100 GB by default)</td><td>Set by you (2 GB per share by default)</td></tr>
|
||||||
<tr><th scope="row">Licence and cost</th><td class="is-us">MIT, free</td><td>BSD-2-Clause, free</td><td>BSD-2-Clause, free</td><td>AGPL-3.0, free</td><td>MIT, free</td></tr>
|
<tr><th scope="row">Licence and cost</th><td class="is-us">MIT, free</td><td>BSD-2-Clause, free</td><td>BSD-2-Clause, free</td><td>AGPL-3.0, free</td><td>MIT, free</td></tr>
|
||||||
<tr><th scope="row">QR code for the link</th><td class="is-us">Yes</td><td>Yes</td><td>Yes</td><td>Yes</td><td>No</td></tr>
|
<tr><th scope="row">QR code for the link</th><td class="is-us">Yes</td><td>Yes</td><td>Yes</td><td>Yes</td><td>No</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="compare__note">SealShare encrypts files on the server as they arrive. With a share password the key is never stored; without one it is kept in SealShare's database. A value marked “—” could not be confirmed from the product's own sources. Checked on 13 September 2026.</p>
|
<p class="compare__note">SealShare encrypts files in the browser before they are uploaded. With a share password the key is never stored as it is; without one it is kept in SealShare's database. A value marked “—” could not be confirmed from the product's own sources. Other products checked on 13 September 2026.</p>
|
||||||
<div class="compare__notes">
|
<div class="compare__notes">
|
||||||
<p class="compare__note"><sup>1</sup> WeTransfer's pricing page and its help centre disagree. The pricing page lists password protection on every plan, expiry up to 7 days and up to 100 GB a month on Free; the help centre (May 2026) lists password protection under Ultimate, expiry up to 3 days, and up to 10 transfers or 3 GB in 30 days on Free.</p>
|
<p class="compare__note"><sup>1</sup> WeTransfer's pricing page and its help centre disagree. The pricing page lists password protection on every plan, expiry up to 7 days and up to 100 GB a month on Free; the help centre (May 2026) lists password protection under Ultimate, expiry up to 3 days, and up to 10 transfers or 3 GB in 30 days on Free.</p>
|
||||||
<p class="compare__note"><sup>2</sup> The cheapest paid plan, at the price shown in Switzerland on the date above; prices differ by country. WeTransfer's Free plan is for individual, non-commercial use. Dropbox Plus has no passwords or branding for transfers; Google One 100 GB adds storage, not sharing features.</p>
|
<p class="compare__note"><sup>2</sup> The cheapest paid plan, at the price shown in Switzerland on the date above; prices differ by country. WeTransfer's Free plan is for individual, non-commercial use. Dropbox Plus has no passwords or branding for transfers; Google One 100 GB adds storage, not sharing features.</p>
|
||||||
@@ -541,9 +628,10 @@
|
|||||||
<div class="install__copy">
|
<div class="install__copy">
|
||||||
<span class="eyebrow"><svg class="icon" aria-hidden="true"><use href="#i-deployed-code"/></svg>Install</span>
|
<span class="eyebrow"><svg class="icon" aria-hidden="true"><use href="#i-deployed-code"/></svg>Install</span>
|
||||||
<h2 class="headline" id="install-title">Running in a few minutes</h2>
|
<h2 class="headline" id="install-title">Running in a few minutes</h2>
|
||||||
<p class="lede">All you need is a server with Docker and a domain name.</p>
|
<p class="lede">All you need is a server with Docker, a domain name and HTTPS.</p>
|
||||||
<ul class="install__list">
|
<ul class="install__list">
|
||||||
<li><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg><span>The image includes the web server, with automatic TLS certificates if you want them, and uses SQLite — no separate database.</span></li>
|
<li><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg><span>The image includes the web server and uses SQLite — no separate database.</span></li>
|
||||||
|
<li><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg><span>Uploads need HTTPS. Set <code>AUTO_HTTPS</code> to get a Let's Encrypt certificate, or put a reverse proxy that terminates TLS in front.</span></li>
|
||||||
<li><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg><span>Migrations run on start. Open your domain and the setup wizard creates the first admin account.</span></li>
|
<li><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg><span>Migrations run on start. Open your domain and the setup wizard creates the first admin account.</span></li>
|
||||||
<li><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg><span>Your files and database live in Docker volumes on your server.</span></li>
|
<li><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg><span>Your files and database live in Docker volumes on your server.</span></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -558,7 +646,8 @@ cp docker-compose.example.yml docker-compose.yml
|
|||||||
<span class="comment"># Generate an app key and paste it into docker-compose.yml</span>
|
<span class="comment"># Generate an app key and paste it into docker-compose.yml</span>
|
||||||
docker run --rm gitea.nonameweb.ch/nonameweb/sealshare:latest php artisan key:generate --show
|
docker run --rm gitea.nonameweb.ch/nonameweb/sealshare:latest php artisan key:generate --show
|
||||||
|
|
||||||
<span class="comment"># Edit docker-compose.yml — set APP_KEY and APP_URL; AUTO_HTTPS and SERVER_NAME for automatic TLS</span>
|
<span class="comment"># Edit docker-compose.yml — set APP_KEY and APP_URL</span>
|
||||||
|
<span class="comment"># Uploads need HTTPS: set AUTO_HTTPS and SERVER_NAME, or use a reverse proxy</span>
|
||||||
<span class="comment"># Then start:</span>
|
<span class="comment"># Then start:</span>
|
||||||
docker compose up -d</code></pre>
|
docker compose up -d</code></pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -575,7 +664,14 @@ docker compose up -d</code></pre>
|
|||||||
<summary>Is SealShare end-to-end encrypted?<svg class="icon" aria-hidden="true"><use href="#i-expand-more"/></svg></summary>
|
<summary>Is SealShare end-to-end encrypted?<svg class="icon" aria-hidden="true"><use href="#i-expand-more"/></svg></summary>
|
||||||
<div class="faq__answer">
|
<div class="faq__answer">
|
||||||
<p>No. The uploader's browser encrypts each file in chunks with AES-256-GCM before sending it, and SealShare stores the files only in encrypted form. But the key comes from your server, which checks every chunk and decrypts the files again for downloads — so the server can read them, which is why it matters that the server is yours.</p>
|
<p>No. The uploader's browser encrypts each file in chunks with AES-256-GCM before sending it, and SealShare stores the files only in encrypted form. But the key comes from your server, which checks every chunk and decrypts the files again for downloads — so the server can read them, which is why it matters that the server is yours.</p>
|
||||||
<p>For a share without a password the key is kept in SealShare's database. With a share password the key is locked with that password and never stored as it is, so the files cannot be decrypted without it.</p>
|
<p>For a share without a password the key is kept in SealShare's database. With a share password the key is wrapped with a key derived from that password (Argon2id) and never stored as it is, so the files cannot be decrypted without it.</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>Why do uploads need HTTPS?<svg class="icon" aria-hidden="true"><use href="#i-expand-more"/></svg></summary>
|
||||||
|
<div class="faq__answer">
|
||||||
|
<p>Files are encrypted in the uploader's browser with WebCrypto, and browsers only offer WebCrypto over HTTPS or on localhost. Over plain HTTP the upload page says so and takes no files; downloads keep working.</p>
|
||||||
|
<p>Set <code>AUTO_HTTPS: "true"</code> and <code>SERVER_NAME</code> to get a Let's Encrypt certificate, with ports 80 and 443 reachable from the internet. Or put a reverse proxy that terminates TLS in front of SealShare.</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<details>
|
<details>
|
||||||
@@ -587,7 +683,8 @@ docker compose up -d</code></pre>
|
|||||||
<details>
|
<details>
|
||||||
<summary>How big can files be?<svg class="icon" aria-hidden="true"><use href="#i-expand-more"/></svg></summary>
|
<summary>How big can files be?<svg class="icon" aria-hidden="true"><use href="#i-expand-more"/></svg></summary>
|
||||||
<div class="faq__answer">
|
<div class="faq__answer">
|
||||||
<p>As big as your server allows. The Docker image accepts up to 4 GB per file by default; within that, you set the maximum file size and share size in the admin settings (100 MB and 2 GB out of the box). Behind a reverse proxy, raise its request size and timeouts as well.</p>
|
<p>As big as you allow. You set the maximum file size, share size and storage quota in the admin settings (100 MB, 2 GB and 20 GB out of the box); PHP's upload limits do not cap them.</p>
|
||||||
|
<p>Files go up in chunks of 16 MB (<code>UPLOAD_CHUNK_SIZE_MB</code>), so a reverse proxy only needs a request body limit a little above one chunk. Unfinished uploads count towards the quota and are deleted after 4 hours.</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<details>
|
<details>
|
||||||
@@ -655,6 +752,23 @@ docker compose up -d</code></pre>
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// The top app bar takes surface-container once the page scrolls under it.
|
||||||
|
const nav = document.querySelector('.nav');
|
||||||
|
const markScrolled = () => nav.toggleAttribute('data-scrolled', window.scrollY > 0);
|
||||||
|
|
||||||
|
window.addEventListener('scroll', markScrolled, { passive: true });
|
||||||
|
markScrolled();
|
||||||
|
|
||||||
|
// Theme: Light, then Dark, then System (the <head> script applies and remembers it).
|
||||||
|
const themeToggle = document.getElementById('themeToggle');
|
||||||
|
const themeNames = { light: 'Light', dark: 'Dark', system: 'System' };
|
||||||
|
const nextTheme = { light: 'dark', dark: 'system', system: 'light' };
|
||||||
|
const nameThemeToggle = () => themeToggle.setAttribute('aria-label', 'Theme: ' + themeNames[sealshareTheme.choice]);
|
||||||
|
|
||||||
|
themeToggle.addEventListener('click', () => sealshareTheme.set(nextTheme[sealshareTheme.choice]));
|
||||||
|
document.addEventListener('sealshare:theme', nameThemeToggle);
|
||||||
|
nameThemeToggle();
|
||||||
|
|
||||||
// Phone menu.
|
// Phone menu.
|
||||||
const navToggle = document.getElementById('navToggle');
|
const navToggle = document.getElementById('navToggle');
|
||||||
const navLinks = document.getElementById('navLinks');
|
const navLinks = document.getElementById('navLinks');
|
||||||
@@ -684,11 +798,14 @@ docker compose up -d</code></pre>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Screenshot gallery: one device at a time, in the chosen theme (the visitor's, to begin with).
|
// Screenshot gallery: one device at a time, in the site's theme to begin with and whenever it
|
||||||
|
// changes, until the visitor picks Light or Dark here.
|
||||||
const deviceButtons = document.querySelectorAll('[data-gallery-device]');
|
const deviceButtons = document.querySelectorAll('[data-gallery-device]');
|
||||||
const themeButtons = document.querySelectorAll('[data-gallery-theme]');
|
const themeButtons = document.querySelectorAll('[data-gallery-theme]');
|
||||||
const galleries = document.querySelectorAll('[data-gallery]');
|
const galleries = document.querySelectorAll('[data-gallery]');
|
||||||
const shots = document.querySelectorAll('[data-gallery] img');
|
const shots = document.querySelectorAll('[data-gallery] img');
|
||||||
|
let galleryTheme = 'light';
|
||||||
|
let galleryThemeChosen = false;
|
||||||
|
|
||||||
const press = (buttons, pressed) => buttons.forEach((button) => button.setAttribute('aria-pressed', String(button === pressed)));
|
const press = (buttons, pressed) => buttons.forEach((button) => button.setAttribute('aria-pressed', String(button === pressed)));
|
||||||
|
|
||||||
@@ -699,6 +816,12 @@ docker compose up -d</code></pre>
|
|||||||
|
|
||||||
const showTheme = (theme) => {
|
const showTheme = (theme) => {
|
||||||
press(themeButtons, [...themeButtons].find((button) => button.dataset.galleryTheme === theme));
|
press(themeButtons, [...themeButtons].find((button) => button.dataset.galleryTheme === theme));
|
||||||
|
|
||||||
|
if (theme === galleryTheme) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
galleryTheme = theme;
|
||||||
shots.forEach((shot) => {
|
shots.forEach((shot) => {
|
||||||
shot.srcset = shot.dataset[theme + 'Srcset'];
|
shot.srcset = shot.dataset[theme + 'Srcset'];
|
||||||
shot.src = shot.dataset[theme + 'Src'];
|
shot.src = shot.dataset[theme + 'Src'];
|
||||||
@@ -706,11 +829,18 @@ docker compose up -d</code></pre>
|
|||||||
};
|
};
|
||||||
|
|
||||||
deviceButtons.forEach((button) => button.addEventListener('click', () => showDevice(button.dataset.galleryDevice)));
|
deviceButtons.forEach((button) => button.addEventListener('click', () => showDevice(button.dataset.galleryDevice)));
|
||||||
themeButtons.forEach((button) => button.addEventListener('click', () => showTheme(button.dataset.galleryTheme)));
|
themeButtons.forEach((button) => button.addEventListener('click', () => {
|
||||||
|
galleryThemeChosen = true;
|
||||||
|
showTheme(button.dataset.galleryTheme);
|
||||||
|
}));
|
||||||
|
|
||||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
document.addEventListener('sealshare:theme', (event) => {
|
||||||
showTheme('dark');
|
if (!galleryThemeChosen) {
|
||||||
}
|
showTheme(event.detail.theme);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
showTheme(sealshareTheme.theme === 'dark' ? 'dark' : 'light');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -7,6 +7,83 @@
|
|||||||
<meta name="description" content="How the SealShare website handles visitor data: server logs at the host and Plausible analytics, nothing else.">
|
<meta name="description" content="How the SealShare website handles visitor data: server logs at the host and Plausible analytics, nothing else.">
|
||||||
<meta name="theme-color" content="#faf4ff" media="(prefers-color-scheme: light)">
|
<meta name="theme-color" content="#faf4ff" media="(prefers-color-scheme: light)">
|
||||||
<meta name="theme-color" content="#10062d" media="(prefers-color-scheme: dark)">
|
<meta name="theme-color" content="#10062d" media="(prefers-color-scheme: dark)">
|
||||||
|
<script>
|
||||||
|
// Light, Dark or System, decided before the first paint: the visitor's choice in the nav, kept in
|
||||||
|
// localStorage, else the system's. css/theme.css keys on <html data-theme>.
|
||||||
|
(function () {
|
||||||
|
var root = document.documentElement;
|
||||||
|
var key = 'sealshare-website-theme';
|
||||||
|
var choices = ['light', 'dark', 'system'];
|
||||||
|
var surfaces = { light: '#faf4ff', dark: '#10062d' };
|
||||||
|
var system = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
var choice = 'system';
|
||||||
|
|
||||||
|
try {
|
||||||
|
var stored = localStorage.getItem(key);
|
||||||
|
|
||||||
|
if (choices.indexOf(stored) !== -1) {
|
||||||
|
choice = stored;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Storage is blocked: follow the system.
|
||||||
|
}
|
||||||
|
|
||||||
|
// A <picture>'s dark source (data-theme-dark) follows the site's theme instead of the system's.
|
||||||
|
var sources = function () {
|
||||||
|
document.querySelectorAll('source[data-theme-dark]').forEach(function (source) {
|
||||||
|
source.media = root.dataset.theme === 'dark' ? 'all' : 'not all';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var apply = function () {
|
||||||
|
var theme = choice === 'system' ? (system.matches ? 'dark' : 'light') : choice;
|
||||||
|
|
||||||
|
root.dataset.themeChoice = choice;
|
||||||
|
root.dataset.theme = theme;
|
||||||
|
|
||||||
|
document.querySelectorAll('meta[name="theme-color"]').forEach(function (meta) {
|
||||||
|
meta.removeAttribute('media');
|
||||||
|
meta.setAttribute('content', surfaces[theme]);
|
||||||
|
});
|
||||||
|
|
||||||
|
sources();
|
||||||
|
document.dispatchEvent(new CustomEvent('sealshare:theme', { detail: { choice: choice, theme: theme } }));
|
||||||
|
};
|
||||||
|
|
||||||
|
window.sealshareTheme = {
|
||||||
|
sources: sources,
|
||||||
|
get choice() {
|
||||||
|
return choice;
|
||||||
|
},
|
||||||
|
get theme() {
|
||||||
|
return root.dataset.theme;
|
||||||
|
},
|
||||||
|
set: function (next) {
|
||||||
|
if (choices.indexOf(next) === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
choice = next;
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key, next);
|
||||||
|
} catch (error) {
|
||||||
|
// Storage is blocked: the page still switches, it just will not remember.
|
||||||
|
}
|
||||||
|
|
||||||
|
apply();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
system.addEventListener('change', function () {
|
||||||
|
if (choice === 'system') {
|
||||||
|
apply();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
apply();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
<!-- Privacy-friendly analytics by Plausible -->
|
<!-- Privacy-friendly analytics by Plausible -->
|
||||||
<script async src="https://plausible.io/js/pa-zWB4R2-rrBeLfwms3uIBT.js"></script>
|
<script async src="https://plausible.io/js/pa-zWB4R2-rrBeLfwms3uIBT.js"></script>
|
||||||
<script>
|
<script>
|
||||||
@@ -19,18 +96,21 @@
|
|||||||
<link rel="stylesheet" href="css/theme.css">
|
<link rel="stylesheet" href="css/theme.css">
|
||||||
</head>
|
</head>
|
||||||
<body id="top">
|
<body id="top">
|
||||||
<a class="skip-link" href="#main">Skip to content</a>
|
<a class="skip-link btn btn--filled" href="#main">Skip to content</a>
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="sprite" aria-hidden="true">
|
<svg xmlns="http://www.w3.org/2000/svg" class="sprite" aria-hidden="true">
|
||||||
<symbol id="i-menu" viewBox="0 -960 960 960"><path d="M160-240q-17 0-28.5-11.5T120-280q0-17 11.5-28.5T160-320h640q17 0 28.5 11.5T840-280q0 17-11.5 28.5T800-240H160Zm0-200q-17 0-28.5-11.5T120-480q0-17 11.5-28.5T160-520h640q17 0 28.5 11.5T840-480q0 17-11.5 28.5T800-440H160Zm0-200q-17 0-28.5-11.5T120-680q0-17 11.5-28.5T160-720h640q17 0 28.5 11.5T840-680q0 17-11.5 28.5T800-640H160Z"/></symbol>
|
<symbol id="i-menu" viewBox="0 -960 960 960"><path d="M160-240q-17 0-28.5-11.5T120-280q0-17 11.5-28.5T160-320h640q17 0 28.5 11.5T840-280q0 17-11.5 28.5T800-240H160Zm0-200q-17 0-28.5-11.5T120-480q0-17 11.5-28.5T160-520h640q17 0 28.5 11.5T840-480q0 17-11.5 28.5T800-440H160Zm0-200q-17 0-28.5-11.5T120-680q0-17 11.5-28.5T160-720h640q17 0 28.5 11.5T840-680q0 17-11.5 28.5T800-640H160Z"/></symbol>
|
||||||
<symbol id="i-close" viewBox="0 -960 960 960"><path d="M480-424 284-228q-11 11-28 11t-28-11q-11-11-11-28t11-28l196-196-196-196q-11-11-11-28t11-28q11-11 28-11t28 11l196 196 196-196q11-11 28-11t28 11q11 11 11 28t-11 28L536-480l196 196q11 11 11 28t-11 28q-11 11-28 11t-28-11L480-424Z"/></symbol>
|
<symbol id="i-close" viewBox="0 -960 960 960"><path d="M480-424 284-228q-11 11-28 11t-28-11q-11-11-11-28t11-28l196-196-196-196q-11-11-11-28t11-28q11-11 28-11t28 11l196 196 196-196q11-11 28-11t28 11q11 11 11 28t-11 28L536-480l196 196q11 11 11 28t-11 28q-11 11-28 11t-28-11L480-424Z"/></symbol>
|
||||||
<symbol id="i-open-in-new" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h240q17 0 28.5 11.5T480-800q0 17-11.5 28.5T440-760H200v560h560v-240q0-17 11.5-28.5T800-480q17 0 28.5 11.5T840-440v240q0 33-23.5 56.5T760-120H200Zm560-584L416-360q-11 11-28 11t-28-11q-11-11-11-28t11-28l344-344H600q-17 0-28.5-11.5T560-800q0-17 11.5-28.5T600-840h200q17 0 28.5 11.5T840-800v200q0 17-11.5 28.5T800-560q-17 0-28.5-11.5T760-600v-104Z"/></symbol>
|
<symbol id="i-open-in-new" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h240q17 0 28.5 11.5T480-800q0 17-11.5 28.5T440-760H200v560h560v-240q0-17 11.5-28.5T800-480q17 0 28.5 11.5T840-440v240q0 33-23.5 56.5T760-120H200Zm560-584L416-360q-11 11-28 11t-28-11q-11-11-11-28t11-28l344-344H600q-17 0-28.5-11.5T560-800q0-17 11.5-28.5T600-840h200q17 0 28.5 11.5T840-800v200q0 17-11.5 28.5T800-560q-17 0-28.5-11.5T760-600v-104Z"/></symbol>
|
||||||
|
<symbol id="i-light-mode" viewBox="0 -960 960 960"><path d="M480-360q50 0 85-35t35-85q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 50 35 85t85 35Zm0 80q-83 0-141.5-58.5T280-480q0-83 58.5-141.5T480-680q83 0 141.5 58.5T680-480q0 83-58.5 141.5T480-280ZM80-440q-17 0-28.5-11.5T40-480q0-17 11.5-28.5T80-520h80q17 0 28.5 11.5T200-480q0 17-11.5 28.5T160-440H80Zm720 0q-17 0-28.5-11.5T760-480q0-17 11.5-28.5T800-520h80q17 0 28.5 11.5T920-480q0 17-11.5 28.5T880-440h-80ZM480-760q-17 0-28.5-11.5T440-800v-80q0-17 11.5-28.5T480-920q17 0 28.5 11.5T520-880v80q0 17-11.5 28.5T480-760Zm0 720q-17 0-28.5-11.5T440-80v-80q0-17 11.5-28.5T480-200q17 0 28.5 11.5T520-160v80q0 17-11.5 28.5T480-40ZM226-678l-43-42q-12-11-11.5-28t11.5-29q12-12 29-12t28 12l42 43q11 12 11 28t-11 28q-11 12-27.5 11.5T226-678Zm494 495-42-43q-11-12-11-28.5t11-27.5q11-12 27.5-11.5T734-282l43 42q12 11 11.5 28T777-183q-12 12-29 12t-28-12Zm-42-495q-12-11-11.5-27.5T678-734l42-43q11-12 28-11.5t29 11.5q12 12 12 29t-12 28l-43 42q-12 11-28 11t-28-11ZM183-183q-12-12-12-29t12-28l43-42q12-11 28.5-11t27.5 11q12 11 11.5 27.5T282-226l-42 43q-11 12-28 11.5T183-183Zm297-297Z"/></symbol>
|
||||||
|
<symbol id="i-dark-mode" viewBox="0 -960 960 960"><path d="M480-120q-151 0-255.5-104.5T120-480q0-138 90-239.5T440-838q13-2 23 3.5t16 14.5q6 9 6.5 21t-7.5 23q-17 26-25.5 55t-8.5 61q0 90 63 153t153 63q31 0 61.5-9t54.5-25q11-7 22.5-6.5T819-479q10 5 15.5 15t3.5 24q-14 138-117.5 229T480-120Zm0-80q88 0 158-48.5T740-375q-20 5-40 8t-40 3q-123 0-209.5-86.5T364-660q0-20 3-40t8-40q-78 32-126.5 102T200-480q0 116 82 198t198 82Zm-10-270Z"/></symbol>
|
||||||
|
<symbol id="i-brightness-auto" viewBox="0 -960 960 960"><path d="M408-412h146l25 73q3 8 10.5 13.5T606-320q15 0 23.5-12.5T633-359L519-661q-3-9-11-14t-17-5h-22q-9 0-17 5t-11 14L327-360q-5 14 3.5 27t24.5 13q10 0 17.5-5.5T383-340l25-72Zm18-52 52-150h4l52 150H426Zm-80 304H240q-33 0-56.5-23.5T160-240v-106l-77-78q-11-12-17-26.5T60-480q0-15 6-29.5T83-536l77-78v-106q0-33 23.5-56.5T240-800h106l78-77q12-11 26.5-17t29.5-6q15 0 29.5 6t26.5 17l78 77h106q33 0 56.5 23.5T800-720v106l77 78q11 12 17 26.5t6 29.5q0 15-6 29.5T877-424l-77 78v106q0 33-23.5 56.5T720-160H614l-78 77q-12 11-26.5 17T480-60q-15 0-29.5-6T424-83l-78-77Zm34-80 100 100 100-100h140v-140l100-100-100-100v-140H580L480-820 380-720H240v140L140-480l100 100v140h140Zm100-240Z"/></symbol>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<header class="nav">
|
<header class="nav">
|
||||||
<div class="container nav__bar">
|
<div class="container nav__bar">
|
||||||
<a class="brand" href="index.html"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2Z" stroke="currentColor" stroke-width="1.5"/><path d="M14 2v5a1 1 0 0 0 1 1h5" stroke="currentColor" stroke-width="1.5"/><path d="M12 17v-6m0 0-2.5 2.5M12 11l2.5 2.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg><span>SealShare</span></a>
|
<a class="brand" href="index.html"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2Z" stroke="currentColor" stroke-width="1.5"/><path d="M14 2v5a1 1 0 0 0 1 1h5" stroke="currentColor" stroke-width="1.5"/><path d="M12 17v-6m0 0-2.5 2.5M12 11l2.5 2.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg><span>SealShare</span></a>
|
||||||
<button class="nav__toggle" id="navToggle" type="button" aria-expanded="false" aria-controls="navLinks" aria-label="Menu">
|
<button class="icon-btn nav__toggle" id="navToggle" type="button" aria-expanded="false" aria-controls="navLinks" aria-label="Menu">
|
||||||
<svg class="icon icon-menu" aria-hidden="true"><use href="#i-menu"/></svg>
|
<svg class="icon icon-menu" aria-hidden="true"><use href="#i-menu"/></svg>
|
||||||
<svg class="icon icon-close" aria-hidden="true"><use href="#i-close"/></svg>
|
<svg class="icon icon-close" aria-hidden="true"><use href="#i-close"/></svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -45,6 +125,11 @@
|
|||||||
<li><a class="btn btn--filled" href="https://gitea.nonameweb.ch/noNameWEB/SealShare"><svg class="icon" aria-hidden="true"><use href="#i-open-in-new"/></svg>Gitea</a></li>
|
<li><a class="btn btn--filled" href="https://gitea.nonameweb.ch/noNameWEB/SealShare"><svg class="icon" aria-hidden="true"><use href="#i-open-in-new"/></svg>Gitea</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
<button class="icon-btn theme-toggle" id="themeToggle" type="button" aria-label="Theme: System">
|
||||||
|
<svg class="icon icon-light-mode" aria-hidden="true"><use href="#i-light-mode"/></svg>
|
||||||
|
<svg class="icon icon-dark-mode" aria-hidden="true"><use href="#i-dark-mode"/></svg>
|
||||||
|
<svg class="icon icon-brightness-auto" aria-hidden="true"><use href="#i-brightness-auto"/></svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -118,6 +203,24 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// The top app bar takes surface-container once the page scrolls under it.
|
||||||
|
const nav = document.querySelector('.nav');
|
||||||
|
const markScrolled = () => nav.toggleAttribute('data-scrolled', window.scrollY > 0);
|
||||||
|
|
||||||
|
window.addEventListener('scroll', markScrolled, { passive: true });
|
||||||
|
markScrolled();
|
||||||
|
|
||||||
|
// Theme: Light, then Dark, then System (the <head> script applies and remembers it).
|
||||||
|
const themeToggle = document.getElementById('themeToggle');
|
||||||
|
const themeNames = { light: 'Light', dark: 'Dark', system: 'System' };
|
||||||
|
const nextTheme = { light: 'dark', dark: 'system', system: 'light' };
|
||||||
|
const nameThemeToggle = () => themeToggle.setAttribute('aria-label', 'Theme: ' + themeNames[sealshareTheme.choice]);
|
||||||
|
|
||||||
|
themeToggle.addEventListener('click', () => sealshareTheme.set(nextTheme[sealshareTheme.choice]));
|
||||||
|
document.addEventListener('sealshare:theme', nameThemeToggle);
|
||||||
|
nameThemeToggle();
|
||||||
|
|
||||||
|
// Phone menu.
|
||||||
const navToggle = document.getElementById('navToggle');
|
const navToggle = document.getElementById('navToggle');
|
||||||
const navLinks = document.getElementById('navLinks');
|
const navLinks = document.getElementById('navLinks');
|
||||||
|
|
||||||
|
|||||||