add about page with features and competitor comparison.
Includes section nav, flow diagrams with lightbox, comparison table, brand theme tokens, and router hash scroll.
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
@@ -1,7 +1,5 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
@@ -28,43 +26,23 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
--background: oklch(0.99 0.002 260);
|
--background: oklch(1 0 0);
|
||||||
--foreground: oklch(0.2 0.02 260);
|
--foreground: oklch(0.244 0.012 263);
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0);
|
||||||
--card-foreground: oklch(0.2 0.02 260);
|
--card-foreground: oklch(0.244 0.012 263);
|
||||||
--primary: oklch(0.42 0.08 260);
|
--primary: oklch(0.497 0.227 263);
|
||||||
--primary-foreground: oklch(0.99 0.002 260);
|
--primary-foreground: oklch(1 0 0);
|
||||||
--secondary: oklch(0.96 0.01 260);
|
--secondary: oklch(0.94 0.025 263);
|
||||||
--secondary-foreground: oklch(0.28 0.03 260);
|
--secondary-foreground: oklch(0.28 0.04 263);
|
||||||
--muted: oklch(0.96 0.008 260);
|
--muted: oklch(0.955 0.018 263);
|
||||||
--muted-foreground: oklch(0.48 0.02 260);
|
--muted-foreground: oklch(0.46 0.04 263);
|
||||||
--accent: oklch(0.94 0.02 260);
|
--accent: oklch(0.93 0.04 263);
|
||||||
--accent-foreground: oklch(0.28 0.03 260);
|
--accent-foreground: oklch(0.28 0.04 263);
|
||||||
--destructive: oklch(0.55 0.2 25);
|
--destructive: oklch(0.55 0.2 25);
|
||||||
--success: oklch(0.45 0.14 155);
|
--success: oklch(0.45 0.14 155);
|
||||||
--border: oklch(0.9 0.01 260);
|
--border: oklch(0.88 0.025 263);
|
||||||
--input: oklch(0.9 0.01 260);
|
--input: oklch(0.88 0.025 263);
|
||||||
--ring: oklch(0.42 0.08 260);
|
--ring: oklch(0.497 0.227 263);
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
--background: oklch(0.16 0.02 260);
|
|
||||||
--foreground: oklch(0.96 0.01 260);
|
|
||||||
--card: oklch(0.2 0.02 260);
|
|
||||||
--card-foreground: oklch(0.96 0.01 260);
|
|
||||||
--primary: oklch(0.72 0.1 260);
|
|
||||||
--primary-foreground: oklch(0.18 0.02 260);
|
|
||||||
--secondary: oklch(0.24 0.02 260);
|
|
||||||
--secondary-foreground: oklch(0.96 0.01 260);
|
|
||||||
--muted: oklch(0.24 0.02 260);
|
|
||||||
--muted-foreground: oklch(0.68 0.02 260);
|
|
||||||
--accent: oklch(0.28 0.03 260);
|
|
||||||
--accent-foreground: oklch(0.96 0.01 260);
|
|
||||||
--destructive: oklch(0.62 0.2 25);
|
|
||||||
--success: oklch(0.68 0.14 155);
|
|
||||||
--border: oklch(0.32 0.02 260);
|
|
||||||
--input: oklch(0.32 0.02 260);
|
|
||||||
--ring: oklch(0.72 0.1 260);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
@@ -76,3 +54,67 @@
|
|||||||
@apply bg-background text-foreground antialiased;
|
@apply bg-background text-foreground antialiased;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body:has(.lightbox:target) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-image__link {
|
||||||
|
color: inherit;
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 100;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox:target {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox__backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgb(0 0 0 / 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox__img {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
max-width: min(95vw, 75rem);
|
||||||
|
max-height: 90vh;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox__close {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--foreground);
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox__close:hover {
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+4
@@ -11,6 +11,10 @@ export {}
|
|||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
AboutComparisonTable: typeof import('./components/about/AboutComparisonTable.vue')['default']
|
||||||
|
AboutSectionNav: typeof import('./components/about/AboutSectionNav.vue')['default']
|
||||||
|
AppHeader: typeof import('./components/layout/AppHeader.vue')['default']
|
||||||
|
ExpandableImage: typeof import('./components/ExpandableImage.vue')['default']
|
||||||
InlineTabList: typeof import('./components/InlineTabList.vue')['default']
|
InlineTabList: typeof import('./components/InlineTabList.vue')['default']
|
||||||
MetadataInspector: typeof import('./components/upload/MetadataInspector.vue')['default']
|
MetadataInspector: typeof import('./components/upload/MetadataInspector.vue')['default']
|
||||||
MetadataOutputPanel: typeof import('./components/upload/MetadataOutputPanel.vue')['default']
|
MetadataOutputPanel: typeof import('./components/upload/MetadataOutputPanel.vue')['default']
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div class="expandable-image">
|
||||||
|
<figure class="m-0">
|
||||||
|
<a
|
||||||
|
:href="`#${lightboxId}`"
|
||||||
|
class="expandable-image__link block"
|
||||||
|
:aria-label="expandLabel"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="src"
|
||||||
|
:alt="alt"
|
||||||
|
class="w-full rounded-lg border border-border transition-colors hover:border-primary/50"
|
||||||
|
:width="width"
|
||||||
|
:height="height"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<div
|
||||||
|
:id="lightboxId"
|
||||||
|
class="lightbox"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
:aria-label="expandLabel"
|
||||||
|
>
|
||||||
|
<a class="lightbox__backdrop" :href="closeHash" aria-label="Close" />
|
||||||
|
<img
|
||||||
|
class="lightbox__img"
|
||||||
|
:src="src"
|
||||||
|
:alt="alt"
|
||||||
|
:width="width"
|
||||||
|
:height="height"
|
||||||
|
/>
|
||||||
|
<a class="lightbox__close" :href="closeHash" aria-label="Close">×</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps({
|
||||||
|
src: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
alt: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
lightboxId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
closeHash: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
expandLabel: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<template>
|
||||||
|
<div class="overflow-x-auto rounded-lg border border-border">
|
||||||
|
<table class="w-full min-w-160 border-collapse text-left text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-border bg-muted/40">
|
||||||
|
<th class="sticky left-0 z-10 min-w-48 bg-muted/40 px-3 py-2.5 font-medium text-foreground">
|
||||||
|
Feature
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
v-for="host in comparisonHosts"
|
||||||
|
:key="host.id"
|
||||||
|
class="min-w-38 px-3 py-2.5 font-medium"
|
||||||
|
:class="host.id === 'privateImage' ? 'bg-primary/5 text-foreground' : 'text-muted-foreground'"
|
||||||
|
>
|
||||||
|
{{ host.name }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-muted-foreground">
|
||||||
|
<tr v-for="row in comparisonRows" :key="row.feature" class="border-b border-border last:border-0">
|
||||||
|
<td
|
||||||
|
class="sticky left-0 z-10 bg-card px-3 py-2.5 font-medium text-foreground shadow-[1px_0_0_0_var(--border)]"
|
||||||
|
>
|
||||||
|
{{ row.feature }}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
v-for="host in comparisonHosts"
|
||||||
|
:key="host.id"
|
||||||
|
class="px-3 py-2.5 text-center align-middle"
|
||||||
|
:class="host.id === 'privateImage' ? 'bg-primary/3' : ''"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center justify-center"
|
||||||
|
:title="row.values[host.id] ? 'Yes' : 'No'"
|
||||||
|
>
|
||||||
|
<span class="sr-only">{{ row.values[host.id] ? 'Yes' : 'No' }}</span>
|
||||||
|
<span
|
||||||
|
v-if="row.values[host.id]"
|
||||||
|
class="inline-flex size-6 shrink-0 items-center justify-center rounded-full bg-success text-white shadow-sm"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<svg class="size-3.5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M16.704 4.153a.75.75 0 0 1 .143 1.052l-8 10.5a.75.75 0 0 1-1.127.075l-4.5-4.5a.75.75 0 0 1 1.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 0 1 1.05-.143Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="inline-flex size-6 shrink-0 items-center justify-center rounded-full bg-destructive text-white shadow-sm"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<svg class="size-3.5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ComparisonHost, ComparisonRow } from '@/types/about/ComparisonTable';
|
||||||
|
|
||||||
|
const comparisonHosts: ComparisonHost[] = [
|
||||||
|
{ id: 'privateImage', name: 'Private Image' },
|
||||||
|
{ id: 'imgbb', name: 'ImgBB' },
|
||||||
|
{ id: 'imgur', name: 'Imgur' },
|
||||||
|
{ id: 'catbox', name: 'Catbox.moe' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const comparisonRows: ComparisonRow[] = [
|
||||||
|
{
|
||||||
|
feature: 'Metadata removed in your browser before upload',
|
||||||
|
values: { privateImage: true, imgbb: false, imgur: false, catbox: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: 'Metadata inspector before upload',
|
||||||
|
values: { privateImage: true, imgbb: false, imgur: false, catbox: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: 'Image encrypted in your browser before upload',
|
||||||
|
values: { privateImage: true, imgbb: false, imgur: false, catbox: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: 'Zero knowledge encryption',
|
||||||
|
values: { privateImage: true, imgbb: false, imgur: false, catbox: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: 'Hotlink mode with encryption at rest',
|
||||||
|
values: { privateImage: true, imgbb: false, imgur: false, catbox: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: 'Embeddable image URLs for forums and websites',
|
||||||
|
values: { privateImage: true, imgbb: true, imgur: true, catbox: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: 'Upload expiry or auto-delete timer',
|
||||||
|
values: { privateImage: true, imgbb: true, imgur: false, catbox: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: 'Max views and burn-after-read',
|
||||||
|
values: { privateImage: true, imgbb: false, imgur: false, catbox: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: 'Delete link with each upload (no account)',
|
||||||
|
values: { privateImage: true, imgbb: true, imgur: false, catbox: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: 'Self-hostable and open source',
|
||||||
|
values: { privateImage: true, imgbb: false, imgur: false, catbox: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: 'Tor onion service and dual clearnet + onion links',
|
||||||
|
values: { privateImage: true, imgbb: false, imgur: false, catbox: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: 'No account required to upload',
|
||||||
|
values: { privateImage: true, imgbb: true, imgur: true, catbox: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: 'No third-party ads',
|
||||||
|
values: { privateImage: true, imgbb: false, imgur: false, catbox: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: 'No content scanning',
|
||||||
|
values: { privateImage: true, imgbb: true, imgur: false, catbox: true }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<nav
|
||||||
|
class="mt-5 flex flex-wrap gap-x-4 gap-y-2 border-b border-border pb-4 text-sm text-muted-foreground"
|
||||||
|
aria-label="On this page"
|
||||||
|
>
|
||||||
|
<RouterLink
|
||||||
|
v-for="item in aboutPageNavSections"
|
||||||
|
:key="item.id"
|
||||||
|
:to="{ name: ROUTE_NAMES.About, hash: `#${item.id}` }"
|
||||||
|
class="text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</RouterLink>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ROUTE_NAMES } from '@/consts/routeNames';
|
||||||
|
|
||||||
|
const aboutPageNavSections = [
|
||||||
|
{ id: 'exiftool', label: 'ExifTool' },
|
||||||
|
{ id: 'share-modes', label: 'Share modes' },
|
||||||
|
{ id: 'crypto-shred', label: 'Crypto-shredding' },
|
||||||
|
{ id: 'limits', label: 'Limits' },
|
||||||
|
{ id: 'self-hosting', label: 'Self-hosting' },
|
||||||
|
{ id: 'clearnet-and-onion', label: 'Clearnet & onion' },
|
||||||
|
{ id: 'compare', label: 'Compare hosts' }
|
||||||
|
];
|
||||||
|
</script>
|
||||||
@@ -3,6 +3,17 @@ import { ROUTE_NAMES } from '@/consts/routeNames';
|
|||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
scrollBehavior(to, _from, savedPosition) {
|
||||||
|
if (savedPosition) {
|
||||||
|
return savedPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (to.hash) {
|
||||||
|
return { el: to.hash, behavior: 'smooth' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { top: 0 };
|
||||||
|
},
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -10,6 +21,12 @@ const router = createRouter({
|
|||||||
component: () => import('@/views/UploadView.vue'),
|
component: () => import('@/views/UploadView.vue'),
|
||||||
meta: { title: 'Private Image - Upload' }
|
meta: { title: 'Private Image - Upload' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/about',
|
||||||
|
name: ROUTE_NAMES.About,
|
||||||
|
component: () => import('@/views/AboutView.vue'),
|
||||||
|
meta: { title: 'Private Image - About' }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/v/:id',
|
path: '/v/:id',
|
||||||
name: ROUTE_NAMES.Viewer,
|
name: ROUTE_NAMES.Viewer,
|
||||||
@@ -31,4 +48,10 @@ const router = createRouter({
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.afterEach(to => {
|
||||||
|
const title = to.meta.title;
|
||||||
|
|
||||||
|
document.title = typeof title === 'string' ? title : 'Private Image';
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export type ComparisonHostId = 'privateImage' | 'imgbb' | 'imgur' | 'catbox';
|
||||||
|
|
||||||
|
export type ComparisonHost = {
|
||||||
|
id: ComparisonHostId;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ComparisonRow = {
|
||||||
|
feature: string;
|
||||||
|
values: Record<ComparisonHostId, boolean>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
<template>
|
||||||
|
<main class="mx-auto max-w-5xl flex-1 px-4 py-8 sm:px-6">
|
||||||
|
<article class="max-w-3xl">
|
||||||
|
<h1 class="text-2xl font-semibold tracking-tight sm:text-3xl">About Private Image</h1>
|
||||||
|
|
||||||
|
<AboutSectionNav />
|
||||||
|
|
||||||
|
<p class="mt-6 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
Private Image is an image-hosting service built with privacy in mind. Many image hosts store uploaded
|
||||||
|
files as they are, including EXIF and other metadata that can reveal where a photo was taken, which
|
||||||
|
device was used, and when it was captured. The service operator may also be able to open and view the
|
||||||
|
original image.
|
||||||
|
</p>
|
||||||
|
<p class="mt-4 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
Private Image takes a different approach. It removes metadata using
|
||||||
|
<a
|
||||||
|
href="#exiftool"
|
||||||
|
class="font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
||||||
|
>
|
||||||
|
ExifTool
|
||||||
|
</a>
|
||||||
|
compiled to run directly in your browser, then encrypts the cleaned image before uploading it. The
|
||||||
|
server receives encrypted data along with your chosen expiry and view limits, but not the original image
|
||||||
|
in readable form.
|
||||||
|
</p>
|
||||||
|
<p class="mt-4 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
Each upload comes with links to view and delete the image. There is no account registration, and your
|
||||||
|
upload history isn't retained after you close the page.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<section id="exiftool" class="mt-14 max-w-3xl scroll-mt-20">
|
||||||
|
<h2 class="text-lg font-medium text-foreground">ExifTool runs in your browser</h2>
|
||||||
|
<p class="mt-3 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
Private Image uses ExifTool, a widely used metadata tool normally run on a desktop, compiled to work in
|
||||||
|
your browser. It processes images locally rather than sending them to the server for inspection.
|
||||||
|
</p>
|
||||||
|
<p class="mt-4 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
When you add an image, ExifTool reads its embedded metadata, including EXIF data, and removes it before
|
||||||
|
upload. You can inspect the metadata before proceeding. The image that's encrypted and uploaded is the
|
||||||
|
cleaned version, without embedded location data, device details, and similar information.
|
||||||
|
</p>
|
||||||
|
<p class="mt-4 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
All of this happens in your browser, before the image reaches the server.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="share-modes" class="mt-14 max-w-3xl scroll-mt-20">
|
||||||
|
<h2 class="text-lg font-medium text-foreground">Share modes</h2>
|
||||||
|
|
||||||
|
<h3 id="zero-knowledge" class="mt-6 scroll-mt-20 text-base font-medium text-foreground">
|
||||||
|
Zero knowledge
|
||||||
|
</h3>
|
||||||
|
<p class="mt-2 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
Images are encrypted end to end (E2E), so the server cannot decrypt or view them. Anyone with the
|
||||||
|
viewing link can access the image, unless you've also protected the upload with a password.
|
||||||
|
</p>
|
||||||
|
<p class="mt-4 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
This mode is best for sensitive images, private messages, and sharing with a small group when you don't
|
||||||
|
want the server operator to be able to read the file.
|
||||||
|
</p>
|
||||||
|
<p class="mt-4 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
Zero-knowledge links aren't compatible with BBCode, HTML, or similar image embeds.
|
||||||
|
</p>
|
||||||
|
<ExpandableImage
|
||||||
|
class="mt-6"
|
||||||
|
src="/about/zk-flow.jpg"
|
||||||
|
alt="Zero knowledge upload flow: select image, remove metadata with ExifTool, encrypt in browser, upload ciphertext, store encrypted on server, recipient decrypts locally"
|
||||||
|
:width="1024"
|
||||||
|
:height="471"
|
||||||
|
lightbox-id="lightbox-zk-flow"
|
||||||
|
close-hash="#zero-knowledge"
|
||||||
|
expand-label="View larger zero knowledge flow diagram"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h3 id="hotlink" class="mt-8 scroll-mt-20 text-base font-medium text-foreground">Hotlink</h3>
|
||||||
|
<p class="mt-2 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
Hotlink mode keeps images encrypted at rest but decrypts them on the server when they're requested. This
|
||||||
|
makes it possible to embed images directly in forum posts, chats, wikis, and other places that support
|
||||||
|
BBCode, HTML, or similar formats.
|
||||||
|
</p>
|
||||||
|
<p class="mt-4 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
Metadata is removed in the browser using the same
|
||||||
|
<a
|
||||||
|
href="#exiftool"
|
||||||
|
class="font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
||||||
|
>
|
||||||
|
ExifTool
|
||||||
|
</a>
|
||||||
|
process as in zero-knowledge mode. Since hotlinked images are intended to be publicly accessible,
|
||||||
|
encryption at rest mainly provides an additional layer of protection through
|
||||||
|
<a
|
||||||
|
href="#crypto-shred"
|
||||||
|
class="font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
||||||
|
>
|
||||||
|
crypto-shredding
|
||||||
|
</a>
|
||||||
|
when an image is deleted or expires.
|
||||||
|
</p>
|
||||||
|
<ExpandableImage
|
||||||
|
class="mt-6"
|
||||||
|
src="/about/hotlink-flow.jpg"
|
||||||
|
alt="Hotlink upload flow: select image, remove metadata with ExifTool, encrypt in browser, upload, store encrypted at rest, server decrypts on request for display or embedding"
|
||||||
|
:width="1024"
|
||||||
|
:height="527"
|
||||||
|
lightbox-id="lightbox-hotlink-flow"
|
||||||
|
close-hash="#hotlink"
|
||||||
|
expand-label="View larger hotlink flow diagram"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="crypto-shred" class="mt-14 max-w-3xl scroll-mt-20">
|
||||||
|
<h2 class="text-lg font-medium text-foreground">Crypto-shredding</h2>
|
||||||
|
<p class="mt-3 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
Deleting a file doesn't always mean every copy of its data has disappeared from a storage device.
|
||||||
|
Backups, snapshots, and the way modern drives manage storage can leave behind fragments that may remain
|
||||||
|
recoverable for some time.
|
||||||
|
</p>
|
||||||
|
<p class="mt-4 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
Crypto-shredding addresses this by storing data as encrypted blobs. Even if someone recovers leftover
|
||||||
|
data from a drive, they can't read it without the decryption key.
|
||||||
|
</p>
|
||||||
|
<p class="mt-4 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
Private Image stores uploads as encrypted data and keeps the information needed to view an image in the
|
||||||
|
sharing link rather than saving the key separately on the server. When you delete an upload or it
|
||||||
|
expires, the encrypted blob is removed and the link stops working.
|
||||||
|
</p>
|
||||||
|
<p class="mt-4 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
This means that any recovered disk fragments remain unreadable without the link and its decryption
|
||||||
|
information. It's an important safeguard for hotlinked images and adds another layer of protection to
|
||||||
|
zero-knowledge uploads, which the server cannot decrypt in the first place.
|
||||||
|
</p>
|
||||||
|
<p class="mt-4 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
Deleting an image or letting it expire ends practical access to it. It doesn't guarantee that every byte
|
||||||
|
has been physically erased from every drive, but crypto-shredding ensures that any leftover encrypted
|
||||||
|
data is useless without the key.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="limits" class="mt-14 max-w-3xl scroll-mt-20">
|
||||||
|
<h2 class="text-lg font-medium text-foreground">Set your own limits</h2>
|
||||||
|
<p class="mt-3 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
Choose how long each upload stays available, from a short, fixed period to no expiry. You can also limit
|
||||||
|
the number of times a link can be used, including setting it to allow just one view before it burns.
|
||||||
|
</p>
|
||||||
|
<p class="mt-4 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
Each upload also has a delete link, so you can remove the image immediately whenever you want.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="self-hosting" class="mt-14 max-w-3xl scroll-mt-20">
|
||||||
|
<h2 class="text-lg font-medium text-foreground">Self-hosting</h2>
|
||||||
|
<p class="mt-3 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
Private Image is open source, so you can run your own instance on hardware you control instead of
|
||||||
|
relying on a public service. The upload experience works the same way: ExifTool, encryption, and the
|
||||||
|
different sharing modes all run in the browser.
|
||||||
|
</p>
|
||||||
|
<p class="mt-4 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
The production setup uses Docker Compose with the application, database, Nginx, and Tor. You'll need to
|
||||||
|
configure your domain, TLS, and environment variables using the deployment guide in the project's
|
||||||
|
repository.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="clearnet-and-onion" class="mt-14 max-w-3xl scroll-mt-20">
|
||||||
|
<h2 class="text-lg font-medium text-foreground">Clearnet and onion access</h2>
|
||||||
|
<p class="mt-3 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
Private Image is available over the regular web (clearnet, via HTTPS) and through Tor as an onion
|
||||||
|
service. You can upload images and open sharing links over either network, with the same encryption and
|
||||||
|
sharing options available on both.
|
||||||
|
</p>
|
||||||
|
<p class="mt-4 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
After uploading, you'll get separate clearnet and onion links for viewing and deleting the image, as
|
||||||
|
well as direct image URLs in hotlink mode. Choose the set that matches how you and your recipients
|
||||||
|
access the service.
|
||||||
|
</p>
|
||||||
|
<p class="mt-4 text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
Forum embed codes, including BBCode and HTML, use clearnet URLs because most external sites expect
|
||||||
|
standard web addresses.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="compare" class="mt-16 max-w-5xl scroll-mt-20">
|
||||||
|
<h2 class="text-lg font-medium text-foreground">How other image hosts compare</h2>
|
||||||
|
<p class="mt-3 max-w-3xl text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
The table below compares Private Image with ImgBB, Imgur, and Catbox, three free image hosts commonly
|
||||||
|
used for forum posts and quick sharing.
|
||||||
|
</p>
|
||||||
|
<p class="mt-4 max-w-3xl text-sm leading-relaxed text-muted-foreground sm:text-base sm:leading-relaxed">
|
||||||
|
Features and privacy policies can change, so check each provider's current documentation before relying
|
||||||
|
on any particular capability.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<AboutComparisonTable class="mt-6" />
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user