Fork pd_web, remove everything we don't need

This commit is contained in:
2025-09-24 15:37:57 +02:00
parent 9dcdd94b3a
commit fd5cd0bfd1
415 changed files with 146269 additions and 120786 deletions

282
svelte/package-lock.json generated
View File

@@ -11,6 +11,7 @@
"behave-js": "^1.5.0",
"chart.js": "^4.4.6",
"country-data-list": "^1.4.0",
"pdfjs-dist": "^5.4.149",
"pure-color": "^1.3.0",
"rollup-plugin-includepaths": "^0.2.4",
"svelte-preprocess": "^6.0.3",
@@ -45,15 +46,15 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.25.9",
"@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
"picocolors": "^1.0.0"
"picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
@@ -352,9 +353,9 @@
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"devOptional": true,
"license": "MIT",
"engines": {
@@ -362,9 +363,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"devOptional": true,
"license": "MIT",
"engines": {
@@ -397,27 +398,27 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
"integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz",
"integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.25.9",
"@babel/types": "^7.26.0"
"@babel/template": "^7.27.1",
"@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz",
"integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==",
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz",
"integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.26.0"
"@babel/types": "^7.27.1"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -1511,28 +1512,25 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
"dev": true,
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.25.9",
"@babel/parser": "^7.25.9",
"@babel/types": "^7.25.9"
"@babel/code-frame": "^7.27.1",
"@babel/parser": "^7.27.2",
"@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1558,14 +1556,14 @@
}
},
"node_modules/@babel/types": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz",
"integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz",
"integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.25.9"
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1654,6 +1652,191 @@
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==",
"license": "MIT"
},
"node_modules/@napi-rs/canvas": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz",
"integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==",
"license": "MIT",
"optional": true,
"workspaces": [
"e2e/*"
],
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@napi-rs/canvas-android-arm64": "0.1.80",
"@napi-rs/canvas-darwin-arm64": "0.1.80",
"@napi-rs/canvas-darwin-x64": "0.1.80",
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80",
"@napi-rs/canvas-linux-arm64-gnu": "0.1.80",
"@napi-rs/canvas-linux-arm64-musl": "0.1.80",
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.80",
"@napi-rs/canvas-linux-x64-gnu": "0.1.80",
"@napi-rs/canvas-linux-x64-musl": "0.1.80",
"@napi-rs/canvas-win32-x64-msvc": "0.1.80"
}
},
"node_modules/@napi-rs/canvas-android-arm64": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz",
"integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-darwin-arm64": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz",
"integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-darwin-x64": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz",
"integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz",
"integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz",
"integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz",
"integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz",
"integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz",
"integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-x64-musl": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz",
"integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz",
"integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -2444,9 +2627,9 @@
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2983,6 +3166,18 @@
"dev": true,
"license": "ISC"
},
"node_modules/pdfjs-dist": {
"version": "5.4.149",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.149.tgz",
"integrity": "sha512-Xe8/1FMJEQPUVSti25AlDpwpUm2QAVmNOpFP0SIahaPIOKBKICaefbzogLdwey3XGGoaP4Lb9wqiw2e9Jqp0LA==",
"license": "Apache-2.0",
"engines": {
"node": ">=20.16.0 || >=22.3.0"
},
"optionalDependencies": {
"@napi-rs/canvas": "^0.1.77"
}
},
"node_modules/periscopic": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
@@ -3094,13 +3289,6 @@
"node": ">=4"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"dev": true,
"license": "MIT"
},
"node_modules/regenerator-transform": {
"version": "0.15.2",
"resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz",

View File

@@ -11,14 +11,11 @@ const production = !process.env.ROLLUP_WATCH;
const builddir = "../res/static/svelte"
export default [
"file_viewer",
"filesystem",
"user_home",
"admin_panel",
"home_page",
"text_upload",
"speedtest",
"upload_history",
"login",
].map((name, index) => ({
input: `src/${name}.js`,

View File

@@ -1,8 +0,0 @@
import App from './file_viewer/FileViewer.svelte';
const app = new App({
target: document.getElementById("body"),
props: {}
});
export default app;

View File

@@ -1,129 +0,0 @@
<script>
import { onMount } from "svelte"
let ad_type = ""
onMount(() => {
// 40% pixeldrain socials
// 10% reviews
// 50% patreon
// let rand = Math.random()
// if (rand < 0.4) {
// ad_type = "socials"
// } else if (rand < 0.5) {
// ad_type = "reviews"
// } else {
// ad_type = "patreon_support"
// }
})
</script>
{#if ad_type === "patreon_support"}
<div class="banner support_banner">
<span style="display: block; margin-bottom: 2px;">
No ads today. Pixeldrain is currently funded by our subscribers!
</span>
<a href="/#pro" rel="noreferrer" class="button button_highlight" target="_blank">
<i class="icon">bolt</i>
Support Pixeldrain to help keep the project going
</a>
</div>
{:else if ad_type === "socials"}
<div class="banner center">
<div class="socials">
Pixeldrain on social media<br/>
<a href="https://mastodon.social/@fornax" rel="noreferrer" class="button" target="_blank" style="background-color: #595aff; color: #ffffff;">
<i class="icon small">people</i>
Mastodon
</a>
<a href="https://bsky.app/profile/fornax.bsky.social"
rel="noreferrer" class="button" target="_blank" style="background-color: #208bfe; color: #ffffff;"
>
<i class="icon small">people</i>
Bluesky
</a>
<a href="https://discord.gg/TWKGvYAFvX" rel="noreferrer" class="button" target="_blank" style="background-color: #5b5eee; color: #ffffff;">
<i class="icon small">people</i>
Discord
</a>
<a href="https://www.reddit.com/r/PixelDrain"
rel="noreferrer" class="button" target="_blank" style="background-color: #ff4500; color: #ffffff;"
>
<i class="icon small">people</i>
Reddit
</a>
</div>
</div>
{:else if ad_type === "reviews"}
<div class="banner support_banner">
<span style="display: block; margin-bottom: 2px;">
Are you liking pixeldrain? Write a review! It really helps
</span>
<a href="https://alternativeto.net/software/pixeldrain/about/"
rel="noreferrer" class="button" target="_blank" style="background-color: #0C9EF0; color: #FFFFFF; font-weight: bold;"
>
<i class="icon">rate_review</i>
AlternativeTo
</a>
<a href="https://www.trustpilot.com/review/pixeldrain.com"
rel="noreferrer" class="button" target="_blank" style="background-color: #00B67A; color: #FFFFFF; font-weight: bold;"
>
<i class="icon">rate_review</i>
Trustpilot
</a>
</div>
{:else if ad_type === "ad_block"}
<div class="banner support_banner">
<span style="display: block; margin-bottom: 2px;">
Protect your privacy, protect your sanity. Get an ad blocker!
</span>
<a href="https://ublockorigin.com/"
rel="noreferrer" class="button" target="_blank" style="background-color: #800000; color: #FFFFFF; font-weight: bold;"
>
<i class="icon">security</i>
Get uBlock Origin
</a>
</div>
{/if}
<style>
.banner {
display: block;
margin: auto;
transform-origin: 0 0;
font-size: 1.2em;
line-height: 1.2em;
}
.center {
text-align: center;
}
.support_banner {
text-align: center;
padding: 2px;
}
.socials {
display: inline-block;
text-align: center;
margin: 0 5px;
}
/* Try to avoid text wrapping */
@media(max-width: 600px) {
.banner {
font-size: 1em;
}
.socials {
font-size: 0.9em;
}
}
</style>

View File

@@ -1,34 +0,0 @@
<script>
export let src = ""
export let link = ""
export let border_top = false;
</script>
{#if src}
<div class:border_top>
{#if link}
<a href={link} target="_blank" rel="noreferrer">
<img class="image" src="{src}" alt="User-provided banner"/>
</a>
{:else}
<img class="image" src="{src}" alt="User-provided banner"/>
{/if}
</div>
{/if}
<style>
.border_top {
border-top: solid 2px var(--separator);
}
.image {
display: block;
margin: auto;
max-height: 90px;
max-width: 100%;
}
@media(max-height: 600px) {
.image {
max-height: 60px;
}
}
</style>

View File

@@ -1,280 +0,0 @@
<script>
import { onMount } from "svelte";
import { formatDataVolume, formatDate, formatThousands } from "util/Formatting"
import { color_by_name, domain_url } from "util/Util.svelte";
import Chart from "util/Chart.svelte";
export let file = {
id: "",
name: "",
mime_type: "",
date_upload: "",
size: 0,
downloads: 0,
bandwidth_used: 0,
bandwidth_used_paid: 0,
description: "",
hash_sha256: "",
timeseries_href: "",
}
$: update_file(file.id)
let update_file = id => {
if (id) {
update_chart(0, 0)
}
}
let chart
let chart_timespan = 0
let chart_interval = 0
let chart_timespans = [
{label: "Day (1m)", span: 1440, interval: 1},
{label: "Week (1h)", span: 10080, interval: 60},
{label: "Month (1h)", span: 43200, interval: 60},
{label: "Quarter (1d)", span: 131400, interval: 1440},
{label: "Year (1d)", span: 525600, interval: 1440},
{label: "Two Years (1d)", span: 1051200, interval: 1440},
{label: "Five Years (1d)", span: 2628000, interval: 1440},
]
let update_chart = (timespan, interval) => {
// If the timespan is 0 we calculate the maximum timespan based on the age
// of the file
if (timespan === 0) {
let minutes_since_upload = (new Date().getTime() - Date.parse(file.date_upload)) / 1000 / 60
for (let i = 0; i < chart_timespans.length; i++) {
timespan = chart_timespans[i].span
interval = chart_timespans[i].interval
if (chart_timespans[i].span > minutes_since_upload) {
break;
}
}
}
chart_timespan = timespan
chart_interval = interval
console.log("updating graph", chart_timespan, chart_interval)
let start = new Date()
start.setMinutes(start.getMinutes() - timespan)
let end = new Date()
fetch(
file.timeseries_href +
"?start=" + start.toISOString() +
"&end=" + end.toISOString() +
"&interval=" + interval
).then(resp => {
if (!resp.ok) { return null }
return resp.json()
}).then(resp => {
resp.views.timestamps.forEach((val, idx) => {
let date = new Date(val);
let str = ("00" + (date.getMonth() + 1)).slice(-2);
str += "-" + ("00" + date.getDate()).slice(-2);
str += " " + ("00" + date.getHours()).slice(-2);
str += ":" + ("00" + date.getMinutes()).slice(-2);
resp.views.timestamps[idx] = " " + str + " "; // Poor man's padding
});
resp.bandwidth.amounts.forEach((val, idx) => {
resp.bandwidth.amounts[idx] = Math.round(val / file.size);
});
resp.bandwidth_paid.amounts.forEach((val, idx) => {
resp.bandwidth.amounts[idx] += Math.round(val / file.size);
});
chart.data().labels = resp.views.timestamps
chart.data().datasets[0].data = resp.views.amounts
chart.data().datasets[1].data = resp.bandwidth.amounts
chart.update()
})
}
let download_info = false
onMount(() => {
chart.data().datasets = [
{
label: "Views",
borderWidth: 2,
pointRadius: 0,
borderColor: color_by_name("highlight_color"),
backgroundColor: color_by_name("highlight_color"),
},
{
label: "Downloads",
borderWidth: 2,
pointRadius: 0,
borderColor: color_by_name("danger_color"),
backgroundColor: color_by_name("danger_color"),
},
];
})
</script>
<div class="indent">
<table>
<tbody>
<tr>
<td>Name</td>
<td>{file.name}</td>
</tr>
<tr>
<td>URL</td>
<td><a href="/u/{file.id}">{domain_url()}/u/{file.id}</a></td>
</tr>
<tr>
<td>Mime Type</td>
<td>{file.mime_type}</td>
</tr>
<tr>
<td>ID</td>
<td>{file.id}</td>
</tr>
<tr>
<td>Size</td>
<td>{formatDataVolume(file.size, 4)} ( {formatThousands(file.size)} B )</td>
</tr>
<tr>
<td>Free bandwidth used</td>
<td>
{formatDataVolume(file.bandwidth_used, 4)}
( {formatThousands(file.bandwidth_used)} B ),
{(file.bandwidth_used/file.size).toFixed(1)}x file size
</td>
</tr>
<tr>
<td>Premium bandwidth used</td>
<td>
{formatDataVolume(file.bandwidth_used_paid, 4)}
( {formatThousands(file.bandwidth_used_paid)} B ),
{(file.bandwidth_used_paid/file.size).toFixed(1)}x file size
</td>
</tr>
<tr>
<td>
Unique downloads
<button class="button small_button round"
class:button_highlight={download_info}
style="margin: 0;"
on:click={() => download_info = !download_info}
>
<i class="icon">help</i>
</button>
</td>
<td>{formatThousands(file.downloads)}</td>
</tr>
{#if download_info}
<tr>
<td colspan="2">
The unique download counter only counts downloads once per IP
address. So this number shows how many individual people have
attempted to download the file. The download counter on the
toolbar on the other hand shows how many real downloads the file
has had. Real downloads are counted by dividing the total
bandwidth usage by the size of the file.
</td>
</tr>
{/if}
<tr>
<td>Upload Date</td>
<td>{formatDate(file.date_upload, true, true, true)}</td>
</tr>
{#if file.description}
<tr>
<td>Description</td>
<td>{file.description}</td>
</tr>
{/if}
<tr>
<td>SHA256 hash</td>
<td style="word-break: break-all;">{file.hash_sha256}</td>
</tr>
</tbody>
</table>
<h2>Views and downloads</h2>
<div class="button_bar">
{#each chart_timespans as ts}
<button
on:click={() => { update_chart(ts.span, ts.interval) }}
class:button_highlight={chart_timespan == ts.span}>
{ts.label}
</button>
{/each}
</div>
<Chart bind:this={chart} />
<p style="text-align: center">
Charts rendered by the amazing <a href="https://www.chartjs.org/" target="_blank" rel="noreferrer">Chart.js</a>.
</p>
<h3>Keyboard Controls</h3>
<h4>Global</h4>
<div class="shortcuts">
<div><div>c</div><div>Copy page URL</div></div>
<div><div>s</div><div>Download current file</div></div>
<div><div>q</div><div>Close window</div></div>
<div><div>g</div><div>Grab file (copy to your account)</div></div>
<div><div>i</div><div>Show details window</div></div>
<div><div>e</div><div>Show edit window</div></div>
<div><div>r</div><div>Show abuse report window</div></div>
</div>
<h4>List</h4>
<div class="shortcuts">
<div><div>a or &#8592;</div><div>Previous file</div></div>
<div><div>d or &#8594;</div><div>Next file</div></div>
<div><div>shift + s</div><div>Download all files as zip</div></div>
<div><div>u</div><div>Upload files to album</div></div>
</div>
<h4>Video / audio</h4>
<div class="shortcuts">
<div><div>space</div><div>Pause / resume playback</div></div>
<div><div>f</div><div>Toggle fullscreen</div></div>
<div><div>esc</div><div>Exit fullscreen</div></div>
<div><div>m</div><div>Mute / unmute playback</div></div>
<div><div>h</div><div>Skip 20 seconds backward</div></div>
<div><div>j</div><div>Skip 5 seconds backward</div></div>
<div><div>k</div><div>Skip 5 seconds forward</div></div>
<div><div>l</div><div>Skip 20 seconds forward</div></div>
<div><div>,</div><div>Skip 40ms backward</div></div>
<div><div>.</div><div>Skip 40ms forward</div></div>
</div>
</div>
<style>
.button_bar {
display: block;
width: 100%;
text-align: center;
}
.shortcuts {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 5px;
}
.shortcuts > div {
flex: 0 0 10em;
display: flex;
flex-direction: column;
text-align: center;
border-radius: 8px;
overflow: hidden;
border: 2px solid var(--card_color);
}
.shortcuts > div > div:first-child {
font-size: 1.4em;
padding: 4px;
background: var(--card_color);
}
</style>

View File

@@ -1,164 +0,0 @@
<script>
import { tick } from "svelte"
import Modal from "util/Modal.svelte"
export let file = {
id: "",
name: "",
availability: "",
download_href: "",
}
export let list = {
id: "",
title: "",
download_href: "",
}
let load_captcha_script = false
let download_captcha_window
let captcha_type = "" // rate_limit or malware
let captcha_window_title = ""
let captcha_container
let error_window = null
let error_code = ""
let error_message = ""
export const download_file = () => {
if (!window.viewer_data.captcha_key) {
console.debug("Server doesn't support captcha, starting download", file.download_href)
download(file.download_href, file.name)
return
}
if (file.availability === "") {
console.debug("File is available, starting download", file.download_href)
download(file.download_href, file.name)
return
}
if (!file.availability.endsWith("_captcha_required")) {
error_code = file.availability
error_message = file.availability_message
error_window.show()
console.debug("File is unavailable, showing error message")
return
}
console.debug("File is not readily available, showing captcha dialog")
// When the captcha is filled in by the user this function is called. Here
// we trigger the download using the captcha token Google provided us with
let captcha_complete_callback = token => {
// Download the file using the recaptcha token
console.debug("Captcha validation successful, starting download", file.download_href)
download(file.download_href + "&recaptcha_response=" + token, file.name)
download_captcha_window.hide()
}
// Function which will be called when the captcha script is loaded. This
// renders the checkbox in the modal window
window.captcha_script_loaded = async () => {
download_captcha_window.show()
await tick()
grecaptcha.render(captcha_container, {
sitekey: window.viewer_data.captcha_key,
theme: "dark",
callback: captcha_complete_callback,
})
}
if (file.availability === "file_rate_limited_captcha_required") {
captcha_type = "rate_limit"
captcha_window_title = "Rate limiting enabled!"
} else if (file.availability === "virus_detected_captcha_required") {
captcha_type = "malware"
captcha_window_title = "Malware warning!"
} else if (
file.availability === "ip_download_limited_captcha_required" ||
file.availability === "ip_transfer_limited_captcha_required"
) {
captcha_type = "ip_rate_limit"
captcha_window_title = "IP address rate limited"
} else {
captcha_window_title = "CAPTCHA required"
error_code = file.availability
error_message = file.availability_message
}
if (load_captcha_script) {
console.debug("Captcha script is already loaded. Show the modal")
captcha_script_loaded()
} else {
console.debug("Captcha script has not been loaded yet. Embedding now")
load_captcha_script = true
}
}
export const download_list = () => {
if (list.id !== "") {
download(list.download_href, list.title+".zip")
}
}
const download = (href, file_name) => {
let a = document.createElement("a")
a.href = href
a.download = file_name
// You can't call .click() on an element that is not in the DOM. But
// emitting a click event works
a.dispatchEvent(new MouseEvent("click"))
}
</script>
<svelte:head>
{#if load_captcha_script}
<script src="https://www.google.com/recaptcha/api.js?onload=captcha_script_loaded&render=explicit"></script>
{/if}
</svelte:head>
<Modal bind:this={download_captcha_window} title={captcha_window_title} width="500px">
{#if captcha_type === "rate_limit"}
<p class="indent">
This file is using a suspicious amount of bandwidth relative to
its popularity. To continue downloading this file you will have
to prove that you're a human first.
</p>
{:else if captcha_type === "malware"}
<p class="indent">
According to our scanning systems this file may contain a virus.
You can continue downloading this file at your own risk, but you
will have to prove that you're a human first.
</p>
{:else if captcha_type === "ip_rate_limit"}
<p class="indent">
A lot of downloads have originated from this IP address lately.
Please prove that you are not a robot:
</p>
{:else}
<p class="indent">
{error_message}
</p>
<p class="indent">
Reponse code: {error_code}
</p>
{/if}
<br/>
<div bind:this={captcha_container} class="captcha_container"></div>
</Modal>
<Modal bind:this={error_window} title="Download error" width="500px" padding>
<p>
Can't download file: {error_code}
</p>
<p>
{error_message}
</p>
</Modal>
<style>
.captcha_container {
text-align: center;
}
/* global() to silence the unused selector warning */
.captcha_container > :global(div) {
display: inline-block;
}
</style>

View File

@@ -1,202 +0,0 @@
<script>
import { createEventDispatcher } from "svelte";
import Spinner from "util/Spinner.svelte"
let dispatch = createEventDispatcher()
export let file = {
id: "",
name: "",
get_href: "",
can_edit: false,
}
export let list = {
id: "",
title: "",
files: [],
can_edit: false,
info_href: "",
}
let loading = false
let result_success = false
let result_text = ""
let file_name = ""
$: update_file(file.id)
let update_file = () => {
file_name = file.name
}
let list_name = ""
$: update_list(list.id)
let update_list = () => {
list_name = list.title
}
let rename_file = async e => {
e.preventDefault()
loading = true
const form = new FormData()
form.append("action", "rename")
form.append("name", file_name)
try {
const resp = await fetch(file.get_href, { method: "POST", body: form });
if (resp.status >= 400) {
throw (await resp.json()).message
}
result_success = true
result_text = "File name has been changed. Reload the page to see the changes"
} catch (err) {
result_success = false
result_text = "Could not change file name: " + err
} finally {
loading = false
dispatch("reload")
}
}
let delete_file = async e => {
if (!confirm("Are you sure you want to delete '" + file.name + "'?")) {
return
}
loading = true
try {
const resp = await fetch(file.get_href, { method: "DELETE" });
if (resp.status >= 400) {
throw (await resp.json()).message
}
result_success = true
result_text = "This file has been deleted, you can close the page"
} catch (err) {
result_success = false
result_text = "Could not delete file: " + err
} finally {
loading = false
}
}
let rename_list = async e => {
e.preventDefault()
loading = true
let listjson = {
title: list_name,
files: [],
}
list.files.forEach(f => {
listjson.files.push({
id: f.id,
})
})
try {
const resp = await fetch(
list.info_href,
{ method: "PUT", body: JSON.stringify(listjson) },
);
if (resp.status >= 400) {
throw (await resp.json()).message
}
result_success = true
result_text = "Album name has been changed. Reload the page to see the changes"
} catch (err) {
result_success = false
result_text = "Could not change album name: " + err
} finally {
loading = false
dispatch("reload")
}
}
let delete_list = async e => {
if (!confirm("Are you sure you want to delete '" + list.title + "'?")) {
return
}
loading = true
try {
const resp = await fetch(list.info_href, { method: "DELETE" });
if (resp.status >= 400) {
throw (await resp.json()).message
}
result_success = true
result_text = "This album has been deleted, you can close the page"
} catch (err) {
result_success = false
result_text = "Could not delete album: " + err
} finally {
loading = false
}
}
</script>
<div class="indent">
{#if loading}
<div class="spinner_container">
<Spinner></Spinner>
</div>
{/if}
{#if result_text !== ""}
<div class:highlight_green={result_success} class:highligt_red={!result_success}>
{result_text}
</div>
{/if}
{#if list.can_edit}
<h3>Edit album</h3>
Name:<br/>
<form on:submit={rename_list} style="display: flex;">
<input bind:value={list_name} type="text" style="flex: 1 1 auto"/>
<button type="submit" style="flex: 0 0 auto" class="button_highlight">
<i class="icon">save</i> Save
</button>
</form>
<h4>Delete</h4>
<p>
When you delete an album the files in the album will not be deleted,
only the album itself.
</p>
<button on:click={delete_list} class="button_red">
<i class="icon small">delete</i> Delete album
</button>
{/if}
{#if file.can_edit}
<h3>Edit file</h3>
Name:<br/>
<form on:submit={rename_file} style="display: flex;">
<input bind:value={file_name} type="text" style="flex: 1 1 auto"/>
<button type="submit" style="flex: 0 0 auto" class="button_highlight">
<i class="icon">save</i> Save
</button>
</form>
<h4>Delete</h4>
<p>
When you delete a file it cannot be recovered.
Nobody will be able to download it and the link will
stop working. The file will also disappear from any
lists it's contained in.
</p>
<button on:click={delete_file} class="button_red">
<i class="icon small">delete</i> Delete file
</button>
{/if}
</div>
<style>
.spinner_container {
position: absolute;
top: 10px;
left: 10px;
height: 100px;
width: 100px;
}
</style>

View File

@@ -1,193 +0,0 @@
<script>
import CopyButton from "layout/CopyButton.svelte";
import ThemePicker from "util/ThemePicker.svelte";
import { domain_url } from "util/Util.svelte";
import { file_type } from "./FileUtilities.svelte";
export let file = {
id: "",
mime_type: "",
get_href: "",
download_href: "",
}
export let list = {
id: "",
}
let tab = "iframe"
let embed_html = ""
let preview_area
$: update_file(file.id, list.id)
let update_file = () => {
if (preview_area) {
preview_area.innerHTML = ""
}
if (tab === "iframe") {
embed_iframe()
} else if (tab === "hotlink") {
embed_hotlink()
}
}
let style = ""
let set_style = s => {
style = s
embed_iframe()
update_example()
}
let embed_iframe = () => {
tab = "iframe"
let style_part = ""
if (style) {
style_part = "&style="+style
}
let url
if (list.id === "") {
// Not a list, use file ID
url = domain_url()+"/u/"+file.id+"?embed"+style_part
} else {
url = domain_url()+"/l/"+list.id+"?embed"+style_part+window.location.hash
}
embed_html = `<iframe ` +
`src="${url}" ` +
`style="border: none; width: 800px; max-width: 100%; height: 600px; max-height: 100%; border-radius: 8px;" ` +
`allowfullscreen` +
`></iframe>`
}
let embed_hotlink = () => {
tab = "hotlink"
let t = file_type(file)
if (t === "video") {
embed_html = `<video controls playsinline style="max-width: 100%; max-height: 100%;">`+
`<source src="${domain_url()}${file.get_href}" type="${file.mime_type}" />`+
`</video>`
} else if (t === "audio") {
embed_html = `<audio controls style="width: 100%;">`+
`<source src="${domain_url()}${file.get_href}" type="${file.mime_type}" />`+
`</audio>`
} else if (t === "image") {
embed_html = `<img src="${domain_url()}${file.get_href}" alt="${html_escape(file.name)}" style="max-width: 100%; max-height: 100%;">`
} else {
embed_html = `<a href="${domain_url()}${file.download_href}">`+
`Download ${html_escape(file.name)} here`+
`</a>`
}
}
let html_escape = s => {
return s.replace(/&/g, "&amp;").
replace(/</g, "&lt;").
replace(/>/g, "&gt;").
replace(/"/g, "&quot;").
replace(/'/g, "&#039;");
}
let example = false
const toggle_example = () => {
example = !example
update_example()
}
const update_example = () => {
if (example) {
preview_area.innerHTML = embed_html
} else {
preview_area.innerHTML = ""
}
}
</script>
<div class="container">
<div class="indent">
<p>
If you have a website you can embed pixeldrain files in your own
webpages here.
</p>
<p>
The IFrame embed gives you a frame with a slightly more minimalistic
file viewer in it. The embedded file viewer has a fullscreen button
and the toolbar is collapsed by default. If you do not have a
pixeldrain Pro account the frame will also have advertisements in
it.
</p>
<p>
The hotlink embed option only works for single files uploaded with a
Pro account. You can use this to directly embed a video player,
audio player, photo element or a download button in your site. Make
sure you have hotlinking enabled on your
<a href="/user/sharing/bandwidth">sharing settings page</a> or the
embed will not work.
</p>
</div>
<div class="tab_bar">
<button on:click={embed_iframe} class:button_highlight={tab === "iframe"}>
<i class="icon">code</i>
IFrame
</button>
{#if file.id}
<button on:click={embed_hotlink} class:button_highlight={tab === "hotlink"}>
<i class="icon">code</i>
Hotlink
</button>
{/if}
</div>
<div class="indent">
{#if tab === "iframe"}
<h3>Appearance</h3>
<p>
You can change the pixeldrain theme for your embedded file. Try the
available themes <a href="/appearance">here</a>.
</p>
<ThemePicker on:theme_change={e => set_style(e.detail)}></ThemePicker>
{:else}
<h3>Direct link</h3>
<p>
Hotlinking is only supported on <a href="/#pro">Pro</a>
accounts. If this file was not uploaded with a Pro account the
download will be blocked.
</p>
<p>
You can directly download the file from this link without using the
file viewer:
<br/>
{domain_url()}{file.get_href}
</p>
{/if}
<h3>Code</h3>
<p>
Put this code in your website to embed the file.
</p>
<div class="center">
<textarea bind:value={embed_html} style="width: 99%; height: 4em;"></textarea>
<br/>
<CopyButton text={embed_html}>Copy HTML</CopyButton>
<button on:click={toggle_example} class:button_highlight={example}>
<i class="icon">visibility</i> Show example
</button>
</div>
<h3>Example</h3>
</div>
<div bind:this={preview_area} style="text-align: center;"></div>
</div>
<style>
.center {
text-align: center;
}
.container {
width: 100%;
overflow: hidden;
}
.tab_bar {
border-bottom: 2px solid var(--separator);
}
</style>

View File

@@ -1,148 +0,0 @@
<script>
import { createEventDispatcher, tick } from "svelte";
import { formatDataVolume } from "util/Formatting";
import DirectoryElement from "user_home/filemanager/DirectoryElement.svelte";
import Modal from "util/Modal.svelte";
let dispatch = createEventDispatcher()
export let multi_select = true
export let title = ""
let modal;
let directory_element;
let input_search;
export const open = async () => {
modal.show()
await tick()
directory_element.setSelectionMode(true)
get_files()
}
let get_files = () => {
fetch(window.api_endpoint + "/user/files").then(resp => {
if (!resp.ok) { Promise.reject("yo") }
return resp.json()
}).then(resp => {
directory_element.reset()
for (let i in resp.files) {
directory_element.addFile(
resp.files[i].id,
window.api_endpoint + "/file/" + resp.files[i].id + "/thumbnail?width=32&height=32",
resp.files[i].name,
"/u/" + resp.files[i].id,
resp.files[i].mime_type,
resp.files[i].size,
formatDataVolume(resp.files[i].size, 4),
resp.files[i].date_upload,
)
}
directory_element.renderFiles()
}).catch((err) => {
throw (err)
})
}
const search = (e) => {
if (e.keyCode === 27) { // Escape
e.preventDefault()
input_search.value = ""
input_search.blur()
}
requestAnimationFrame(() => {
directory_element.search(input_search.value)
})
}
let done = () => {
let selected_files = directory_element.getSelectedFiles()
if (selected_files.length > 0) {
dispatch("files", selected_files)
}
modal.hide()
}
const keydown = (e) => {
if (!modal.is_visible()) {
return // Prevent a closed window from catching key events
} else if (e.ctrlKey || e.altKey || e.metaKey) {
return // prevent custom shortcuts from interfering with system shortcuts
} else if (document.activeElement.type && document.activeElement.type === "text") {
return // Prevent shortcuts from interfering with input fields
}
if (e.key === "/") {
e.preventDefault()
input_search.focus()
}
}
</script>
<svelte:window on:keydown={keydown} />
<Modal bind:this={modal} width="1400px" height="1200px">
<div class="header" slot="title">
<button class="button round" on:click={modal.hide}>
<i class="icon">close</i> Cancel
</button>
<div class="title">
{title}
</div>
<input bind:this={input_search} on:keyup={search} class="search" type="text" placeholder="press / to search"/>
<button class="button button_highlight round" on:click={done}>
<i class="icon">done</i> Add
</button>
</div>
<div class="dir_container">
<DirectoryElement bind:this={directory_element} multi_select={multi_select}></DirectoryElement>
</div>
</Modal>
<style>
.dir_container {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.header {
flex-grow: 1;
flex-shrink: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
font-size: 1em;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.title {
flex-grow: 1;
flex-shrink: 1;
text-align: center;
font-size: 1.2em;
}
.search {
min-width: 100px;
max-width: 300px;
flex-grow: 1;
flex-shrink: 1;
align-self: stretch;
}
@media(max-width: 700px) {
.title {
display: none;
}
.search {
max-width: none;
}
}
.button {
flex-grow: 0;
flex-shrink: 0;
}
</style>

View File

@@ -1,62 +0,0 @@
<script>
import { formatDataVolume, formatThousands } from "util/Formatting"
import { set_file, stats } from "lib/StatsSocket"
export let file = {
id: "",
views: 0,
size: 0,
downloads: 0,
bandwidth_used: 0,
bandwidth_used_paid: 0,
}
let views = 0
let downloads = 0
let size = 0
$: {
size = file.size
if ($stats.file_stats_init) {
views = $stats.file_stats.views
if (file.size === 0) {
downloads = $stats.file_stats.downloads
} else {
downloads = Math.round(($stats.file_stats.bandwidth + $stats.file_stats.bandwidth_paid) / file.size)
}
} else {
views = file.views
if (file.size === 0) {
downloads = file.downloads
} else {
downloads = Math.round((file.bandwidth_used + file.bandwidth_used_paid) / file.size)
}
}
}
$: set_file(file.id)
</script>
<div>
<div class="label">Views</div>
<div class="stat">{formatThousands(views)}</div>
<div class="label">Downloads</div>
<div class="stat">{formatThousands(downloads)}</div>
<div class="label">Size</div>
<div class="stat">{formatDataVolume(size, 3)}</div>
</div>
<style>
.label {
text-align: left;
padding-left: 10px;
font-size: 0.8em;
line-height: 0.7em;
margin-top: 0.5em;
}
.stat {
text-align: center;
}
</style>

View File

@@ -1,78 +0,0 @@
<script context="module">
export const file_struct = {
id: "",
name: "",
size: 0,
bandwidth_used: 0,
bandwidth_used_paid: 0,
downloads: 0,
views: 0,
mime_type: "",
availability: "",
abuse_type: "",
hash_sha256: "",
show_ads: false,
can_edit: false,
can_download: false,
get_href: "",
info_href: "",
download_href: "",
icon_href: "",
}
export const list_struct = {
id: "",
title: "",
files: [],
download_href: "",
info_href: "",
can_edit: false,
}
export const file_set_href = file => {
file.get_href = window.api_endpoint+"/file/"+file.id
file.info_href = window.api_endpoint+"/file/"+file.id+"/info"
file.download_href = window.api_endpoint+"/file/"+file.id+"?download"
file.icon_href = window.api_endpoint+"/file/"+file.id+"/thumbnail"
file.timeseries_href = window.api_endpoint+"/file/"+file.id+"/timeseries"
}
export const file_type = file => {
if (file.mime_type === "application/bittorrent" || file.mime_type === "application/x-bittorrent") {
return "torrent"
} else if (
file.mime_type === "application/zip" ||
file.mime_type === "application/x-7z-compressed" ||
file.mime_type === "application/x-tar" ||
(file.mime_type === "application/gzip" && file.name.endsWith(".tar.gz")) ||
(file.mime_type === "application/x-xz" && file.name.endsWith(".tar.xz")) ||
(file.mime_type === "application/zstd" && file.name.endsWith(".tar.zst"))
) {
return "zip"
} else if (file.mime_type.startsWith("image")) {
return "image"
} else if (
file.mime_type.startsWith("video") &&
file.mime_type !== "video/x-matroska"
) {
return "video"
} else if (
file.mime_type.startsWith("audio") ||
file.mime_type === "application/ogg" ||
file.name.endsWith(".mp3")
) {
return "audio"
} else if (
file.mime_type === "application/pdf" ||
file.mime_type === "application/x-pdf"
) {
return "pdf"
} else if (
file.mime_type === "application/json" ||
file.mime_type === "application/x-shellscript" ||
file.mime_type.startsWith("text")
) {
return "text"
} else {
return "file"
}
}
</script>

View File

@@ -1,736 +0,0 @@
<script>
import { onMount, tick } from "svelte";
import { file_struct, list_struct, file_set_href } from "./FileUtilities.svelte";
import Modal from "util/Modal.svelte";
import DetailsWindow from "./DetailsWindow.svelte";
import FilePreview from "./viewers/FilePreview.svelte";
import ListNavigator from "./ListNavigator.svelte";
import FileStats from "./FileStats.svelte";
import EditWindow from "./EditWindow.svelte";
import EmbedWindow from "./EmbedWindow.svelte";
import ReportWindow from "./ReportWindow.svelte";
import BottomBanner from "./BottomBanner.svelte";
import Sharebar from "./Sharebar.svelte";
import GalleryView from "./GalleryView.svelte";
import Downloader from "./Downloader.svelte";
import CustomBanner from "./CustomBanner.svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import TransferLimit from "./TransferLimit.svelte";
import ListStats from "./ListStats.svelte";
import ListUpdater from "./ListUpdater.svelte";
import CopyButton from "layout/CopyButton.svelte";
import Menu from "filesystem/Menu.svelte"
import AffiliatePrompt from "user_home/AffiliatePrompt.svelte";
let loading = true
let embedded = false
let ads_enabled = false
let view = "" // file or gallery
let file = file_struct
let list = list_struct
let is_list = false
let list_downloadable = false
let file_viewer
let file_preview
let list_navigator
let sharebar
let sharebar_visible = false
let fullscreen = false
let toggle_sharebar = () => {
if (navigator.share) {
let name = file.name
if (is_list) {
name = list.title
}
navigator.share({
title: name,
text: "I would like to share '" + name + "' with you",
url: window.location.href
})
return
}
sharebar_visible = !sharebar_visible
if (sharebar_visible) {
sharebar.show()
} else {
sharebar.hide()
}
}
let toolbar_visible = (window.innerWidth > 600)
let toolbar_toggle = () => {
toolbar_visible = !toolbar_visible
if (!toolbar_visible && sharebar_visible) {
toggle_sharebar()
}
}
let downloader
let list_updater
let details_window
let affiliate_prompt
let details_visible = false
let qr_window
let qr_visible = false
let edit_window
let edit_visible = false
let report_window
let report_visible = false
let embed_window
let embed_visible = false
onMount(() => {
let viewer_data = window.viewer_data
embedded = viewer_data.embedded
if (embedded) {
toolbar_visible = false
}
if (viewer_data.type === "list") {
open_list(viewer_data.api_response)
} else {
list.files = [viewer_data.api_response]
open_file_index(0)
}
ads_enabled = list.files[0].show_ads
loading = false
})
const reload = async () => {
loading = true
if (is_list) {
try {
const resp = await fetch(list.info_href);
if (resp.status >= 400) {
throw (await resp.json()).message
}
open_list(await resp.json())
} catch (err) {
alert(err)
}
} else {
try {
const resp = await fetch(file.info_href);
if (resp.status >= 400) {
throw (await resp.json()).message
}
list.files = [await resp.json()]
open_file_index(0)
} catch (err) {
alert(err)
}
}
loading = false
}
const open_list = l => {
l.download_href = window.api_endpoint+"/list/"+l.id+"/zip"
l.info_href = window.api_endpoint+"/list/"+l.id
list_downloadable = true
l.files.forEach(f => {
file_set_href(f)
if (!f.can_download) {
list_downloadable = false
}
})
list = l
// Setting is_list to true activates the ListNavgator, which makes sure the
// correct file is opened
is_list = true
if (l.files.length !== 0) {
apply_customizations(l.files[0])
}
hash_change()
}
const hash_change = () => {
// Skip to the file defined in the link hash
let matches = location.hash.match(/item=([\d]+)/)
let index = parseInt(matches ? matches[1] : null)
if (Number.isInteger(index)) {
// The URL contains an item number. Navigate to that item
open_file_index(index)
return
}
// If the hash does not contain a file ID we open the gallery
if (view !== "gallery") {
view = "gallery"
file = file_struct // Empty the file struct
document.title = list.title+" ~ pixeldrain"
}
}
const open_file_index = async index => {
if (index >= list.files.length) {
index = 0
} else if (index < 0) {
index = list.files.length - 1
}
if (list.files[index] === file) {
console.debug("ignoring request to load the same file that is currently loaded")
return
}
console.debug("received request to open file", index)
file_set_href(list.files[index])
file = list.files[index]
// Switch from gallery view to file view if it's not already so
if (view !== "file") {
view = "file"
await tick() // Wait for the file_preview and list_navigator to render
}
// Tell the preview window to start rendering the file
file_preview.set_file(file)
// Tell the list_navigator to highlight the loaded file
if (is_list) {
// Update the URL. This triggers the hash_change again, but it gets
// ignored because the file is already loaded
window.location.hash = "#item=" + index
document.title = file.name+" ~ "+list.title+" ~ pixeldrain"
list_navigator.set_item(index)
} else {
document.title = file.name+" ~ pixeldrain"
}
apply_customizations(file)
}
const toggle_gallery = () => {
if (view === "gallery") {
window.location.hash = "#item=0"
} else {
window.location.hash = ""
}
}
const toggle_fullscreen = () => {
if (fullscreen || document.fullscreenElement) {
try {
document.exitFullscreen()
} catch (err) {
console.debug("Failed to exit fullscreen", err)
}
fullscreen = false
} else {
file_viewer.requestFullscreen()
fullscreen = true
}
}
// Premium page customizations. In the gallery view we will use the
// customizations for the first file in the list, else we simply use the
// selected file. In most cases they are all the same so the user won't notice
// any change
let file_preview_background
let custom_header = ""
let custom_header_link = ""
let custom_background = ""
let custom_footer = ""
let custom_footer_link = ""
let disable_download_button = false
let disable_share_button = false
const apply_customizations = file => {
if (!file.branding) {
return
}
if (file.branding.header_image) {
custom_header = window.api_endpoint+"/file/"+file.branding.header_image
}
if (file.branding.header_link) {
custom_header_link = file.branding.header_link
}
if (file.branding.footer_image) {
custom_footer = window.api_endpoint+"/file/"+file.branding.footer_image
}
if (file.branding.footer_link) {
custom_footer_link = file.branding.footer_link
}
if (file.branding.affiliate_prompt) {
affiliate_prompt.prompt(file.branding.affiliate_prompt)
}
if (file.branding.disable_download_button && !file.can_edit) {
disable_download_button = true
}
if (file.branding.disable_share_button && !file.can_edit) {
disable_share_button = true
}
if (file.branding.background_image) {
custom_background = window.api_endpoint+"/file/"+file.branding.background_image
file_preview_background.style.backgroundImage = "url('"+custom_background+"')"
} else {
file_preview_background.style.backgroundImage = ""
}
}
const grab_file = async () => {
if (!window.user_authenticated) {
return
}
const form = new FormData()
form.append("grab_file", file.id)
try {
const resp = await fetch(
window.api_endpoint + "/file",
{ method: "POST", body: form },
);
if (resp.status >= 400) {
throw (await resp.json()).message
}
window.open("/u/" + (await resp.json()).id, "_blank")
} catch (err) {
alert("Failed to grab file: " + err)
return
}
}
let copy_btn
const keyboard_event = evt => {
if (evt.ctrlKey || evt.altKey || evt.metaKey) {
return // prevent custom shortcuts from interfering with system shortcuts
}
if (
document.activeElement.type && (
document.activeElement.type === "text" ||
document.activeElement.type === "email" ||
document.activeElement.type === "textarea"
)
) {
return // Prevent shortcuts from interfering with input fields
}
console.debug("Key pressed: " + evt.key)
switch (evt.key) {
case "a": // A or left arrow key go to previous file
case "ArrowLeft":
if (list_navigator) {
list_navigator.prev()
}
break
case "d": // D or right arrow key go to next file
case "ArrowRight":
if (list_navigator) {
list_navigator.next()
}
break
case " ": // Spacebar pauses / unpauses video and audio playback
if (file_preview.toggle_playback()) {
evt.preventDefault()
evt.stopPropagation()
}
break
case "m": // M to mute video / audio
file_preview.toggle_mute()
break
case "h":
file_preview.seek(-20)
break
case "j":
file_preview.seek(-5)
break
case "k":
file_preview.seek(5)
break
case "l":
file_preview.seek(20)
break
case ",":
file_preview.seek(-0.04) // Roughly a single frame.. assuming 25fps
break
case ".":
file_preview.seek(0.04)
break
case "s":
case "S":
if (evt.shiftKey) {
downloader.download_list() // SHIFT + S downloads all files in list
} else {
downloader.download_file() // S to download the current file
}
break
case "r": // R to toggle the report window
report_window.toggle()
break
case "c": // C to copy to clipboard
copy_btn.copy()
break
case "i": // I to open the details window
details_window.toggle()
break
case "e": // E to open the edit window
if (file.can_edit || list.can_edit) {
edit_window.toggle()
}
break
case "g": // G to grab this file
grab_file()
break
case "q": // Q to close the window
window.close()
break
case "u": // U to upload new files
if (list_updater) {
list_updater.pick_files()
}
}
}
</script>
<svelte:window on:keydown={keyboard_event} on:hashchange={hash_change}/>
<div class="file_viewer" bind:this={file_viewer}>
<div class="headerbar">
<button
on:click={toolbar_toggle}
class="round"
class:button_highlight={toolbar_visible}
style="line-height: 1em;"
title="Open or close the toolbar">
<i class="icon">menu</i>
</button>
<Menu embedded={embedded}/>
<div class="file_viewer_headerbar_title">
{#if list.title !== ""}{list.title}<br/>{/if}
{#if file.name !== ""}{file.name}{/if}
</div>
{#if embedded}
<a href={window.location.pathname} target="_blank" class="button round" title="Open this page in a new tab" rel="noreferrer">
<i class="icon" id="btn_fullscreen_icon">open_in_new</i>
</a>
{/if}
</div>
{#if is_list && view === "file"}
<ListNavigator
bind:this={list_navigator}
files={list.files}
on:set_file={e => open_file_index(e.detail)}
on:toggle_gallery={toggle_gallery}
>
</ListNavigator>
{/if}
<CustomBanner src={custom_header} link={custom_header_link} border_top={true}></CustomBanner>
<div class="file_preview_row">
<div class="toolbar" class:toolbar_visible>
{#if view === "file"}
<FileStats file={file}/>
{:else if view === "gallery"}
<ListStats list={list}/>
{/if}
<div class="separator"></div>
{#if view === "file" && !disable_download_button}
<button
on:click={downloader.download_file}
class="toolbar_button"
class:button_red={file.can_download === false}
title="Save this file to your computer">
<i class="icon">download</i>
<span>Download</span>
</button>
{/if}
{#if is_list && list_downloadable && !disable_download_button}
<button
on:click={downloader.download_list}
class="toolbar_button"
title="Download all files in this album as a zip archive">
<i class="icon">download</i>
<span>DL all files</span>
</button>
{/if}
<CopyButton bind:this={copy_btn} text={window.location.href} style="width: calc(100% - 4px)">
<u>C</u>opy link
</CopyButton>
{#if !disable_share_button}
<button
on:click={toggle_sharebar}
class="toolbar_button"
class:button_highlight={sharebar_visible}
title="Share this file on social media">
<i class="icon">share</i>
<span>Share</span>
</button>
{/if}
<button
class="toolbar_button"
on:click={qr_window.toggle}
class:button_highlight={qr_visible}
title="Show a QR code with a link to this page. Useful for sharing files in-person">
<i class="icon">qr_code</i>
<span>QR code</span>
</button>
<button
class="toolbar_button"
on:click={toggle_fullscreen}
class:button_highlight={fullscreen}
title="Open page in full screen mode">
{#if fullscreen}
<i class="icon">fullscreen_exit</i>
{:else}
<i class="icon">fullscreen</i>
{/if}
<span>Fullscreen</span>
</button>
{#if view === "file"}
<button
class="toolbar_button"
on:click={details_window.toggle}
class:button_highlight={details_visible}
title="Information and statistics about this file">
<i class="icon">help</i>
<span>Deta<u>i</u>ls</span>
</button>
{/if}
<div class="separator"></div>
{#if file.can_edit || list.can_edit}
<button
class="toolbar_button"
on:click={edit_window.toggle}
class:button_highlight={edit_visible}
title="Edit or delete this file or album">
<i class="icon">edit</i>
<span><u>E</u>dit</span>
</button>
{/if}
{#if view === "file" && window.user_authenticated && !disable_download_button}
<button
on:click={grab_file}
class="toolbar_button"
title="Copy this file to your own pixeldrain account">
<i class="icon">save_alt</i>
<span><u>G</u>rab file</span>
</button>
{/if}
<button
class="toolbar_button"
title="Report this file as abusive"
on:click={report_window.toggle}
class:button_highlight={report_visible}>
<i class="icon">flag</i>
<span>Report</span>
</button>
{#if !disable_download_button}
<button
class="toolbar_button"
title="Include this file in your own webpages"
on:click={embed_window.toggle}
class:button_highlight={embed_visible}>
<i class="icon">code</i>
<span>Embed</span>
</button>
{/if}
</div>
<div bind:this={file_preview_background}
class="file_preview"
class:checkers={!custom_background}
class:custom_background={!!custom_background}
class:toolbar_visible
>
{#if view === "file"}
<FilePreview
bind:this={file_preview}
is_list={is_list}
on:download={downloader.download_file}
on:prev={() => { if (list_navigator) { list_navigator.prev() }}}
on:next={() => { if (list_navigator) { list_navigator.next() }}}
on:loading={e => {loading = e.detail}}
on:reload={reload}
/>
{:else if view === "gallery"}
<GalleryView
list={list}
on:reload={reload}
on:update_list={e => list_updater.update(e.detail)}
on:pick_files={() => list_updater.pick_files()}
on:upload_files={e => list_updater.upload_files(e.detail)}
/>
{/if}
</div>
<Sharebar bind:this={sharebar}></Sharebar>
</div>
{#if ads_enabled}
<BottomBanner/>
<TransferLimit/>
{:else if custom_footer}
<CustomBanner src={custom_footer} link={custom_footer_link}></CustomBanner>
{/if}
<Modal bind:this={details_window} on:is_visible={e => {details_visible = e.detail}} title="File details" width="1000px">
<DetailsWindow file={file}></DetailsWindow>
</Modal>
<Modal bind:this={qr_window} on:is_visible={e => {qr_visible = e.detail}} title="QR code" width="500px">
<img src="{window.api_endpoint}/misc/qr?text={encodeURIComponent(window.location.href)}" alt="QR code" style="display: block; width: 100%;"/>
</Modal>
<Modal bind:this={edit_window} on:is_visible={e => {edit_visible = e.detail}} title={"Editing "+file.name}>
<EditWindow file={file} list={list} on:reload={reload}></EditWindow>
</Modal>
<Modal bind:this={embed_window} on:is_visible={e => {embed_visible = e.detail}} title="Embed file" width="820px">
<EmbedWindow file={file} list={list}></EmbedWindow>
</Modal>
<Modal bind:this={report_window} on:is_visible={e => {report_visible = e.detail}} title="Report abuse" width="800px">
<ReportWindow file={file} list={list}></ReportWindow>
</Modal>
<Downloader bind:this={downloader} file={file} list={list}></Downloader>
{#if is_list && list.can_edit}
<ListUpdater
bind:this={list_updater}
list={list}
on:reload={reload}
on:loading={e => {loading = e.detail}}
/>
{/if}
<!-- At the bottom so it renders over everything else -->
<LoadingIndicator loading={loading}/>
<AffiliatePrompt bind:this={affiliate_prompt}/>
</div>
<style>
.file_viewer {
position: absolute;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow: hidden;
background: var(--body_background);
}
/* Headerbar (row 1) */
.headerbar {
flex-grow: 0;
flex-shrink: 0;
display: flex;
flex-direction: row;
text-align: left;
align-items: center;
}
@media(max-height: 600px) {
.headerbar {
padding: 1px;
}
}
/* Headerbar components */
.headerbar > * {
flex-grow: 0;
flex-shrink: 0;
margin: 3px;
}
.headerbar > .file_viewer_headerbar_title {
flex-grow: 1;
flex-shrink: 1;
display: flex;
flex-direction: column;
overflow: hidden;
line-height: 1.2em; /* When the page is a list there will be two lines. Don't want to stretch the container */
white-space: nowrap;
text-overflow: ellipsis;
justify-content: center;
}
.headerbar > button > .icon {
font-size: 1.6em;
}
/* File preview area (row 3) */
.file_preview_row {
flex-grow: 1;
flex-shrink: 1;
position: relative;
display: block;
}
.file_preview {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: block;
min-height: 100px;
min-width: 100px;
transition: left 0.5s;
overflow: auto;
text-align: center;
border-radius: 8px;
border: 2px solid var(--separator);
}
.file_preview.toolbar_visible { left: 8.2em; }
.file_preview.custom_background {
background-size: cover;
background-position: center;
}
/* Toolbars */
.toolbar {
position: absolute;
width: 8.2em;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: none;
left: -8.2em;
bottom: 0;
top: 0;
padding: 0;
text-align: left;
transition: left 0.5s, right 0.5s;
z-index: 1;
}
.toolbar::-webkit-scrollbar {
display: none;
}
.toolbar.toolbar_visible { left: 0; }
.toolbar_button{
width: calc(100% - 4px);
}
.toolbar_button > span {
vertical-align: middle;
}
.toolbar > .separator {
height: 2px;
width: 100%;
margin: 4px 0;
background-color: var(--separator);
}
</style>

View File

@@ -1,265 +0,0 @@
<script>
import { createEventDispatcher } from "svelte"
import { flip } from "svelte/animate"
import FilePicker from "./FilePicker.svelte"
import { file_type } from "./FileUtilities.svelte";
import { get_video_position } from "lib/VideoPosition"
import ProgressBar from "util/ProgressBar.svelte"
let dispatch = createEventDispatcher()
export let list = {
files: [],
can_edit: false,
}
let file_picker;
const add_files = async files => {
let list_files = list.files;
files.forEach(f => {
list_files.push(f)
})
dispatch("update_list", list_files)
}
const delete_file = async index => {
let list_files = list.files
list_files.splice(index, 1)
list.files = list_files // Update the view (and play animation)
dispatch("update_list", list_files)
}
const move_left = async index => {
if (index === 0) {
return;
}
let f = list.files;
[f[index], f[index-1]] = [f[index-1], f[index]];
list.files = f // Update the view (and play animation)
dispatch("update_list", f)
}
const move_right = async index => {
if (index >= list.files.length-1) {
return;
}
let f = list.files;
[f[index], f[index+1]] = [f[index+1], f[index]];
list.files = f // Update the view (and play animation)
dispatch("update_list", f)
}
// Index of the file which is being hovered over. -1 is nothing and -2 is the
// Add files button
let hovering = -1
let dragging = false
const drag = (e, index) => {
dragging = true
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.dropEffect = 'move';
e.dataTransfer.setData('text/plain', index);
}
const drop = (e, index) => {
hovering = -1
dragging = false
if (e.dataTransfer.files.length !== 0) {
// This is not a rearrangement, this is a file upload
dispatch("upload_files", e.dataTransfer.files)
return
} else if (index === -2) {
return
}
e.dataTransfer.dropEffect = 'move';
let start = parseInt(e.dataTransfer.getData("text/plain"));
let list_files = list.files
if (start < index) {
list_files.splice(index + 1, 0, list_files[start]);
list_files.splice(start, 1);
} else if (start > index) {
list_files.splice(index, 0, list_files[start]);
list_files.splice(start + 1, 1);
} else {
return; // Nothing changed
}
dispatch("update_list", list_files)
}
</script>
<div class="gallery">
{#if list.can_edit}
<div class="add_button"
on:drop|preventDefault={e => drop(e, -2)}
on:dragover|preventDefault|stopPropagation
on:dragenter={() => hovering = -2}
on:dragend={() => {hovering = -1}}
class:highlight={!dragging && hovering === -2}
role="listitem"
>
<button class="add_button_part" on:click={e => dispatch("pick_files")}>
<i class="icon">cloud_upload</i>
Upload files
</button>
<button class="add_button_part" on:click={file_picker.open}>
<i class="icon">add</i>
Add files
</button>
</div>
{/if}
{#each list.files as file, index (file)}
{@const vp = get_video_position(file.id)}
<a
href="#item={index}"
class="file"
draggable={list.can_edit}
on:dragstart={e => drag(e, index)}
on:drop|preventDefault={e => drop(e, index)}
on:dragover|preventDefault|stopPropagation
on:dragenter={() => hovering = index}
on:dragend={() => {hovering = -1; dragging = false}}
class:highlight={dragging && hovering === index}
animate:flip={{duration: 400}}>
<div
class="icon_container"
class:editing={list.can_edit}
class:wide={file_type(file) === "image" || file_type(file) === "video"}
style="background-image: url('{file.icon_href}?width=256&height=256');">
{#if list.can_edit}
<div class="button_row">
<i class="icon" style="cursor: grab;">
drag_indicator
</i>
<div class="separator"></div>
<button class="icon" on:click|stopPropagation|preventDefault={() => {move_left(index)}}>
chevron_left
</button>
<button class="icon" on:click|stopPropagation|preventDefault={() => {move_right(index)}}>
chevron_right
</button>
<button class="icon" on:click|stopPropagation|preventDefault={() => {delete_file(index)}}>
delete
</button>
</div>
{/if}
{#if vp !== null}
<div class="grow"></div>
<ProgressBar no_margin used={vp.pos} total={vp.dur}/>
{/if}
</div>
{file.name}
</a>
{/each}
</div>
<FilePicker
bind:this={file_picker}
on:files={e => {add_files(e.detail)}}
multi_select={true}
title="Select files to add to album">
</FilePicker>
<style>
.gallery{
width: 100%;
max-height: 100%;
overflow: auto;
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.file{
width: 200px;
max-width: 42%;
height: 200px;
margin: 8px;
overflow: hidden;
border-radius: 8px;
background: var(--input_background);
word-break: break-all;
text-align: center;
line-height: 1.2em;
display: inline-block;
text-overflow: ellipsis;
text-decoration: none;
vertical-align: top;
color: var(--body_text_color);
transition: background 0.2s, padding 0.2s, box-shadow 0.2s;
box-shadow: 1px 1px 0px 0px var(--shadow_color);
}
.file:hover {
background: var(--input_hover_background);
}
.highlight {
box-shadow: 0 0 0px 2px var(--highlight_color);
text-decoration: none;
}
.icon_container {
display: flex;
flex-direction: column;
margin: 3px;
height: 148px;
border-radius: 6px;
background-position: center;
background-size: contain;
background-repeat: no-repeat;
font-size: 22px;
text-align: left;
}
.icon_container.editing {
box-shadow: inset 0 60px 40px -20px var(--body_color);
}
.icon_container.wide {
background-size: cover;
}
.button_row {
display: flex;
flex-direction: row;
}
.button_row > .icon {
flex: 0 0 auto;
color: var(--body_text_color);
}
.button_row > button {
flex: 0 0 auto;
padding: 0;
}
.button_row>.separator {
flex: 1 1 auto;
}
.add_button{
width: 200px;
max-width: 42%;
height: 200px;
margin: 8px;
border-radius: 8px;
background: var(--body_color);
text-align: center;
line-height: 1.2em;
display: inline-block;
vertical-align: top;
color: var(--body_text_color);
display: flex;
flex-direction: column;
}
.add_button > * {
flex: 1 1 auto;
font-size: 1.5em;
cursor: pointer;
flex-direction: column;
justify-content: center;
}
.grow {
flex: 1 1 auto;
}
</style>

View File

@@ -1,82 +0,0 @@
<script>
import { onMount } from "svelte";
import { fade } from "svelte/transition";
let popup
let visible = false
export let target
$: set_target(target)
const set_target = el => {
if (!el) {
return
}
move_to_element(el)
setTimeout(() => { move_to_element(el) }, 500)
}
const move_to_element = el => {
if (visible && popup) {
let rect = el.getBoundingClientRect()
popup.style.top = (rect.top + el.offsetHeight + 20) + "px"
popup.style.left = (rect.left + (el.clientWidth / 2) - 40) + "px"
}
}
const close = () => {
localStorage.setItem("viewer_intro_popup_dismissed", "🍆")
visible = false
}
onMount(() => {
if (localStorage.getItem("viewer_intro_popup_dismissed") === "🍆") {
return
}
visible = true
})
</script>
{#if visible}
<div bind:this={popup} in:fade out:fade class="intro_popup">
<span class="light">Upload your own files here</span>
<p style="margin: 0.4em 0;">
With pixeldrain you can share your files anywhere on the web. The
sky is the limit!
</p>
<button on:click={close} class="close button_highlight round">
<i class="icon">check</i> Got it
</button>
</div>
{/if}
<style>
.intro_popup {
position: absolute;
top: 0;
left: 0;
width: 380px;
max-width: 80%;
height: auto;
padding: 8px;
background-color: var(--card_color);
box-shadow: 1px 1px 10px -2px var(--shadow_color);
border-radius: 20px;
z-index: 50;
transition: opacity .4s, left .5s, top .5s;
}
.intro_popup:before {
content: "";
display: block;
position: absolute;
left: 30px;
top: -15px;
border-bottom: 15px solid var(--card_color);
border-left: 15px solid transparent;
border-right: 15px solid transparent;
}
.light {
font-size: 1.6em;
}
.close {
float: right;
margin: 0;
}
</style>

View File

@@ -1,147 +0,0 @@
<script>
import { createEventDispatcher } from "svelte";
let dispatch = createEventDispatcher()
export let files = []
let file_list_div
let selected_file_index = 0
export const next = () => dispatch("set_file", selected_file_index+1)
export const prev = () => dispatch("set_file", selected_file_index-1)
export const toggle_gallery = () => dispatch("toggle_gallery")
let history = []
export const rand_item = () => {
// Avoid viewing the same file multiple times
let rand
do {
rand = Math.floor(Math.random() * files.length)
console.log("rand is " + rand)
} while(history.indexOf(rand) > -1)
dispatch("set_file", rand)
}
export const set_item = idx => {
// Remove the class from the previous selected file
selected_file_index = idx
files.forEach((f, i) => {
f.selected = selected_file_index === i
})
files = files
// Add item to history
if(history.length >= (files.length - 6)){
history.shift()
}
history.push(idx)
// Smoothly scroll the navigator to the correct element
let selected_file = file_list_div.children[idx]
let cst = window.getComputedStyle(selected_file)
let itemWidth = selected_file.offsetWidth + parseInt(cst.marginLeft) + parseInt(cst.marginRight)
let start = file_list_div.scrollLeft
let end = ((idx * itemWidth) + (itemWidth / 2)) - (file_list_div.clientWidth / 2)
let steps = 30 // One second
let stepSize = (end - start)/steps
let animateScroll = (pos, step) => {
file_list_div.scrollLeft = pos
if (step < steps) {
requestAnimationFrame(() => {
animateScroll(pos+stepSize, step+1)
})
}
}
animateScroll(start, 0)
}
</script>
<div class="nav_container">
<button class="nav_button" on:click={toggle_gallery} title="Opens a gallery view of the album">
<i class="icon">photo_library</i>
Gallery
</button>
<div bind:this={file_list_div} class="list_navigator">
{#each files as file, index (file)}
<a
href="#item={index}"
title="Open {file.name}"
class="file_button"
class:file_selected={file.selected}>
<img src={file.icon_href+"?width=64&height=64"} alt={file.name} class="list_item_thumbnail" loading="lazy"/>
{file.name}
</a>
{/each}
</div>
</div>
<style>
.nav_container{
flex-grow: 0;
flex-shrink: 0;
display: flex;
position: relative;
width: 100%;
border-top: 2px solid var(--separator);
text-align: center;
line-height: 1em;
}
.nav_button{
flex-grow: 0;
flex-shrink: 0;
}
.list_navigator {
flex-grow: 1;
flex-shrink: 1;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
}
.file_button {
position: relative;
height: 2.6em;
width: 220px;
margin: 2px;
padding: 0;
overflow: hidden;
border-radius: 6px;
background: var(--input_background);
color: var(--body_text_color);
word-break: break-all;
text-align: left;
line-height: 1.2em;
display: inline-block;
transition: background 0.2s;
white-space: normal;
text-decoration: none;
vertical-align: top;
cursor: pointer;
border-width: 1px;
border-style: solid;
border-color: var(--input_background);
box-shadow: 1px 1px 0px 0px var(--shadow_color);
}
.file_button:hover {
text-decoration: none;
background: var(--input_hover_background);
}
.file_button>img {
height: 100%;
margin-right: 5px;
float: left;
display: block;
}
.file_selected {
text-decoration: none;
border-color: var(--highlight_color);
}
</style>

View File

@@ -1,46 +0,0 @@
<script>
import { formatDataVolume, formatThousands } from "util/Formatting"
export let list = {
files: [],
}
$: size = list.files.reduce((acc, file) => acc += file.size, 0)
$: views = list.files.reduce((acc, file) => acc += file.views, 0)
$: downloads = list.files.reduce(
(acc, file) => {
if (file.size === 0) {
acc += file.downloads
} else {
acc += Math.round((file.bandwidth_used + file.bandwidth_used_paid) / file.size)
}
return acc
},
0,
)
</script>
<div>
<div class="label">Files</div>
<div class="stat">{list.files.length}</div>
<div class="label">Views</div>
<div class="stat">{formatThousands(views)}</div>
<div class="label">Downloads</div>
<div class="stat">{formatThousands(downloads)}</div>
<div class="label">Size</div>
<div class="stat">{formatDataVolume(size, 3)}</div>
</div>
<style>
.label {
text-align: left;
padding-left: 10px;
font-size: 0.8em;
line-height: 0.7em;
margin-top: 0.5em;
}
.stat {
text-align: center;
}
</style>

View File

@@ -1,82 +0,0 @@
<script>
import { createEventDispatcher } from "svelte";
import UploadWidget from "util/upload_widget/UploadWidget.svelte";
let dispatch = createEventDispatcher()
export let list = {
title: "",
files: [],
info_href: "",
}
export const update = async new_files => {
dispatch("loading", true)
// If the list is empty we simply delete it
if (list.files.length === 0) {
try {
let resp = await fetch(list.info_href, {method: "DELETE"})
if (resp.status >= 400) {
throw (await resp.json()).message
}
window.close()
} catch (err) {
alert("Failed to delete album: "+err)
} finally {
dispatch("loading", false)
}
return
}
let listjson = {
title: list.title,
files: [],
}
new_files.forEach(f => {
listjson.files.push({
id: f.id,
})
})
try {
const resp = await fetch(
list.info_href,
{ method: "PUT", body: JSON.stringify(listjson) },
);
if (resp.status >= 400) {
throw (await resp.json()).message
}
} catch (err) {
alert("Failed to update album: "+err)
} finally {
dispatch("loading", false)
dispatch("reload")
}
}
let upload_widget
export const pick_files = () => upload_widget.pick_files()
export const upload_files = files => upload_widget.upload_files(files)
const uploads_finished = async (file_ids) => {
let list_files = list.files;
file_ids.forEach(id => {
list_files.push({id: id})
})
await update(list_files)
}
const paste = (e) => {
if (e.clipboardData.files.length !== 0) {
e.preventDefault();
e.stopPropagation();
upload_widget.upload_files(e.clipboardData.files)
}
}
</script>
<svelte:window on:paste={paste}/>
<UploadWidget bind:this={upload_widget} on:uploads_finished={e => uploads_finished(e.detail)}/>

View File

@@ -1,308 +0,0 @@
<script>
import Spinner from "util/Spinner.svelte"
export let file = {
id: "",
name: "",
get_href: "",
mime_type: "",
}
export let list = {
id: "",
files: [],
}
const filter_visual = type => {
return type.startsWith("image/") ||
type.startsWith("video/") ||
type === "application/pdf"
}
const filter_audio = type => {
return type.startsWith("audio/")
}
const filter_audiovisual = type => {
return filter_visual(type) || filter_audio(type)
}
const filter_app = type => {
return type.startsWith("application/") ||
type.startsWith("text/")
}
const abuse_categories = [
{
type: "terrorism",
name: "Terrorism",
desc: `Videos, images or audio fragments showing or promoting the use
of intentional violence to achieve political aims`,
filter: filter_audiovisual,
}, {
type: "gore",
name: "Gore",
desc: `Graphic and shocking videos or images depicting severe harm to
humans (or animals)`,
filter: filter_visual,
}, {
type: "child_abuse",
name: "Child abuse",
desc: `Videos or images depicting inappropriate touching or nudity of
children under 18 years old`,
}, {
type: "zoophilia",
name: "Zoophilia",
desc: `Videos or images depicting of sexual acts being performed on
animals`,
}, {
type: "revenge_porn",
name: "Revenge porn",
desc: `Sexually explicit images or videos of individuals without their
consent and blackmail content`,
}, {
type: "doxing",
name: "Doxing",
desc: `Personally identifiable information being shared without the
consent of the owner. This includes things like passport scans,
e-mail addresses, telephone numbers and passwords`,
}, {
type: "malware",
name: "Malware",
desc: `Software programs designed to cause harm to computer systems`,
filter: filter_app,
},
]
let abuse_type = ""
let single_or_all = "single"
let loading = false
let results = []
let submit = async e => {
e.preventDefault()
if (abuse_type === "") {
results = [{success: false, text: "Please select an abuse type"}]
return
} else if (description.length > 500) {
results = [{success: false, text: "Description is too long"}]
return
}
loading = true
let files = []
if (file.id === "") {
single_or_all = "all"
} else if (list.id === "") {
single_or_all = "single"
}
if (single_or_all === "all") {
list.files.forEach(file => {
// Only report files which have not been blocked yet
if (file.abuse_type === "") {
files.push(file.id)
}
})
} else {
files.push(file.id)
}
const form = new FormData()
form.append("type", abuse_type)
form.append("description", report_description())
results = []
for (let file_id of files) {
try {
const resp = await fetch(
window.api_endpoint + "/file/" + file_id + "/report_abuse",
{ method: "POST", body: form }
);
if (resp.status >= 400) {
let json = await resp.json()
if (json.value === "resource_already_exists") {
throw "You have already reported this file"
} else if (json.value === "file_already_blocked") {
throw "This file has already been blocked"
} else if (json.value === "multiple_errors") {
throw json.errors[0].message
}
throw json.message
}
results.push({success: true, text: "Report has been sent"})
} catch (err) {
results.push({success: false, text: "Failed to send report: "+err})
}
results = results
}
loading = false
}
let description = ""
let child_abuse_password = ""
const report_description = () => {
if (abuse_type === "child_abuse") {
return "Password: " + child_abuse_password + "\n" +
"Description:\n" + description;
} else {
return description
}
}
</script>
<div class="container">
<p>
If you think this file violates pixeldrain's
<a href="/abuse">content policy</a> you can report it for moderation
with this form. For copyright infringement notices or urgent matters
please use our
<a href="/abuse#toc_2">abuse e-mail address</a>.
</p>
<form on:submit={submit} style="width: 100%" class="report_form">
<h3>Abuse type</h3>
<p>
Which type of abuse is shown in this file? Pick the most
appropriate one.
</p>
{#each abuse_categories as cat}
{#if cat.filter === undefined || cat.filter(file.mime_type) }
<label for="type_{cat.type}">
<input type="radio" bind:group={abuse_type} id="type_{cat.type}" name="abuse_type" value="{cat.type}">
<div>
<b>{cat.name}</b><br/>
{cat.desc}
</div>
</label>
{/if}
{/each}
{#if list.id !== "" && file.id !== ""}
<h3>Report multiple files?</h3>
<label for="report_single">
<input type="radio" bind:group={single_or_all} id="report_single" name="single_or_all" value="single">
<div>Report only the selected file ({file.name})</div>
</label>
<label for="report_all" style="border-bottom: none;">
<input type="radio" bind:group={single_or_all} id="report_all" name="single_or_all" value="all">
<div>Report all {list.files.length} files in this album</div>
</label>
{/if}
<h3>Description</h3>
{#if abuse_type === "child_abuse"}
<div class="highlight_yellow" style="text-align: initial;">
<p>
The child abuse category is only for cases where real
children were abused. This is not for fictional works.
</p>
</div>
<br/>
<div>If this file is an encrypted archive, please provide the password so we can verify the contents</div>
<input type="text" bind:value={child_abuse_password} placeholder="Password..."/>
{:else if abuse_type === "revenge_porn"}
<div class="highlight_yellow" style="text-align: initial;">
<p>
The revenge porn category is for blackmail content and
non-consensual deepfake porn. If you use this category for
copyright violations then your report will be ignored.
</p>
</div>
<br/>
{/if}
<div>
Please provide some context for your report. Why do you think this
file violates the content policy? ({description.length}/500)
</div>
<textarea bind:value={description} placeholder="Context here..." required></textarea>
<div>
This is not a contact form. You will not receive a reply to any
questions asked in this description field.
</div>
<h3>Send</h3>
{#if loading}
<div class="spinner_container">
<Spinner></Spinner>
</div>
{/if}
{#each results as result}
<div class:highlight_green={result.success} class:highlight_red={!result.success}>
{result.text}
</div>
{/each}
<p>
Abuse reports are manually reviewed. Normally this shouldn't
take more than 24 hours. During busy periods it can take
longer.
</p>
<p>
Reports are sent for each file separately. Please wait until all
reports have been submitted after clicking submit.
</p>
<div style="text-align: center;">
<button class="button_highlight abuse_report_submit" type="submit" style="justify-content: center; width: 100%; max-width: 200px">
<i class="icon">send</i>
<span>Submit report</span>
</button>
</div>
</form>
</div>
<style>
.container {
width: 100%;
padding: 10px;
overflow: hidden;
}
label {
padding: 0.2em;
display: flex;
flex-direction: row;
}
label > input {
flex: 0 0 auto;
margin-right: 0.5em;
}
label > div {
flex: 1 1 auto;
padding: 0 0.2em;
border-radius: 6px;
border: 1px solid var(--separator);
}
input[type="radio"]:checked+div {
border-color: var(--highlight_color);
}
.spinner_container {
position: absolute;
top: auto;
left: 10px;
height: 100px;
width: 100px;
z-index: 1000;
}
.report_form {
width: 100%;
}
.report_form > input[type="text"],
.report_form > textarea {
width: 100%;
margin: 0 0 0.5em 0;
}
.report_form > textarea {
height: 5em;
}
</style>

View File

@@ -1,90 +0,0 @@
<script>
export let visible = false
export const show = () => { visible = true }
export const hide = () => { visible = false }
const share_email = () => {
window.open(
'mailto:please@set.address?subject=File%20on%20pixeldrain&body='+encodeURIComponent(window.location.href)
);
}
const share_reddit = () => {
window.open('https://www.reddit.com/submit?url='+encodeURIComponent(window.location.href));
}
const share_twitter = () => {
window.open('https://twitter.com/share?url='+encodeURIComponent(window.location.href));
}
const share_facebook = () => {
window.open('http://www.facebook.com/sharer.php?u='+encodeURIComponent(window.location.href));
}
const share_tumblr = () => {
window.open('http://www.tumblr.com/share/link?url='+encodeURIComponent(window.location.href));
}
</script>
<div class="sharebar" class:visible>
Share on:<br/>
<button class="button_full_width" on:click={share_email}>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M22 4H2v16h20V4zm-2 4l-8 5-8-5V6l8 5 8-5v2z"/>
</svg>
E-Mail
</button>
<button class="button_full_width" on:click={share_reddit}>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 24 24">
<path d="M22,12.14C22,10.92 21,9.96 19.81,9.96C19.22,9.96 18.68,10.19 18.29,10.57C16.79,9.5 14.72,8.79 12.43,8.7L13.43,4L16.7,4.71C16.73,5.53 17.41,6.19 18.25,6.19C19.11,6.19 19.81,5.5 19.81,4.63C19.81,3.77 19.11,3.08 18.25,3.08C17.65,3.08 17.11,3.43 16.86,3.95L13.22,3.18C13.11,3.16 13,3.18 12.93,3.24C12.84,3.29 12.79,3.38 12.77,3.5L11.66,8.72C9.33,8.79 7.23,9.5 5.71,10.58C5.32,10.21 4.78,10 4.19,10C2.97,10 2,10.96 2,12.16C2,13.06 2.54,13.81 3.29,14.15C3.25,14.37 3.24,14.58 3.24,14.81C3.24,18.18 7.16,20.93 12,20.93C16.84,20.93 20.76,18.2 20.76,14.81C20.76,14.6 20.75,14.37 20.71,14.15C21.46,13.81 22,13.04 22,12.14M7,13.7C7,12.84 7.68,12.14 8.54,12.14C9.4,12.14 10.1,12.84 10.1,13.7A1.56,1.56 0 0,1 8.54,15.26C7.68,15.28 7,14.56 7,13.7M15.71,17.84C14.63,18.92 12.59,19 12,19C11.39,19 9.35,18.9 8.29,17.84C8.13,17.68 8.13,17.43 8.29,17.27C8.45,17.11 8.7,17.11 8.86,17.27C9.54,17.95 11,18.18 12,18.18C13,18.18 14.47,17.95 15.14,17.27C15.3,17.11 15.55,17.11 15.71,17.27C15.85,17.43 15.85,17.68 15.71,17.84M15.42,15.28C14.56,15.28 13.86,14.58 13.86,13.72A1.56,1.56 0 0,1 15.42,12.16C16.28,12.16 17,12.86 17,13.72C17,14.56 16.28,15.28 15.42,15.28Z" />
</svg>
Reddit
</button>
<button class="button_full_width" on:click={share_twitter}>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 24 24">
<path d="M22.46,6C21.69,6.35 20.86,6.58 20,6.69C20.88,6.16 21.56,5.32 21.88,4.31C21.05,4.81 20.13,5.16 19.16,5.36C18.37,4.5 17.26,4 16,4C13.65,4 11.73,5.92 11.73,8.29C11.73,8.63 11.77,8.96 11.84,9.27C8.28,9.09 5.11,7.38 3,4.79C2.63,5.42 2.42,6.16 2.42,6.94C2.42,8.43 3.17,9.75 4.33,10.5C3.62,10.5 2.96,10.3 2.38,10C2.38,10 2.38,10 2.38,10.03C2.38,12.11 3.86,13.85 5.82,14.24C5.46,14.34 5.08,14.39 4.69,14.39C4.42,14.39 4.15,14.36 3.89,14.31C4.43,16 6,17.26 7.89,17.29C6.43,18.45 4.58,19.13 2.56,19.13C2.22,19.13 1.88,19.11 1.54,19.07C3.44,20.29 5.7,21 8.12,21C16,21 20.33,14.46 20.33,8.79C20.33,8.6 20.33,8.42 20.32,8.23C21.16,7.63 21.88,6.87 22.46,6Z" />
</svg>
Twitter
</button>
<button class="button_full_width" on:click={share_facebook}>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 24 24">
<path d="M5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3M18,5H15.5A3.5,3.5 0 0,0 12,8.5V11H10V14H12V21H15V14H18V11H15V9A1,1 0 0,1 16,8H18V5Z" />
</svg>
Facebook
</button>
<button class="button_full_width" on:click={share_tumblr}>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 24 24">
<path d="M17,11H13V15.5C13,16.44 13.28,17 14.5,17H17V21C17,21 15.54,21.05 14.17,21.05C10.8,21.05 9.5,19 9.5,16.75V11H7V7C10.07,6.74 10.27,4.5 10.5,3H13V7H17" />
</svg>
Tumblr
</button>
</div>
<style>
.sharebar {
position: absolute;
width: 7em;
left: -8em;
bottom: 0;
top: 0;
overflow-y: scroll;
overflow-x: hidden;
background: var(--shaded_background);
backdrop-filter: blur(4px);
border-top-left-radius: 16px;
border-bottom-left-radius: 16px;
text-align: center;
overflow: hidden;
opacity: 0;
transition: left 0.4s, opacity 0.4s;
}
.visible {
left: calc(8em + 2px);
opacity: 1;
}
.button_full_width {
flex-direction: column;
width: calc(100% - 6px);
}
.button_full_width > svg {
height: 3em;
width: 3em;
fill: currentColor;
}
</style>

View File

@@ -1,78 +0,0 @@
<script>
import { formatDataVolume } from "util/Formatting";
import { stats } from "lib/StatsSocket"
let percent = 0
let title = ""
$: {
if ($stats.limits_init === true) {
if ($stats.limits.transfer_limit === 0) {
percent = 0 // Avoid division by 0
} else if ($stats.limits.transfer_limit_used / $stats.limits.transfer_limit > 1) {
percent = 100
} else {
percent = ($stats.limits.transfer_limit_used / $stats.limits.transfer_limit) * 100
}
title = "Download limit used: " +
formatDataVolume($stats.limits.transfer_limit_used, 3) +
" of " +
formatDataVolume($stats.limits.transfer_limit, 3);
}
}
</script>
<!-- Always show the outer bar to prevent layout shift -->
<div class="progress_bar_outer" title="{title}">
{#if $stats.limits_init}
<div class="progress_bar_text">
{title}
</div>
<div class="progress_bar_inner" style="width: {percent}%;">
{title}
</div>
{/if}
</div>
<style>
.progress_bar_outer {
position: relative;
display: block;
width: 100%;
/* the font-size is two pixels smaller than the progress bar, this leaves
one px margin top and bottom */
height: 18px;
font-size: 15px;
line-height: 18px;
overflow: hidden;
}
.progress_bar_inner {
position: absolute;
display: block;
background: var(--highlight_background);
height: 100%;
width: 0;
transition: width 5s linear;
/* Welcome to Hacktown! What's happening here is that the text in the
progress bar and the text behind the progress bar are perfectly aligned. The
text in the background is dark and the text on the foreground is light, this
makes it look like the text changes colour as the progress bar progresses.
The text-align: right makes the text move along with the tip of the progress
bar once the width of the text has been exceeded. */
text-align: right;
overflow: hidden;
white-space: nowrap;
color: var(--highlight_text_color);
padding-right: 4px;
padding-left: 4px;
z-index: 2;
}
.progress_bar_text {
position: absolute;
display: block;
top: 0;
left: 4px;
z-index: 1;
}
</style>

View File

@@ -1,58 +0,0 @@
<script>
import { createEventDispatcher } from "svelte";
import IconBlock from "layout/IconBlock.svelte";
import TextBlock from "layout/TextBlock.svelte"
import FileTitle from "layout/FileTitle.svelte";
let dispatch = createEventDispatcher()
export const set_file = f => file = f
let file = {
id: "",
name: "",
abuse_type: "",
abuse_reporter_name: "",
can_download: false,
icon_href: "",
}
</script>
<FileTitle title={file.name}/>
<TextBlock>
<h2>Unavailable for legal reasons</h2>
<p>
This file has been removed for violating pixeldrain's
<a href="/abuse">content policy</a>. Type of abuse: {file.abuse_type}.
</p>
<p>
{#if file.abuse_reporter_name === "User submitted reports"}
The file was reported by users of pixeldrain with the report button
in the toolbar.
{:else}
The file was reported through pixeldrain's abuse e-mail address.
{/if}
</p>
<p>
Pixeldrain has zero tolerance towards abuse. The IP address this file
originated from has been banned and is no longer able to upload files to
pixeldrain.
</p>
</TextBlock>
{#if file.can_download}
<IconBlock icon_href={file.icon_href}>
This file cannot be shared, but since you are the uploader of the file
you can still download it.
<br/>
<button class="button_highlight" on:click={() => {dispatch("download")}}>
<i class="icon">download</i>
<span>Download</span>
</button>
</IconBlock>
{/if}

View File

@@ -1,126 +0,0 @@
<script>
import { createEventDispatcher, tick } from "svelte";
import BandwidthUsage from "./BandwidthUsage.svelte";
import FileTitle from "layout/FileTitle.svelte";
let dispatch = createEventDispatcher()
export let is_list = false
let file = {
id: "",
name: "",
mime_type: "",
get_href: "",
show_ads: false,
download_speed_limit: 0,
}
$: loop = file.name.includes(".loop.")
let player
let playing = false
let audio_reload = false
export const set_file = async f => {
let same_file = f.id == file.id
file = f
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('play', () => player.play());
navigator.mediaSession.setActionHandler('pause', () => player.pause());
navigator.mediaSession.setActionHandler('stop', () => player.stop());
navigator.mediaSession.setActionHandler('previoustrack', () => dispatch("prev", {}));
navigator.mediaSession.setActionHandler('nexttrack', () => dispatch("next", {}));
navigator.mediaSession.metadata = new MediaMetadata({
title: file.name,
artist: "pixeldrain",
album: "unknown",
});
console.log("updating media session")
}
// When the component receives a new ID the video track does not
// automatically start playing the new video. So we use this little hack to
// make sure that the video is unloaded and loaded when the ID changes
if (!same_file) {
audio_reload = true
await tick()
audio_reload = false
}
}
export const toggle_playback = () => playing ? player.pause() : player.play()
export const toggle_mute = () => player.muted = !player.muted
export const seek = delta => {
// fastseek can be pretty imprecise, so we don't use it for small seeks
// below 5 seconds
if (player.fastSeek && delta > 5) {
player.fastSeek(player.currentTime + delta)
} else {
player.currentTime = player.currentTime + delta
}
}
</script>
<div class="container">
<FileTitle title={file.name}/>
{#if is_list}
<button on:click={() => dispatch("prev") }>
<i class="icon">skip_previous</i>
</button>
{/if}
<button on:click={() => player.currentTime -= 10 }>
<i class="icon">replay_10</i>
</button>
<button on:click={toggle_playback}>
{#if playing}
<i class="icon">pause</i>
{:else}
<i class="icon">play_arrow</i>
{/if}
</button>
<button on:click={() => player.currentTime += 10 }>
<i class="icon">forward_10</i>
</button>
{#if is_list}
<button on:click={() => dispatch("next") }>
<i class="icon">skip_next</i>
</button>
{/if}
<br/><br/>
{#if file.id && !audio_reload}
<!-- svelte-ignore a11y-media-has-caption -->
<audio
bind:this={player}
class="player"
controls
playsinline
autoplay
loop={loop}
on:pause={() => playing = false }
on:play={() => playing = true }
on:ended={() => {dispatch("next", {})}}
>
<source src={file.get_href} type={file.mime_type} />
</audio>
{/if}
<br/><br/>
{#if file.show_ads}
<BandwidthUsage/>
{/if}
</div>
<style>
.container {
width: 100%;
margin: 30px 0 0 0;
padding: 0;
text-align: center;
}
.player {
width: 90%;
}
</style>

View File

@@ -1,31 +0,0 @@
<script lang="ts">
import { formatDataVolume } from "util/Formatting";
import TextBlock from "layout/TextBlock.svelte"
import ProgressBar from "util/ProgressBar.svelte";
import { stats } from "lib/StatsSocket"
</script>
{#if $stats.limits_init}
<TextBlock center={true}>
<p>
You have used
{formatDataVolume($stats.limits.transfer_limit_used, 3)}
of your daily
{formatDataVolume($stats.limits.transfer_limit, 3)}
transfer limit. When the transfer limit is exceeded the download
speed for new downloads will be limited. Exceeding the limit no
longer affects running downloads.
</p>
<p>
<strong>
<a href="/user/prepaid/deposit" target="_blank" class="button button_highlight" rel="noreferrer">
<i class="icon">bolt</i> Upgrade your account
</a>
to disable the transfer limit
</strong>
</p>
<ProgressBar total={$stats.limits.transfer_limit} used={$stats.limits.transfer_limit_used}></ProgressBar>
</TextBlock>
{/if}

View File

@@ -1,38 +0,0 @@
<script>
import { createEventDispatcher } from "svelte";
import BandwidthUsage from "./BandwidthUsage.svelte";
import IconBlock from "layout/IconBlock.svelte";
import FileTitle from "layout/FileTitle.svelte";
import { formatDataVolume } from "util/Formatting";
let dispatch = createEventDispatcher()
export const set_file = f => file = f
let file = {
id: "",
size: 0,
name: "",
mime_type: "",
icon_href: "",
show_ads: false,
download_speed_limit: 0,
}
</script>
<FileTitle title={file.name}/>
<slot></slot>
<IconBlock icon_href={file.icon_href}>
Type: {file.mime_type}<br/>
Size: {formatDataVolume(file.size, 3)}<br/>
No preview is available for this file type. Download to view it locally.
<br/>
<button class="button_highlight" on:click={() => {dispatch("download")}}>
<i class="icon">download</i>
<span>Download</span>
</button>
</IconBlock>
{#if file.show_ads}
<BandwidthUsage/>
{/if}

View File

@@ -1,131 +0,0 @@
<script>
import { tick } from "svelte";
import Spinner from "util/Spinner.svelte";
import Video from "./Video.svelte";
import Audio from "./Audio.svelte";
import Image from "./Image.svelte";
import PDF from "./PDF.svelte";
import Text from "./Text.svelte";
import File from "./File.svelte";
import Abuse from "./Abuse.svelte";
import { file_type } from "file_viewer/FileUtilities.svelte";
import RateLimit from "./RateLimit.svelte";
import Torrent from "./Torrent.svelte";
import { stats } from "lib/StatsSocket"
import Zip from "./Zip.svelte";
import SlowDown from "layout/SlowDown.svelte";
import TextBlock from "layout/TextBlock.svelte";
let viewer
let viewer_type = "loading"
export let is_list = false
let current_file
let premium_download = false
export const set_file = async file => {
if (file.id === "") {
viewer_type = "loading"
return
} else if (file.abuse_type !== "") {
viewer_type = "abuse"
} else if (
file.availability === "file_rate_limited_captcha_required" ||
file.availability === "ip_download_limited_captcha_required"
) {
viewer_type = "rate_limit"
} else if (file.availability === "server_overload_captcha_required") {
viewer_type = "overload"
} else {
viewer_type = file_type(file)
}
console.log("opening file", file)
current_file = file
premium_download = !file.show_ads
// Render the viewer component and set the file type
await tick()
if (viewer) {
viewer.set_file(file)
}
}
export const toggle_playback = () => {
if (viewer && viewer.toggle_playback) {
viewer.toggle_playback()
return true
}
return false
}
export const toggle_mute = () => {
if (viewer && viewer.toggle_mute) {
viewer.toggle_mute()
return true
}
return false
}
export const seek = delta => {
if (viewer && viewer.seek) {
viewer.seek(delta)
}
}
</script>
{#if viewer_type === "loading"}
<div class="center">
<Spinner></Spinner>
</div>
{:else if viewer_type === "abuse"}
<Abuse bind:this={viewer} on:download></Abuse>
{:else if !premium_download && $stats.limits.transfer_limit_used > $stats.limits.transfer_limit}
<SlowDown
on:download
file_size={current_file.size}
file_name={current_file.name}
file_type={current_file.mime_type}
icon_href={current_file.icon_href}
/>
{:else if viewer_type === "overload"}
<File bind:this={viewer} on:download on:reload>
<TextBlock><div class="highlight_yellow">
<p>
Pixeldrain's servers are currently overloaded. There are too
many people downloading too many things. In order to ensure
stability for our paying customers, free users are asked to
complete a CAPTCHA before starting a new download.
</p>
</div></TextBlock>
</File>
{:else if viewer_type === "rate_limit"}
<RateLimit bind:this={viewer} on:download></RateLimit>
{:else if viewer_type === "image"}
<Image bind:this={viewer} is_list={is_list} on:prev on:next on:loading></Image>
{:else if viewer_type === "video"}
<Video bind:this={viewer} is_list={is_list} on:loading on:download on:prev on:next on:reload></Video>
{:else if viewer_type === "audio"}
<Audio bind:this={viewer} is_list={is_list} on:loading on:prev on:next on:reload></Audio>
{:else if viewer_type === "pdf"}
<PDF bind:this={viewer}></PDF>
{:else if viewer_type === "text"}
<Text bind:this={viewer}></Text>
{:else if viewer_type === "torrent"}
<Torrent bind:this={viewer} on:loading on:download />
{:else if viewer_type === "zip"}
<Zip bind:this={viewer} on:loading on:download />
{:else if viewer_type === "file"}
<File bind:this={viewer} on:download on:reload></File>
{/if}
<style>
.center{
position: relative;
display: block;
margin: auto;
width: 100px;
max-width: 100%;
height: 100px;
max-height: 100%;
top: 50%;
transform: translateY(-50%);
}
</style>

View File

@@ -1,123 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from "svelte"
import { swipe_nav } from "lib/SwipeNavigate";
let dispatch = createEventDispatcher()
export const set_file = f => {
file = f
dispatch("loading", true)
}
let file = {
id: "",
name: "",
mime_type: "",
get_href: "",
}
export let is_list = false
let container
let zoom = false
let x, y = 0
let dragging = false
// For some reason the dblclick event is firing twice during testing.. So here's
// an event debouncer
let last_dblclick = 0
const double_click = e => {
let now = Date.now()
if (now - last_dblclick > 500) {
zoom = !zoom
}
last_dblclick = now
}
const mousedown = (e) => {
if (!dragging && e.which === 1 && zoom) {
x = e.pageX
y = e.pageY
dragging = true
e.preventDefault()
e.stopPropagation()
return false
}
}
const mousemove = (e) => {
if (dragging) {
container.scrollLeft = container.scrollLeft - (e.pageX - x)
container.scrollTop = container.scrollTop - (e.pageY - y)
x = e.pageX
y = e.pageY
e.preventDefault()
e.stopPropagation()
return false
}
}
const mouseup = (e) => {
if (dragging) {
dragging = false
e.preventDefault()
e.stopPropagation()
return false
}
}
const on_load = () => dispatch("loading", false)
</script>
<svelte:window on:mousemove={mousemove} on:mouseup={mouseup} />
<div
bind:this={container}
class="container"
class:zoom
use:swipe_nav={{
enabled: !zoom && is_list,
prev: true,
next: true,
on_prev: () => dispatch("prev"),
on_next: () => dispatch("prev"),
}}
>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<img
on:load={on_load}
on:error={on_load}
on:dblclick={double_click}
on:mousedown={mousedown}
class="image"
class:zoom
src={file.get_href}
alt={file.name}
/>
</div>
<style>
.container {
display: flex;
justify-content: center;
height: 100%;
width: 100%;
overflow: hidden;
}
.container.zoom {
overflow: auto;
justify-content: unset;
}
.image {
position: relative;
display: block;
margin: auto;
max-width: 100%;
max-height: 100%;
cursor: pointer;
}
.image.zoom {
max-width: none;
max-height: none;
cursor: move;
}
</style>

View File

@@ -1,24 +0,0 @@
<script>
export const set_file = f => file = f
let file = {
get_href: "",
}
</script>
<iframe
class="container"
src={"/res/misc/pdf-viewer/web/viewer.html?file="+encodeURIComponent(file.get_href)}
title="PDF viewer">
</iframe>
<style>
.container {
position: relative;
display: block;
height: 100%;
width: 100%;
text-align: center;
overflow: hidden;
border: none;
}
</style>

View File

@@ -1,77 +0,0 @@
<script>
import { createEventDispatcher } from "svelte";
import { formatDataVolume } from "util/Formatting";
import { stats } from "lib/StatsSocket"
import IconBlock from "layout/IconBlock.svelte";
import TextBlock from "layout/TextBlock.svelte"
let dispatch = createEventDispatcher()
export const set_file = f => file = f
let file = {
name: "",
mime_type: "",
availability: "",
}
</script>
<TextBlock>
{#if file.availability === "file_rate_limited_captcha_required"}
<h1>
<i class="icon">file_download_off</i>
Hotlink protection enabled
</h1>
<p>
Hotlinking protection has been enabled for this file. This happens
when a file is downloaded many times outside of our file viewer page
(this page). You can find more information about hotlink protection
on the <a href="/about#toc_6">FAQ page</a>.
</p>
{:else if file.availability === "ip_download_limited_captcha_required"}
<h1>
<i class="icon">file_download_off</i>
Download limit reached
</h1>
<p>
You have reached your download limit for today. Without a pixeldrain
account you are limited to downloading {$stats.limits.download_limit} files
or {formatDataVolume($stats.limits.transfer_limit, 3)} per 48 hours. This limit
is counted per IP address, so if you're on a shared network it's
possible that others have also contributed to this limit.
</p>
<p>
In the last 24 hours you have downloaded
{$stats.limits.download_limit_used} files and used
{formatDataVolume($stats.limits.transfer_limit_used, 3)} bandwidth.
</p>
{/if}
<p>
This warning disappears when you have a
<a href="/#pro" target="_blank">
premium account
</a>
or when the uploader of the file enables
<a href="/user/subscription">hotlinking</a> on their Pro account (and
their data cap has not been used up). Using a download manager with a
Pro account is allowed, it will not trigger this warning for other
files.
</p>
<h2>
Continue downloading
</h2>
<p>
The file can be downloaded like usual by clicking the download button.
You will have to complete a CAPTCHA test to prove that you're not a
robot.
</p>
</TextBlock>
<IconBlock icon_href={file.icon_href}>
Name: {file.name}<br/>
Type: {file.mime_type}<br/>
<button on:click={() => {dispatch("download")}}>
<i class="icon">download</i> Download
</button>
<a href="/user/prepaid/deposit" target="_blank" class="button button_highlight">
<i class="icon">bolt</i> Upgrade your account
</a>
</IconBlock>

View File

@@ -1,130 +0,0 @@
<script>
import { tick } from "svelte";
let container
let text_type = ""
export const set_file = file => {
console.log("loading text file", file.id)
if (file.name.endsWith(".md") || file.name.endsWith(".markdown")) {
markdown(file)
} else if (file.name.endsWith(".txt") || file.size > 524288) {
// If the file is larger than 512KiB we do not enable code highlighting
// because it's too slow
text(file)
} else {
code(file)
}
}
let md_container
const markdown = async file => {
text_type = "markdown"
await tick()
fetch("/u/" + file.id + "/preview").then(resp => {
if (!resp.ok) { return Promise.reject(resp.status) }
return resp.text()
}).then(resp => {
md_container.innerHTML = resp
}).catch(err => {
md_container.innerText = "Error loading file: " + err
})
}
let text_pre
const text = async file => {
text_type = "text"
await tick()
if (file.size > 1 << 22) { // File larger than 4 MiB
text_pre.innerText = "File is too large to view online.\nPlease download and view it locally."
return
}
fetch(file.get_href).then(resp => {
if (!resp.ok) { return Promise.reject(resp.status) }
return resp.text()
}).then(resp => {
text_pre.innerText = resp
}).catch(err => {
text_pre.innerText = "Error loading file: " + err
})
}
let code_pre
let prettyprint = false
const code = async file => {
text_type = "code"
await tick()
if (file.size > 1 << 22) { // File larger than 4 MiB
code_pre.innerText = "File is too large to view online.\nPlease download and view it locally."
return
}
fetch(file.get_href).then(resp => {
if (!resp.ok) { return Promise.reject(resp.status) }
return resp.text()
}).then(resp => {
code_pre.innerText = resp
// Load prettyprint script
if (!prettyprint) {
let prettyprint = document.createElement("script")
prettyprint.src = "https://cdn.jsdelivr.net/gh/google/code-prettify@master/loader/run_prettify.js?skin=desert"
container.appendChild(prettyprint)
prettyprint = true
} else {
PR.prettyPrint()
}
}).catch(err => {
code_pre.innerText = "Error loading file: " + err
})
}
</script>
<div bind:this={container} class="container">
{#if text_type === "markdown"}
<section bind:this={md_container} class="md">
Loading...
</section>
{:else if text_type === "text"}
<pre bind:this={text_pre}>
Loading...
</pre>
{:else if text_type === "code"}
<pre bind:this={code_pre} class="pre-container prettyprint linenums">
Loading...
</pre>
{/if}
</div>
<style>
.container {
background: var(--body_color);
text-align: left;
height: 100%;
width: 100%;
line-height: 1.5em;
overflow-y: auto;
overflow-x: hidden;
}
.container > pre {
margin: 0;
padding: 10px;
white-space: pre-wrap;
overflow: hidden;
border: none;
font-size: 0.9em;
word-break: break-word;
}
.container > .md {
display: block;
padding: 10px;
margin: auto;
text-align: justify;
}
</style>

View File

@@ -1,182 +0,0 @@
<script>
import { createEventDispatcher } from "svelte";
import Magnet from "icons/Magnet.svelte";
import { formatDate } from "util/Formatting"
import IconBlock from "layout/IconBlock.svelte";
import TextBlock from "layout/TextBlock.svelte";
import TorrentItem from "./TorrentItem.svelte"
import FileTitle from "layout/FileTitle.svelte";
import CopyButton from "layout/CopyButton.svelte";
let dispatch = createEventDispatcher()
let status = "loading"
export const set_file = async f => {
file = f
dispatch("loading", true)
try {
let resp = await fetch(f.info_href+"/torrent")
if (resp.status >= 400) {
let json = await resp.json()
if (json.value === "torrent_too_large") {
status = "too_large"
return
} else {
status = "parse_failed"
return
}
}
torrent = await resp.json()
// Generate magnet link
magnet = "magnet:?xt=urn:btih:" + torrent.info_hash +
"&dn=" + encodeURIComponent(Object.keys(torrent.files.children)[0])
torrent.trackers.forEach(tracker => {
magnet += "&tr="+encodeURIComponent(tracker)
})
} catch (err) {
console.error(err)
} finally {
dispatch("loading", false)
}
status = "finished"
}
let file = {
id: "",
size: 0,
name: "",
mime_type: "",
icon_href: "",
show_ads: false,
}
let torrent = {
trackers: [],
comment: "",
created_by: "",
created_at: "",
info_hash: "",
files: null,
}
let magnet = ""
</script>
<FileTitle title={file.name}/>
<IconBlock icon_href={file.icon_href}>
{#if status === "finished"}
Created by: {torrent.created_by}<br/>
Comment: {torrent.comment}<br/>
Created at: {formatDate(new Date(torrent.created_at), true, true, true)}<br/>
Info hash: {torrent.info_hash}<br/>
<a href={magnet} class="button button_highlight">
<Magnet/>
<span>Open magnet link</span>
</a>
<CopyButton text={magnet}>Copy magnet link</CopyButton>
{:else if status === "too_large"}
<p>
Torrent file is too large to parse. Please download the file and
add it to your torrent client locally.
</p>
{:else if status === "parse_failed"}
<p>
Torrent file could not be parsed. It may be corrupted.
</p>
{/if}
<button on:click={() => {dispatch("download")}} class="button">
<i class="icon">download</i>
<span>Download torrent file</span>
</button>
</IconBlock>
<TextBlock>
<details>
<summary>How do I download this? (expand for more information)</summary>
<p>
This is a torrent file, which means you will need a torrent client to
download it. Here are some good torrent clients for various platforms:
</p>
<ul>
<li><a href="https://transmissionbt.com/download">Transmission</a> (Linux, Mac, Windows)</li>
<li><a href="https://www.qbittorrent.org/download">qBittorrent</a> (Linux, Mac, Windows)</li>
<li><a href="https://play.google.com/store/apps/details?id=org.proninyaroslav.libretorrent">LibreTorrent</a> (Android)</li>
</ul>
<p>
After installing your torrent client you will be able to use the
<a href={magnet}><Magnet/> Open magnet link</a>
button to download the files in your torrent client.
</p>
<h3>What is a torrent?</h3>
<p>
<a href="https://wikipedia.org/wiki/BitTorrent">BitTorrent</a> is a
peer-to-peer network for sharing files. This torrent file does not
actually contain the files listed below, instead it contains
instructions for your torrent client to download the files from
other people who happen to be downloading the same files currently.
This means that instead of connecting to a single server (like
pixeldrain), you will be connecting to other people on the internet
to download these files.
</p>
<p>
Torrents are a highly efficient and free method of transferring
files over the internet. Since the bandwidth is shared directly
between users there is no need for expensive servers to host the
files for you.
</p>
<h3>Is this safe?</h3>
<p>
Your torrent client will make sure that the files you receive from
your peers are actually what they say it is. This makes it just as
safe as any other form of downloading. Like always when downloading
files you still need to be aware of what you are downloading. Don't
just blindly trust any file anyone sends you.
</p>
<h3>Is it private?</h3>
<p>
When downloading a torrent file you will be part of the so-called
'torrent swarm'. Anyone in the swarm can see each other's IP
addresses. This is not a bad thing on its own, but there a few cases
in which this can be abused.
</p>
<p>
Anyone in the swarm will be able to see what you are downloading,
even across different torrents. This is something to keep in mind
when downloading torrents. If someone can link your IP address to
your identity then there are ways to find out which files you have
downloaded in the past (provided that your IP address has not
changed since then).
</p>
<p>
If you are downloading copyrighted material (which I do not condone)
then rightsholders will be able to see your IP address. In most
cases this is not a problem because your ISP will still protect your
identity. But there are some countries (notably the USA) where your
ISP will not respect your right to privacy and the rightsholder will
be able to contact you. If this worries you then you should look
into VPN services to protect your privacy, like <a
href="https://mullvad.net">Mullvad</a>.
</p>
</details>
</TextBlock>
{#if status === "finished"}
<TextBlock>
<h2>Files in this torrent</h2>
<TorrentItem item={torrent.files} />
</TextBlock>
{/if}
<style>
summary {
cursor: pointer;
border-bottom: 1px solid var(--separator);
}
</style>

View File

@@ -1,28 +0,0 @@
<script>
import { formatDataVolume } from "util/Formatting";
export let item = {
size: 0,
children: null,
}
</script>
<ul class="list_open">
{#each Object.entries(item.children) as [name, child]}
<li class:list_closed={!child.children}>
{name} ({formatDataVolume(child.size, 3)})<br/>
{#if child.children}
<svelte:self item={child}></svelte:self>
{/if}
</li>
{/each}
</ul>
<style>
.list_open {
list-style-type: disclosure-open;
}
.list_closed {
list-style-type: disc;
}
</style>

View File

@@ -1,274 +0,0 @@
<script>
import { onMount, createEventDispatcher, tick } from "svelte";
import { video_position } from "lib/VideoPosition";
import BandwidthUsage from "./BandwidthUsage.svelte";
import IconBlock from "layout/IconBlock.svelte";
let dispatch = createEventDispatcher()
export let is_list = false
let file = {
id: "",
size: 0,
name: "",
mime_type: "",
get_href: "",
icon_href: "",
allow_video_player: true,
show_ads: false,
download_speed_limit: 0,
}
$: loop = file.name.includes(".loop.")
let player
let playing = false
let video_reload = false
let media_session = false
export const set_file = async f => {
let same_file = f.id == file.id
file = f
if (media_session) {
navigator.mediaSession.metadata = new MediaMetadata({
title: file.name,
artist: "pixeldrain",
album: "unknown",
});
console.log("updating media session")
}
// When the component receives a new ID the video track does not
// automatically start playing the new video. So we use this little hack to
// make sure that the video is unloaded and loaded when the ID changes
if (!same_file) {
video_reload = true
await tick()
video_reload = false
}
}
export const toggle_playback = () => playing ? player.pause() : player.play()
export const toggle_mute = () => player.muted = !player.muted
export const seek = delta => {
// fastseek can be pretty imprecise, so we don't use it for small seeks
// below 5 seconds
if (player.fastSeek && delta > 5) {
player.fastSeek(player.currentTime + delta)
} else {
player.currentTime = player.currentTime + delta
}
}
onMount(() => {
if ('mediaSession' in navigator) {
media_session = true
navigator.mediaSession.setActionHandler('play', () => player.play());
navigator.mediaSession.setActionHandler('pause', () => player.pause());
navigator.mediaSession.setActionHandler('stop', () => player.stop());
navigator.mediaSession.setActionHandler('previoustrack', () => dispatch("prev", {}));
navigator.mediaSession.setActionHandler('nexttrack', () => dispatch("next", {}));
}
})
const download = () => { dispatch("download", {}) }
const fullscreen = () => {
if (document.fullscreenElement === null) {
player.requestFullscreen()
} else {
document.exitFullscreen()
}
}
const keypress = e => {
if (
(e.ctrlKey || e.altKey || e.metaKey) ||
(document.activeElement.type && (
document.activeElement.type === "text" ||
document.activeElement.type === "email" ||
document.activeElement.type === "textarea"))
) {
// The first check is to prevent our keybindings from triggering then
// the user uses a global keybind. The second check is to prevent the
// shortcuts from firing if the user is entering text in an input field
return
}
if (e.key === "f") {
fullscreen()
}
}
const video_keydown = e => {
if (e.key === " ") {
// Prevent spacebar from pausing playback in Chromium. This conflicts
// with our own global key handler, causing the video to immediately
// pause again after unpausing.
e.stopPropagation()
}
}
</script>
<svelte:window on:keypress={keypress} />
{#if file.allow_video_player}
{#if !video_reload}
<div class="container">
{#if
file.mime_type === "video/x-matroska" ||
file.mime_type === "video/quicktime" ||
file.mime_type === "video/x-ms-asf"
}
<div class="compatibility_warning">
This video file type is not compatible with every web
browser. If the video fails to play you can try downloading
the video and watching it locally.
</div>
{/if}
<div class="player_and_controls">
<!-- svelte-ignore a11y-media-has-caption -->
<video
bind:this={player}
controls
playsinline
loop={loop}
class="player"
on:pause={() => playing = false }
on:play={() => playing = true }
on:ended={() => dispatch("next", {})}
on:keydown={video_keydown}
use:video_position={() => file.id}
>
<source src={file.get_href} type={file.mime_type} />
</video>
<div class="controls">
<div class="spacer"></div>
{#if is_list}
<button on:click={() => dispatch("prev") }>
<i class="icon">skip_previous</i>
</button>
{/if}
<button on:click={() => seek(-10)}>
<i class="icon">replay_10</i>
</button>
<button on:click={toggle_playback} class="button_highlight">
{#if playing}
<i class="icon">pause</i>
{:else}
<i class="icon">play_arrow</i>
{/if}
</button>
<button on:click={() => seek(10)}>
<i class="icon">forward_10</i>
</button>
{#if is_list}
<button on:click={() => dispatch("next") }>
<i class="icon">skip_next</i>
</button>
{/if}
<div style="width: 16px; height: 8px;"></div>
<button on:click={toggle_mute} class:button_red={player && player.muted}>
{#if player && player.muted}
<i class="icon">volume_off</i>
{:else}
<i class="icon">volume_up</i>
{/if}
</button>
<button on:click={fullscreen}>
<i class="icon">fullscreen</i>
</button>
<div class="spacer"></div>
</div>
</div>
</div>
{/if}
{:else}
<h1>{file.name}</h1>
<IconBlock icon_href={file.icon_href}>
The online video player on pixeldrain is only available while logged in
to an account, or if the uploading user has verified their e-mail
address. You can still download the video and watch it locally on your
computer without an account.
<br/>
<button on:click={download}>
<i class="icon">download</i> Download
</button>
<a href="/login" class="button">
<i class="icon">login</i> Log in
</a>
<a href="/register" class="button">
<i class="icon">how_to_reg</i> Sign up
</a>
</IconBlock>
{#if file.show_ads}
<BandwidthUsage/>
{/if}
{/if}
<style>
h1 {
text-shadow: 1px 1px 3px var(--shadow_color);
line-break: anywhere;
}
.container {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
.player_and_controls {
flex: 1 1 0;
display: flex;
flex-direction: column;
overflow: auto;
height: 100%;
width: 100%;
}
.player {
flex: 1 1 auto;
display: flex;
justify-content: center;
text-align: center;
overflow: hidden;
}
.controls {
flex: 0 0 auto;
display: flex;
flex-direction: row;
overflow: auto;
background-color: var(--shaded_background);
backdrop-filter: blur(4px);
padding: 0 2px 2px 2px;
align-items: center;
}
.controls > * {
flex: 0 0 auto;
}
.controls > .spacer {
flex: 1 1 auto;
}
@media(max-height: 500px) {
.player_and_controls {
flex-direction: row;
}
.controls {
flex-direction: column;
}
}
.compatibility_warning {
background-color: var(--shaded_background);
backdrop-filter: blur(4px);
border-bottom: 2px solid #6666FF;
padding: 4px;
}
</style>

View File

@@ -1,126 +0,0 @@
<script>
import { createEventDispatcher } from "svelte";
import { formatDataVolume, formatDate } from "util/Formatting"
import IconBlock from "layout/IconBlock.svelte";
import TextBlock from "layout/TextBlock.svelte"
import ZipItem from "./ZipItem.svelte";
import BandwidthUsage from "./BandwidthUsage.svelte";
import FileTitle from "layout/FileTitle.svelte";
let dispatch = createEventDispatcher()
let status = "loading"
let file = {
name: "",
mime_type: "",
size: 0,
date_upload: "",
icon_href: ""
}
let zip = {
download_url: "",
size: 0,
children: null,
}
let comp_ratio = 0
let archive_type = ""
let truncated = false
export const set_file = async f => {
file = f
dispatch("loading", true)
if (f.mime_type === "application/zip") {
archive_type = "zip"
} else if (f.mime_type === "application/x-7z-compressed") {
archive_type = "7z"
}
try {
let resp = await fetch(f.info_href+"/zip")
if (resp.status >= 400) {
status = "parse_failed"
return
}
zip = await resp.json()
// Check if the zip has the property which allows separate files to be
// downloaded. If so then we set the download URL for each file
if (zip.properties !== undefined) {
if (zip.properties.includes("read_individual_files")) {
// Set the download URL for each file in the zip
recursive_set_url(f.info_href+"/zip", zip)
}
truncated = zip.properties.includes("truncated")
}
comp_ratio = (zip.size / file.size)
} catch (err) {
console.error(err)
} finally {
dispatch("loading", false)
}
status = "finished"
}
const recursive_set_url = (parent_path, file) => {
file.download_url = parent_path
if (file.children) {
Object.entries(file.children).forEach(child => {
recursive_set_url(file.download_url + "/" +child[0], child[1])
});
}
}
</script>
<FileTitle title={file.name}/>
<IconBlock icon_href={file.icon_href}>
{#if archive_type === "7z"}
This is a 7-zip archive. You will need
<a href="https://www.7-zip.org/">7-zip</a> or compatible software to
extract it<br/>
{/if}
Compressed size: {formatDataVolume(file.size, 3)}<br/>
{#if !truncated}
Uncompressed size: {formatDataVolume(zip.size, 3)} (Ratio: {comp_ratio.toFixed(2)}x)<br/>
{/if}
Uploaded on: {formatDate(file.date_upload, true, true, true)}
<br/>
<button class="button_highlight" on:click={() => {dispatch("download")}}>
<i class="icon">download</i>
<span>Download</span>
</button>
</IconBlock>
{#if file.show_ads}
<BandwidthUsage/>
{/if}
{#if status === "finished"}
<TextBlock>
<h2>Files in this archive</h2>
{#if truncated}
<div class="highlight_yellow">
Due to the large size of this archive, the results have been
truncated. The list below is incomplete!
</div>
{/if}
<ZipItem item={zip} />
</TextBlock>
{:else if status === "parse_failed"}
<TextBlock>
<p>
Zip archive could not be parsed. This usually means that the archive
is encrypted or that it uses an unsupported compression format.
</p>
</TextBlock>
{/if}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { fs_encode_path } from "./FilesystemAPI";
import { fs_encode_path, node_is_shared } from "./FilesystemAPI";
import type { FSNavigator } from "./FSNavigator";
export let nav: FSNavigator
@@ -7,7 +7,6 @@ export let nav: FSNavigator
<div class="breadcrumbs">
{#each $nav.path as node, i (node.path)}
{@const shared = node.id !== undefined && node.id !== "me"}
<a
href={"/d"+fs_encode_path(node.path)}
class="breadcrumb button"
@@ -16,7 +15,7 @@ export let nav: FSNavigator
>
{#if node.abuse_type !== undefined}
<i class="icon small">block</i>
{:else if shared}
{:else if node_is_shared(node)}
<i class="icon small">share</i>
{/if}
<div class="node_name" class:base={$nav.base_index === i}>

View File

@@ -11,8 +11,6 @@ import { fs_download, type FSPath } from "./FilesystemAPI";
import Menu from "./Menu.svelte";
import { FSNavigator } from "./FSNavigator"
import { writable } from "svelte/store";
import TransferLimit from "file_viewer/TransferLimit.svelte";
import { stats } from "lib/StatsSocket"
import { css_from_path } from "filesystem/edit_window/Branding";
import AffiliatePrompt from "user_home/AffiliatePrompt.svelte";
@@ -160,21 +158,6 @@ const keydown = (e: KeyboardEvent) => {
</div>
</div>
{#if $nav.context.premium_transfer === false}
<div class="download_limit">
{#if $stats.limits.transfer_limit_used > $stats.limits.transfer_limit}
<div class="highlight_yellow">
Your free download limit has been used up and your download
speed has been limited to 1 MiB/s. <a href="/#pro"
target="_blank">Upgrade to premium</a> to continue fast
downloading
</div>
{:else}
<TransferLimit/>
{/if}
</div>
{/if}
<DetailsWindow nav={nav} bind:visible={details_visible} />
<EditWindow nav={nav} bind:this={edit_window} bind:visible={edit_visible} />
@@ -239,16 +222,6 @@ const keydown = (e: KeyboardEvent) => {
overflow: hidden;
}
/* Download limit gauge (row 3) */
.download_limit {
flex: 0 0 auto;
display: flex;
flex-direction: column;
text-align: center;
background-color: var(--shaded_background);
backdrop-filter: blur(4px);
}
/* This max-width needs to be synced with the .toolbar max-width in
Toolbar.svelte and the .label max-width in FileStats.svelte */
@media (max-width: 1000px) {

View File

@@ -15,34 +15,41 @@ export type FSPath = {
}
export type FSNode = {
type: string,
path: string,
name: string,
created: string,
modified: string,
mode_string: string,
mode_octal: string,
created_by: string,
type: string
path: string
name: string
created: string
modified: string
mode_string: string
mode_octal: string
created_by: string
abuse_type?: string,
abuse_report_time?: string,
abuse_type?: string
abuse_report_time?: string
custom_domain_name?: string,
custom_domain_name?: string
file_size: number,
file_type: string,
sha256_sum: string,
file_size: number
file_type: string
sha256_sum: string
id?: string,
properties?: FSNodeProperties,
link_permissions?: FSPermissions,
user_permissions?: { [index: string]: FSPermissions },
password_permissions?: { [index: string]: FSPermissions },
id?: string
properties?: FSNodeProperties
link_permissions?: FSPermissions
user_permissions?: { [index: string]: FSPermissions }
password_permissions?: { [index: string]: FSPermissions }
// Added by us
// Indicates whether the file is selected in the file manager
fm_selected?: boolean,
fm_selected?: boolean
}
export const node_is_shared = (node: FSNode): boolean => {
if (node.link_permissions !== undefined && node.link_permissions.read) {
return true
}
return false
}
export type FSNodeProperties = {
@@ -81,7 +88,6 @@ export type NodeOptions = {
mode?: number,
created?: string,
modified?: string,
shared?: boolean,
// Permissions
link_permissions?: FSPermissions,
@@ -316,7 +322,7 @@ export const fs_node_type = (node: FSNode) => {
export const fs_node_icon = (node: FSNode, width = 64, height = 64) => {
if (node.type === "dir") {
// Folders with an ID are publically shared, use the shared folder icon
if (node.id) {
if (node_is_shared(node)) {
return "/res/img/mime/folder-remote.png"
} else {
return "/res/img/mime/folder.png"

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { FSNavigator } from "./FSNavigator";
import { fs_node_icon, fs_share_hotlink_url, fs_share_url, fs_update, type FSNode } from "./FilesystemAPI";
import { fs_node_icon, fs_share_hotlink_url, fs_share_url, fs_update, node_is_shared, type FSNode, type FSPermissions } from "./FilesystemAPI";
import { copy_text } from "util/Util.svelte";
import CopyButton from "layout/CopyButton.svelte";
import Dialog from "layout/Dialog.svelte";
@@ -36,11 +36,16 @@ export const open = async (e: MouseEvent, p: FSNode[]) => {
}
const make_public = async () => {
base = await fs_update(base.path, {shared: true})
await nav.reload()
if (!node_is_shared(base)) {
base = await fs_update(
base.path,
{link_permissions: {read: true} as FSPermissions},
)
await nav.reload()
// Insert the new FSNode into the path
path[path.length-1] = base
// Insert the new FSNode into the path
path[path.length-1] = base
}
}
const share = async () => {

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import ThemePresets from "./ThemePresets.svelte";
import { fs_update, fs_node_type, type FSNode, type NodeOptions } from "filesystem/FilesystemAPI";
import { fs_update, fs_node_type, type FSNode, type NodeOptions, node_is_shared, type FSPermissions } from "filesystem/FilesystemAPI";
import CustomBanner from "filesystem/viewers/CustomBanner.svelte";
import HelpButton from "layout/HelpButton.svelte";
import FilePicker from "filesystem/filemanager/FilePicker.svelte";
@@ -32,8 +32,7 @@ const handle_picker = async (e: CustomEvent<FSNode[]>) => {
alert("Please select one file")
return
}
const f = e.detail[0]
let file_id = f.id
let f = e.detail[0]
if (fs_node_type(f) !== "image") {
alert("Please select an image file")
@@ -44,19 +43,21 @@ const handle_picker = async (e: CustomEvent<FSNode[]>) => {
}
// If this image is not public, it will be made public
if (file_id === undefined || file_id === "") {
if (!node_is_shared(f)) {
try {
let new_file = await fs_update(e.detail[0].path, {shared: true})
file_id = new_file.id
f = await fs_update(
e.detail[0].path,
{link_permissions: {read: true} as FSPermissions},
)
} catch (err) {
alert(err)
}
}
if (picking === "brand_header_image") {
options.brand_header_image = file_id
options.brand_header_image = f.id
} else if (picking === "brand_background_image") {
options.brand_background_image = file_id
options.brand_background_image = f.id
}
}

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { fs_encode_path, fs_node_icon } from "filesystem/FilesystemAPI"
import { fs_encode_path, fs_node_icon, node_is_shared } from "filesystem/FilesystemAPI"
import type { FSNavigator } from "filesystem/FSNavigator";
import { FileAction } from "./FileManagerLib";
@@ -26,7 +26,7 @@ export let hide_edit = false
<div class="node_name">
{child.name}
</div>
{#if child.id}
{#if node_is_shared(child)}
<a
href="/d/{child.id}"
on:click={e => dispatch("file", {index: index, action: FileAction.Share, original: e})}

View File

@@ -1,41 +0,0 @@
<script lang="ts">
import FilePicker from "file_viewer/FilePicker.svelte";
import { fs_import } from "filesystem/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator";
export let nav: FSNavigator
let file_picker: FilePicker
export const open = () => file_picker.open()
// TODO: Give files a proper type
const import_files = async (files: any) => {
nav.set_loading(true)
let fileids = []
files.forEach(file => {
fileids.push(file.id)
})
try {
await fs_import(nav.base.path, fileids)
} catch (err) {
if (err.message) {
alert(err.message)
} else {
console.error(err)
alert(err)
}
return
} finally {
nav.reload()
}
}
</script>
<FilePicker
bind:this={file_picker}
on:files={e => {import_files(e.detail)}}
multi_select={true}
title="Import files from file list">
</FilePicker>

View File

@@ -6,7 +6,6 @@ import ListView from "./ListView.svelte"
import GalleryView from "./GalleryView.svelte"
import CompactView from "./CompactView.svelte"
import Button from "layout/Button.svelte";
import FileImporter from "./FileImporter.svelte";
import { formatDate } from "util/Formatting";
import { drop_target } from "lib/DropTarget"
import SearchBar from "./SearchBar.svelte";
@@ -24,7 +23,6 @@ let uploader: FsUploadWidget
let mode = "viewing"
let creating_dir = false
let show_hidden = false
let file_importer: FileImporter
export const upload = (files: File[]) => {
return uploader.upload(files)
@@ -303,8 +301,6 @@ onMount(() => {
>
<div class="width_container">
{#if mode === "viewing"}
<SearchBar nav={nav}/>
<div class="toolbar">
<div class="toolbar_left">
<button on:click={navigate_back} title="Back">
@@ -350,16 +346,15 @@ onMount(() => {
<Button click={() => {creating_dir = !creating_dir}} highlight={creating_dir} icon="create_new_folder" title="Make folder"/>
<button on:click={() => file_importer.open()} title="Import files from list">
<i class="icon">move_to_inbox</i>
</button>
<button on:click={selecting_mode} title="Select and delete files">
<i class="icon">select_all</i>
</button>
{/if}
</div>
</div>
<SearchBar nav={nav}/>
{:else if mode === "selecting"}
<div class="toolbar toolbar_edit">
<Button click={viewing_mode} icon="close"/>
@@ -415,8 +410,6 @@ onMount(() => {
{/if}
</div>
<FileImporter nav={nav} bind:this={file_importer} />
<style>
.container {
height: 100%;

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { formatDataVolume } from "util/Formatting";
import { fs_encode_path, fs_node_icon } from "filesystem/FilesystemAPI"
import { fs_encode_path, fs_node_icon, node_is_shared } from "filesystem/FilesystemAPI"
import type { FSNavigator } from "filesystem/FSNavigator";
import SortButton from "layout/SortButton.svelte";
import { FileAction } from "./FileManagerLib";
@@ -46,7 +46,7 @@ export let hide_branding = false
<div class="icons_wrap">
{#if child.abuse_type !== undefined}
<i class="icon" title="This file / directory has received an abuse report. It cannot be shared">block</i>
{:else if child.id}
{:else if node_is_shared(child)}
<a
href="/d/{child.id}"
on:click={e => dispatch("file", {index: index, action: FileAction.Share, original: e})}

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { onMount, tick } from "svelte";
import Spinner from "util/Spinner.svelte";
import { fs_node_type, fs_thumbnail_url } from "filesystem/FilesystemAPI";
import { fs_node_type } from "filesystem/FilesystemAPI";
import FileManager from "filesystem/filemanager/FileManager.svelte";
import Audio from "./Audio.svelte";
import File from "./File.svelte";
@@ -12,8 +12,6 @@ import Video from "./Video.svelte";
import Torrent from "./Torrent.svelte";
import Zip from "./Zip.svelte";
import CustomBanner from "./CustomBanner.svelte";
import { stats } from "lib/StatsSocket"
import SlowDown from "layout/SlowDown.svelte";
import type { FSNavigator } from "filesystem/FSNavigator";
import FsUploadWidget from "filesystem/upload_widget/FSUploadWidget.svelte";
import EditWindow from "filesystem/edit_window/EditWindow.svelte";
@@ -82,14 +80,6 @@ export const seek = (delta: number) => {
<FileManager nav={nav} upload_widget={upload_widget} edit_window={edit_window}>
<CustomBanner path={$nav.path}/>
</FileManager>
{:else if $nav.context.premium_transfer === false && $stats.limits.transfer_limit_used > $stats.limits.transfer_limit}
<SlowDown
on:download
file_size={$nav.base.file_size}
file_name={$nav.base.name}
file_type={$nav.base.file_type}
icon_href={fs_thumbnail_url($nav.base.path, 256, 256)}
/>
{:else if viewer_type === "audio"}
<Audio nav={nav} bind:this={viewer}>
<CustomBanner path={$nav.path}/>
@@ -99,7 +89,7 @@ export const seek = (delta: number) => {
{:else if viewer_type === "video"}
<Video nav={nav} bind:this={viewer} on:open_sibling/>
{:else if viewer_type === "pdf"}
<Pdf nav={nav}/>
<Pdf nav={nav} bind:this={viewer}/>
{:else if viewer_type === "text"}
<Text nav={nav} bind:this={viewer}>
<CustomBanner path={$nav.path}/>

View File

@@ -7,7 +7,7 @@ export let nav: FSNavigator
<iframe
class="container"
src={"/res/misc/pdf-viewer/web/viewer.html?file="+encodeURIComponent(fs_path_url($nav.base.path))}
src={"/res/misc/pdf-viewer/web/viewer.html?file="+fs_path_url($nav.base.path)}
title="PDF viewer">
</iframe>

View File

@@ -10,7 +10,7 @@ export type ZipEntry = {
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { formatDataVolume, formatDate } from "util/Formatting"
import ZipItem from "file_viewer/viewers/ZipItem.svelte";
import ZipItem from "filesystem/viewers/ZipItem.svelte";
import IconBlock from "layout/IconBlock.svelte";
import TextBlock from "layout/TextBlock.svelte"
import { fs_node_icon, fs_path_url } from "filesystem/FilesystemAPI";

View File

@@ -196,9 +196,9 @@ import OtherPlans from "./OtherPlans.svelte";
</div>
<div class="feature_cell prepaid_feat">
<span class="bold">PayPal</span>,
<span class="bold">Credit/debit</span>,
<span class="bold">iDEAL</span><br/>
And many regional providers
<span class="bold">Bitcoin</span>,
<span class="bold">Litecoin</span>,
<span class="bold">Monero</span>
</div>
<div></div>

View File

@@ -20,50 +20,38 @@ let upload_widget
<AddressReputation/>
<div class="page_content">
{#if window.user && window.user.username && window.user.username !== ""}
<div
class="drop_target"
use:drop_target={{
upload: (files) => upload_widget.upload_files(files),
shadow: "var(--highlight_color) 0 0 10px 2px inset",
}}
>
<UploadWidget bind:this={upload_widget}/>
</div>
{:else}
<section>
<p>
Pixeldrain offers services for efficiently moving and storing
digital files on the internet.
</p>
<h2>What pixeldrain is good at</h2>
<ul>
<li>
Serving large files to millions of people worldwide
</li>
<li>
Storing files for less money than all the competition
</li>
</ul>
<h2>Things we take very seriously</h2>
<ul>
<li>
<b>Performance</b> - Slow software is a waste of time. We
don't want to make you wait, so pixeldrain is completely
tuned for maximum performance
</li>
<li>
<b>Privacy</b> - There is too much tracking on the web
nowadays. Pixeldrain goes in the other direction, this site
does not contain any advertisements or third party tracking
scripts
</li>
<li>
Bullet lists
</li>
</ul>
</section>
{/if}
<section>
<p>
Pixeldrain offers services for efficiently moving and storing
digital files on the internet.
</p>
<h2>What pixeldrain is good at</h2>
<ul>
<li>
Serving large files to millions of people worldwide
</li>
<li>
Storing files for less money than all the competition
</li>
</ul>
<h2>Things we take very seriously</h2>
<ul>
<li>
<b>Performance</b> - Slow software is a waste of time. We
don't want to make you wait, so pixeldrain is completely
tuned for maximum performance
</li>
<li>
<b>Privacy</b> - There is too much tracking on the web
nowadays. Pixeldrain goes in the other direction, this site
does not contain any advertisements or third party tracking
scripts
</li>
<li>
Bullet lists
</li>
</ul>
</section>
</div>
<header>
@@ -211,9 +199,6 @@ header > span {
max-width: 100%;
border-radius: 12px;
}
.drop_target {
border-radius: 8px;
}
.bold {
font-weight: bold;
color: var(--highlight_color);

View File

@@ -0,0 +1,172 @@
<script>
import { onMount } from "svelte";
import Euro from "util/Euro.svelte";
import ProgressBar from "util/ProgressBar.svelte";
let pixeldrain_storage = 0
let pixeldrain_egress = 0
let pixeldrain_total = 0
let backblaze_storage = 0
let backblaze_egress = 0
let backblaze_api = 0
let backblaze_total = 0
let wasabi_storage = 0
let wasabi_total = 0
let price_amazon = 0
let price_azure = 0
let price_google = 0
let price_max = 0
let storage = 10 // TB
let egress = 10 // TB
let avg_file_size = 1000 // kB
$: {
pixeldrain_storage = storage * 4
pixeldrain_egress = egress * 1
pixeldrain_total = pixeldrain_storage + pixeldrain_egress
// Egress at Backblaze is free up to three times the amount of storage, then
// it's $10/TB
backblaze_storage = storage * 6
backblaze_egress = Math.max(egress - (storage * 3), 0) * 10
backblaze_api = ((egress * 1e12) / (avg_file_size * 1e3)) * 0.0000004
backblaze_total = backblaze_storage + backblaze_egress + backblaze_api
// Wasabi does not have egress fees
wasabi_storage = storage * 6.99
wasabi_total = (egress > storage) ? 0 : wasabi_storage
// price_amazon = (storage * 26) + (egress * 90)
// price_azure = (storage * 20) + (egress * 80)
// price_google = (storage * 20) + (egress * 20)
price_max = Math.max(pixeldrain_total, backblaze_total, wasabi_total, price_amazon, price_azure, price_google)
}
onMount(() => {})
</script>
<h2>Price calculator</h2>
<div class="inputs">
<div>
<div>Storage</div>
<div><input type="number" bind:value={storage}> TB / month</div>
</div>
<div>
<div>Egress</div>
<div><input type="number" bind:value={egress}> TB / month</div>
</div>
<div>
<div>Average file size</div>
<div><input type="number" bind:value={avg_file_size}> kB</div>
</div>
</div>
<div class="bars">
<div>
<div>
Pixeldrain - <Euro amount={pixeldrain_total*1e6}/> / month<br/>
<Euro amount={pixeldrain_storage*1e6}/> storage,
<Euro amount={pixeldrain_egress*1e6}/> egress
<ProgressBar used={pixeldrain_total} total={price_max}/>
</div>
</div>
<div>
<div>
Backblaze B2 - <Euro symbol="$" amount={backblaze_total*1e6}/> / month<br/>
<Euro symbol="$" amount={backblaze_storage*1e6}/> storage,
<Euro symbol="$" amount={backblaze_egress*1e6}/> egress,
<Euro symbol="$" amount={backblaze_api*1e6}/> API calls
<ProgressBar used={backblaze_total} total={price_max}/>
</div>
</div>
<div>
<div>
Wasabi - <Euro symbol="$" amount={wasabi_total*1e6}/> / month<br/>
<Euro symbol="$" amount={wasabi_storage*1e6}/> storage
<ProgressBar used={wasabi_total} total={price_max}/>
</div>
{#if egress > storage}
<div>
Wasabi does not allow users to download more than their amount
of stored data per month. For this reason Wasabi is excluded
from price calculations when egress is higher than storage.
</div>
{/if}
</div>
<!-- <div>
<div>
Amazon S3: <Euro symbol="$" amount={price_amazon*1e6}/> / month
<ProgressBar used={price_amazon} total={price_max}/>
</div>
<div>
Amazon's pricing is too complicated to accurately represent here.
</div>
</div>
<div>
<div>
Microsoft Azure: <Euro symbol="$" amount={price_azure*1e6}/> / month
<ProgressBar used={price_azure} total={price_max}/>
</div>
<div>
Azure's pricing is too complicated to accurately represent here.
</div>
</div>
<div>
<div>
Google: <Euro symbol="$" amount={price_google*1e6}/> / month
<ProgressBar used={price_google} total={price_max}/>
</div>
<div>
Google's pricing is too complicated to accurately represent here.
</div>
</div> -->
</div>
<p>
Note that while pixeldrain might not seem to be the cheapest option in some
cases, most cloud providers have extra hidden costs for API calls and
region-specific prices. This makes it very hard to accurately compare the
pricing of these platforms. Pixeldrain includes no hidden costs, I only
charge for storage and egress.
</p>
<style>
.inputs {
display: flex;
flex-direction: row;
justify-content: center;
gap: 10px;
margin-bottom: 10px;
}
.inputs > div {
flex: 1 1 auto;
display: flex;
flex-direction: column;
border-radius: 6px;
overflow: hidden;
border: 2px solid var(--card_color);
}
.bars {
display: flex;
flex-direction: column;
justify-content: center;
gap: 10px;
}
.bars > div {
flex: 1 1 auto;
display: flex;
flex-direction: column;
border-radius: 6px;
overflow: hidden;
border: 2px solid var(--card_color);
}
.bars > div > div {
padding: 4px;
}
.bars > div > div:first-child {
background: var(--card_color);
}
</style>

View File

@@ -1,304 +0,0 @@
<script>
import { add_upload_history, domain_url } from "util/Util.svelte"
import { formatDataVolume, formatDuration } from "util/Formatting"
import Spinner from "util/Spinner.svelte";
export let job = {}
let file_button
let progress_bar
let tries = 0
let start_time = 0
let remaining_time = 0
let last_update_time = 0
let progress = 0
let transfer_rate = 0
const on_progress = (loaded, total) => {
job.loaded_size = loaded
job.total_size = total
if (last_update_time === 0) {
last_update_time = new Date().getTime()
return
}
let current_time = new Date().getTime()
last_update_time = current_time
let elapsed_time = current_time - start_time
remaining_time = (elapsed_time/progress) - elapsed_time
progress = job.loaded_size / job.total_size
// Calculate transfer rate by dividing the total uploaded size by the total
// running time
transfer_rate = Math.floor(job.loaded_size / ((current_time - start_time) / 1000))
progress_bar.style.width = (progress * 100) + "%"
if (progress >= 1) {
job.status = "processing"
progress_bar.style.opacity = "0"
} else {
progress_bar.style.opacity = "1"
}
}
let href = null
let target = null
const on_success = (resp) => {
transfer_rate = 0
job.loaded_size = job.total_size
job.file = null // Delete reference to file to free memory
job.id = resp.id
job.status = "finished"
job.on_finished(job)
add_upload_history(resp.id)
href = "/u/"+resp.id
target = "_blank"
progress_bar.style.width = "100%"
progress_bar.style.opacity = "0"
}
let error_id = ""
let error_reason = ""
const on_failure = (status, message) => {
transfer_rate = 0
job.loaded_size = job.total_size
job.file = null // Delete reference to file to free memory
error_id = status
error_reason = message
job.status = "error"
file_button.style.background = 'var(--danger_color)'
file_button.style.color = 'var(--highlight_text_color)'
progress_bar.style.width = "0"
job.on_finished(job)
}
export const start = () => {
job.status = "uploading"
// Check the file size limit. For free accounts it's 20 GB
if (window.user.subscription.file_size_limit === 0) {
window.user.subscription.file_size_limit = 20e9
}
if (job.total_size > window.user.subscription.file_size_limit) {
on_failure(
"file_too_large",
"This file is too large. Check out the Pro subscription to increase the file size limit"
)
return
}
start_time = new Date().getTime()
let xhr = new XMLHttpRequest();
xhr.open("PUT", window.api_endpoint+"/file/"+encodeURIComponent(job.name), true);
xhr.timeout = 86400000; // 24 hours, to account for slow connections
xhr.upload.addEventListener("progress", evt => {
if (evt.lengthComputable) {
on_progress(evt.loaded, evt.total)
}
});
xhr.onreadystatechange = () => {
// readystate 4 means the upload is done
if (xhr.readyState !== 4) {
return
}
if (xhr.status >= 100 && xhr.status < 400) {
// Request is a success
on_success(JSON.parse(xhr.response))
} else if (xhr.status >= 400) {
// Request failed
console.log("Upload error. status: " + xhr.status + " response: " + xhr.response);
let resp = {}
if (xhr.status === 429) {
resp = {
value: "too_many_requests",
message: "Too many requests. Please wait a few seconds",
}
} else {
resp = JSON.parse(xhr.response)
}
if (resp.value == "file_too_large"
|| resp.value == "ip_banned"
|| resp.value == "user_out_of_space"
|| tries === 3) {
// Permanent failure
on_failure(resp.value, resp.message)
} else {
// Temporary failure, try again in 5 seconds
tries++
setTimeout(start, 5000)
}
} else if (xhr.status === 0) {
on_failure("request_failed", "The connection was interrupted")
} else {
// Request did not arrive
if (tries < 3) {
// Try again
tries++
setTimeout(start, 5000)
} else {
// Give up after three tries
on_failure(xhr.responseText, xhr.responseText)
}
}
};
xhr.send(job.file);
}
</script>
<a bind:this={file_button} class="upload_task" {href} {target}>
<div class="top_half">
<div class="thumbnail">
{#if job.status === "queued"}
<i class="icon">cloud_queue</i>
{:else if job.status === "uploading"}
<i class="icon">cloud_upload</i>
{:else if job.status === "processing"}
<Spinner></Spinner>
{:else if job.status === "finished"}
<img src="/api/file/{job.id}/thumbnail" alt="file thumbnail" />
{:else if job.status === "error"}
<i class="icon">error</i>
{/if}
</div>
<div class="queue_body">
<div class="title">
{#if job.status === "error"}
{error_reason}
{:else}
{job.name}
{/if}
</div>
<div class="stats">
{#if job.status === "queued"}
Queued...
{:else if job.status === "uploading"}
<div class="stat">
{(progress*100).toPrecision(3)}%
</div>
<div class="stat">
ETA {formatDuration(remaining_time, 0)}
</div>
<div class="stat">
{formatDataVolume(transfer_rate, 3)}/s
</div>
{:else if job.status === "processing"}
Calculating parity data...
{:else if job.status === "finished"}
<span class="file_link">
{domain_url() + "/u/" + job.id}
</span>
{:else if job.status === "error"}
{error_id}
{/if}
</div>
</div>
</div>
<div class="progress">
<div bind:this={progress_bar} class="progress_bar"></div>
</div>
</a>
<style>
.upload_task{
position: relative;
width: 440px;
max-width: 95%;
height: 4em;
margin: 6px;
padding: 0;
overflow: hidden;
border-radius: 6px;
background: var(--input_background);
color: var(--body_text_color);
word-break: break-all;
text-align: left;
line-height: 1.2em;
display: inline-flex;
flex-direction: column;
transition: background 0.2s, opacity 2s;
white-space: normal;
text-overflow: ellipsis;
text-decoration: none;
vertical-align: top;
cursor: pointer;
}
.top_half {
flex: 1 1 auto;
display: flex;
flex-direction: row;
overflow: hidden;
}
.upload_task:hover {
background: var(--input_hover_background);
text-decoration: none;
}
.thumbnail {
display: flex;
flex: 0 0 auto;
width: 4em;
margin-right: 4px;
align-items: center;
justify-content: center;
}
.thumbnail > img {
width: 90%;
border-radius: 4px;
}
.thumbnail > i {
font-size: 3em;
}
.queue_body {
flex: 1 1 auto;
display: flex;
flex-direction: column;
}
.queue_body > .title {
flex: 1 1 auto;
overflow: hidden;
}
.queue_body > .stats {
flex: 0 0 auto;
display: flex;
flex-direction: row;
height: 1.4em;
border-top: 1px solid var(--separator);
text-align: center;
font-family: sans-serif, monospace;
font-size: 0.9em;
}
.queue_body > .stats > .stat {
flex: 0 1 100%;
}
.file_link{
color: var(--highlight_color);
}
.progress {
flex: 0 0 auto;
height: 3px;
}
.progress_bar {
background: var(--highlight_background);
height: 100%;
width: 0;
transition: width 0.25s, opacity 3s;
transition-timing-function: linear, ease;
}
</style>

View File

@@ -1,126 +0,0 @@
<script>
import { formatDataVolume, formatDuration } from "util/Formatting";
import ProgressBar from "util/ProgressBar.svelte";
export let upload_queue = []
let stats_interval = null
let stats_interval_ms = 1000
let finished = false
export const start = () => {
if (stats_interval === null) {
start_time = new Date().getTime()
stats_interval = setInterval(stats_update, stats_interval_ms)
}
finished = false
}
export const finish = () => {
if (stats_interval !== null) {
clearInterval(stats_interval)
stats_interval = null
}
finished = true
start_time = 0
total_loaded = total_size
previously_loaded = total_size
total_progress = 1
total_rate = 0
document.title = "Finished! ~ pixeldrain"
}
let start_time = 0
let total_progress = 0
let total_size = 0
let total_loaded = 0
let previously_loaded = 0
let last_total_loaded = 0
let total_rate = 0
let elapsed_time = 0
let remaining_time = 0
const stats_update = () => {
if (start_time === 0) {
start_time = new Date().getTime()
}
// Get total size of upload queue and size of finished uploads
total_size = 0
total_loaded = 0
for (let i = 0; i < upload_queue.length; i++) {
total_size += upload_queue[i].total_size
total_loaded += upload_queue[i].loaded_size
}
total_progress = (total_loaded - previously_loaded) / (total_size - previously_loaded)
// Calculate ETA by estimating the total time and subtracting the elapsed time
elapsed_time = new Date().getTime() - start_time
remaining_time = (elapsed_time/total_progress) - elapsed_time
// Calculate the rate by comparing the current progress with the last iteration
total_rate = Math.floor(
(total_rate * 0.8) +
(((1000 / stats_interval_ms) * (total_loaded - last_total_loaded)) * 0.2)
)
last_total_loaded = total_loaded
document.title = (total_progress*100).toFixed(0) + "% ~ " +
formatDuration(remaining_time, 0) +
" ~ uploading to pixeldrain"
}
</script>
<div class="stats_box">
<div>
<div>
Size {formatDataVolume(total_size, 3)}
</div>
<div>
Progress {(total_progress*100).toPrecision(3)}%
</div>
</div>
<div>
{#if finished}
<div>
Time {formatDuration(elapsed_time, 0)}
</div>
<div>
Rate {formatDataVolume(total_loaded / (elapsed_time/1000), 3)}/s
</div>
{:else}
<div>
ETA {formatDuration(remaining_time, 0)}
</div>
<div>
Rate {formatDataVolume(total_rate, 3)}/s
</div>
{/if}
</div>
</div>
<ProgressBar total={total_size} used={total_loaded} animation="linear" speed={stats_interval_ms}/>
<style>
.stats_box {
display: inline-flex;
flex-direction: row;
flex-wrap: wrap;
width: 100%;
text-align: center;
}
.stats_box > div {
flex: 1 1 auto;
display: flex;
flex-direction: row;
}
.stats_box > div > div {
flex: 1 1 auto;
min-width: 150px;
}
</style>

View File

@@ -1,478 +0,0 @@
<script>
import UploadProgressBar from "./UploadProgressBar.svelte"
import { domain_url } from "util/Util.svelte"
import { tick } from "svelte"
import Facebook from "icons/Facebook.svelte"
import Reddit from "icons/Reddit.svelte"
import Twitter from "icons/Twitter.svelte"
import Tumblr from "icons/Tumblr.svelte"
import StorageProgressBar from "user_home/StorageProgressBar.svelte"
import Konami from "util/Konami.svelte"
import UploadStats from "./UploadStats.svelte";
import CopyButton from "layout/CopyButton.svelte";
// === UPLOAD LOGIC ===
let file_input_field
const file_input_change = (event) => {
// Start uploading the files async
upload_files(event.target.files)
// This resets the file input field
file_input_field.nodeValue = ""
}
const paste = (e) => {
if (e.clipboardData.files[0]) {
e.preventDefault();
e.stopPropagation();
upload_files(e.clipboardData.files)
}
}
let active_uploads = 0
let upload_queue = []
let state = "idle" // idle, uploading, finished
let upload_stats
export const upload_files = async (files) => {
if (files.length === 0) {
return
}
// Add files to the queue
for (let i = 0; i < files.length; i++) {
if (files[i].type === "" && files[i].size === 0) {
continue
}
upload_queue.push({
file: files[i],
name: files[i].name,
status: "queued",
component: null,
id: "",
total_size: files[i].size,
loaded_size: 0,
on_finished: finish_upload,
})
}
// Reassign array and wait for tick to complete. After the tick is completed
// each upload progress bar will have bound itself to its array item
upload_queue = upload_queue
await tick()
start_upload()
}
const start_upload = () => {
let finished_count = 0
for (let i = 0; i < upload_queue.length && active_uploads < 3; i++) {
if (upload_queue[i].status == "queued") {
active_uploads++
upload_queue[i].component.start()
} else if (
upload_queue[i].status == "finished" ||
upload_queue[i].status == "error"
) {
finished_count++
}
}
if (active_uploads === 0 && finished_count != 0) {
state = "finished"
upload_stats.finish()
uploads_finished()
} else {
state = "uploading"
upload_stats.start()
}
}
const finish_upload = (file) => {
active_uploads--
start_upload()
}
const leave_confirmation = e => {
if (state === "uploading") {
e.preventDefault()
e.returnValue = "If you close the page your files will stop uploading. Do you want to continue?"
return e.returnValue
} else {
return null
}
}
// === SHARING BUTTONS ===
let navigator_share = !!(window.navigator && window.navigator.share)
let share_title = ""
let share_link = ""
let input_album_name = ""
let btn_upload_text
let btn_copy_link
let btn_open_link
let btn_show_qr
let btn_share_email
let btn_share_twitter
let btn_share_facebook
let btn_share_reddit
let btn_share_tumblr
let btn_create_list
let btn_copy_links
let btn_copy_markdown
let btn_copy_bbcode
const uploads_finished = async () => {
let count = upload_queue.reduce(
(acc, curr) => curr.status === "finished" ? acc + 1 : acc, 0,
)
if (count === 1) {
share_title = "Download " + upload_queue[0].name + " here"
share_link = domain_url() + "/u/" + upload_queue[0].id
} else if (count > 1) {
try {
const resp = await create_list(count+" files", true)
console.log("Automatic list ID " + resp.id)
share_title = "View a collection of "+count+" files here"
share_link = domain_url() + "/l/" + resp.id
} catch (err) {
alert("Failed to generate link. Please check your internet connection and try again.\nError: " + err)
}
}
generate_link_list()
}
async function create_list(title, anonymous) {
let files = upload_queue.reduce(
(acc, curr) => {
if (curr.status === "finished") {
acc.push({"id": curr.id})
}
return acc
},
[],
)
const resp = await fetch(
window.api_endpoint+"/list",
{
method: "POST",
headers: { "Content-Type": "application/json; charset=UTF-8" },
body: JSON.stringify({
"title": title,
"anonymous": anonymous,
"files": files
})
}
)
if(!resp.ok) {
return Promise.reject("HTTP error: "+resp.status)
}
return await resp.json()
}
let qr_visible = false
const open_link = () => window.open(share_link, "_blank")
const show_qr_code = () => qr_visible = !qr_visible
const share_mail = () => window.open("mailto:please@set.address?subject=File%20on%20pixeldrain&body=" + share_link)
const share_twitter = () => window.open("https://twitter.com/share?url=" + share_link)
const share_facebook = () => window.open('https://www.facebook.com/sharer.php?u=' + share_link)
const share_reddit = () => window.open('https://www.reddit.com/submit?url=' + share_link)
const share_tumblr = () => window.open('https://www.tumblr.com/share/link?url=' + share_link)
const share_navigator = () => {
window.navigator.share({ title: "Pixeldrain", text: share_title, url: share_link })
}
const create_album = () => {
if (!input_album_name) {
return
}
create_list(input_album_name, false).then(resp => {
window.location = '/l/' + resp.id
}).catch(err => {
alert("Failed to create list. Server says this:\n"+err)
})
}
const get_finished_files = () => {
return upload_queue.reduce(
(acc, curr) => {
if (curr.status === "finished") {
acc.push(curr)
}
return acc
},
[],
)
}
let link_list = ""
let bbcode = ""
let markdown = ""
const generate_link_list = () => {
// Add the text to the textarea
link_list = ""
bbcode = ""
markdown = ""
let files = get_finished_files()
files.forEach(file => {
// Link list example: https://pixeldrain.com/u/abcd1234 Some_file.png
link_list += domain_url() + "/u/" + file.id + " " + file.name + "\n"
// BBCode example: [url=https://pixeldrain.com/u/abcd1234]Some_file.png[/url]
bbcode += "[url=" + domain_url() + "/u/" + file.id + "]" + file.name + "[/url]\n"
// Markdown example: * [Some_file.png](https://pixeldrain.com/u/abcd1234)
if (files.length > 1) {
markdown += " * "
}
markdown += "[" + file.name + "](" + domain_url() + "/u/" + file.id + ")\n"
})
if (share_link.includes("/l/")) {
link_list += "\n" + share_link + " All " + files.length + " files\n"
bbcode += "\n[url=" + share_link + "]All " + files.length + " files[/url]\n"
markdown += " * [All " + files.length + " files](" + share_link + ")\n"
}
}
const keydown = (e) => {
if (e.ctrlKey || e.altKey || e.metaKey) {
return // prevent custom shortcuts from interfering with system shortcuts
}
if (document.activeElement.type && document.activeElement.type === "text") {
return // Prevent shortcuts from interfering with input fields
}
switch (e.key) {
case "u": file_input_field.click(); break
case "t": btn_upload_text.click(); break
case "c": btn_copy_link.copy(); break
case "o": btn_open_link.click(); break
case "q": btn_show_qr.click(); break
case "l": btn_create_list.click(); break
case "e": btn_share_email.click(); break
case "w": btn_share_twitter.click(); break
case "f": btn_share_facebook.click(); break
case "r": btn_share_reddit.click(); break
case "m": btn_share_tumblr.click(); break
case "a": btn_copy_links.copy(); break
case "d": btn_copy_markdown.copy(); break
case "b": btn_copy_bbcode.copy(); break
}
}
</script>
<svelte:window on:paste={paste} on:keydown={keydown} on:beforeunload={leave_confirmation} />
<Konami/>
<!-- If the user is logged in and has used more than 50% of their storage space we will show a progress bar -->
{#if window.user.username !== "" && window.user.storage_space_used/window.user.subscription.storage_space > 0.5}
<section>
<StorageProgressBar used={window.user.storage_space_used} total={window.user.subscription.storage_space}></StorageProgressBar>
</section>
{/if}
<section class="instruction" style="border-top: none;">
<span class="big_number">1</span>
<span class="instruction_text">Select files to upload</span>
<br/>
You can also drop files on this page from your file manager or
paste an image from your clipboard
</section>
<input bind:this={file_input_field} on:change={file_input_change} type="file" name="file" multiple="multiple"/>
<button on:click={() => { file_input_field.click() }} class="big_button button_highlight">
<i class="icon small">cloud_upload</i>
<span><u>U</u>pload Files</span>
</button>
<a bind:this={btn_upload_text} href="/t" id="upload_text_button" class="button big_button button_highlight">
<i class="icon small">text_fields</i>
<span>Upload <u>T</u>ext</span>
</a>
<br/>
<p>
By uploading files to pixeldrain you acknowledge and accept our
<a href="/abuse">content policy</a>.
<p>
<br/>
<section class="instruction" style="margin-bottom: 0;">
<span class="big_number">2</span>
<span class="instruction_text">Wait for the files to finish uploading</span>
<br/>
<UploadStats bind:this={upload_stats} upload_queue={upload_queue}/>
</section>
{#each upload_queue as file}
<UploadProgressBar bind:this={file.component} job={file}></UploadProgressBar>
{/each}
<br/>
<section class="instruction">
<span class="big_number">3</span>
<span class="instruction_text">Share the files</span>
</section>
<br/>
{#if upload_queue.length > 1}
You can create an album to group your files together into one link<br/>
Name:
<form class="album_name_form" on:submit|preventDefault={create_album}>
<input bind:value={input_album_name} type="text" disabled={state !== "finished"} placeholder="My album"/>
<button type="submit" disabled={state !== "finished"}>
<i class="icon">create_new_folder</i> Create
</button>
</form>
<br/><br/>
Other sharing methods:
<br/>
{/if}
<div class="social_buttons" class:hide={!navigator_share}>
<button id="btn_social_share" on:click={share_navigator} class="social_buttons" disabled={state !== "finished"}>
<i class="icon">share</i><br/>
Share
</button>
</div>
<CopyButton bind:this={btn_copy_link} text={share_link} large_icon><u>C</u>opy link</CopyButton>
<button bind:this={btn_open_link} on:click={open_link} class="social_buttons" disabled={state !== "finished"}>
<i class="icon">open_in_new</i>
<span><u>O</u>pen link</span>
</button>
<button bind:this={btn_show_qr} on:click={show_qr_code} class="social_buttons" disabled={state !== "finished"} class:button_highlight={qr_visible}>
<i class="icon">qr_code</i>
<span><u>Q</u>R code</span>
</button>
<div style="display: inline-block;" class:hide={navigator_share}>
<button bind:this={btn_share_email} on:click={share_mail} class="social_buttons" disabled={state !== "finished"}>
<i class="icon">email</i>
<span><u>E</u>-Mail</span>
</button>
<button bind:this={btn_share_twitter} on:click={share_twitter} class="social_buttons" disabled={state !== "finished"}>
<Twitter style="width: 40px; height: 40px;"></Twitter>
<span>T<u>w</u>itter</span>
</button>
<button bind:this={btn_share_facebook} on:click={share_facebook} class="social_buttons" disabled={state !== "finished"}>
<Facebook style="width: 40px; height: 40px;"></Facebook>
<span><u>F</u>acebook</span>
</button>
<button bind:this={btn_share_reddit} on:click={share_reddit} class="social_buttons" disabled={state !== "finished"}>
<Reddit style="width: 40px; height: 40px;"></Reddit>
<span><u>R</u>eddit</span>
</button>
<button bind:this={btn_share_tumblr} on:click={share_tumblr} class="social_buttons" disabled={state !== "finished"}>
<Tumblr style="width: 40px; height: 40px;"></Tumblr>
<span>Tu<u>m</u>blr</span>
</button>
</div>
<br/>
{#if qr_visible}
<img src="/api/misc/qr?text={encodeURIComponent(share_link)}" alt="QR code" style="width: 300px; max-width: 100%;">
<br/>
{/if}
<CopyButton bind:this={btn_copy_links} text={link_list}>Copy <u>a</u>ll links to clipboard</CopyButton>
<CopyButton bind:this={btn_copy_markdown} text={markdown}>Copy mark<u>d</u>own to clipboard</CopyButton>
<CopyButton bind:this={btn_copy_bbcode} text={bbcode}>Copy <u>B</u>BCode to clipboard</CopyButton>
<br/>
{#if window.user.subscription.name === ""}
<section>
<div class="instruction">
<span class="big_number">4</span>
<span class="instruction_text">Support me on Patreon!</span>
</div>
<p>
Pixeldrain costs a lot of money to maintain. Currently the site
makes just barely enough money to pay for hosting. I have never been
able to compensate myself for the hours I have put in developing
this project. Please consider getting a subscription so I can
continue working on pixeldrain and make it even better.
</p>
<p>
Pro costs only <b>€40 per year</b> or <b>€4 per month</b>. You will
get some nice benefits and more features are on the way. You can
help with making pixeldrain the easiest and fastest way to share
files online!
</p>
<br/>
<div style="text-align: center;">
<a href="#pro" class="button big_button" style="min-width: 350px;">
<i class="icon">arrow_downward</i>
Check out Pro
<i class="icon">arrow_downward</i>
</a>
</div>
</section>
{/if}
<br/>
<style>
.big_button {
width: 40%;
min-width: 300px;
max-width: 400px;
margin: 10px;
border-radius: 32px;
font-size: 1.8em;
justify-content: center;
}
.instruction {
border-top: 1px solid var(--separator);
margin: 1em auto;
padding: 5px;
}
.big_number {
font-size: 1.5em;
font-weight: bold;
line-height: 1em;
text-align: center;
display: inline-block;
background: var(--highlight_background);
color: var(--highlight_text_color);
border-radius: 30px;
padding: 0.15em;
margin-right: 0.4em;
width: 1.4em;
height: 1.4em;
vertical-align: middle;
}
.instruction_text {
margin: 0.1em;
font-size: 1.5em;
display: inline;
vertical-align: middle;
}
.album_name_form {
display: inline-flex;
flex-direction: row;
align-items: center;
}
.social_buttons {
flex-direction: column;
min-width: 5em;
}
.social_buttons.hide {
display: none;
}
.social_buttons > .icon {
font-size: 40px;
display: inline-block;
}
.hide {
display: none;
}
</style>

View File

@@ -1,8 +0,0 @@
import App from './text_upload/TextUpload.svelte';
const app = new App({
target: document.getElementById("body"),
props: {}
});
export default app;

View File

@@ -1,149 +0,0 @@
<script>
import { onMount } from "svelte";
import Modal from "util/Modal.svelte";
import Behave from "behave-js";
import { add_upload_history } from "util/Util.svelte";
let textarea
let help
onMount(() => {
new Behave({
textarea: textarea,
autoStrip: false,
autoOpen: false,
overwrite: false,
autoIndent: false,
replaceTab: true,
softTabs: false,
tabSize: 8
});
})
const upload_text = async () => {
var filename = prompt(
"What do you want to call this piece of textual art?\n\n" +
"Please add your own file extension, if you want.",
"Text file.txt"
);
if (!filename){
return; // User pressed cancel
}
try {
let form = new FormData()
form.append("name", filename)
form.append("file", new Blob([textarea.value], {type: "text/plain"}))
let resp = await fetch(
window.api_endpoint+"/file",
{method: "POST", body: form}
)
if(resp.status >= 400) {
throw new Error(await resp.text());
}
let id = (await resp.json()).id
add_upload_history(id)
window.location.href = "/u/" + id
} catch (err) {
alert("File upload failed: " + err)
return
}
}
// Upload the file when ctrl + s is pressed
const keydown = e => {
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
e.preventDefault()
upload_text();
return false;
}
}
</script>
<svelte:window on:keydown={keydown}></svelte:window>
<div id="text_editor" class="text_editor">
<div id="headerbar" class="headerbar">
<a href="/" class="button round">
<i class="icon">arrow_back</i>
</a>
<div id="headerbar_spacer" class="headerbar_spacer"></div>
<button class="button toolbar_button round" on:click={help.toggle}>
<i class="icon">info</i> Information
</button>
<button class="button toolbar_button round button_highlight" on:click={upload_text}>
<i class="icon">save</i> Save
</button>
</div>
<div class="textarea_container">
<!-- svelte-ignore a11y-autofocus -->
<textarea bind:this={textarea} class="textarea" placeholder="Your text here..." autofocus="autofocus"></textarea>
</div>
</div>
<Modal bind:this={help} title="Text editor help" padding width="500px">
<p>
You can type anything you want in here. When you're done press
CTRL + S or click the Save button in the top right corner to
upload your text file to pixeldrain.
</p>
<p>
To show syntax highlighting on pixeldrain's file viewer you
should save your file with a file extension like .js, .go,
.java, etc. If you save your file with the extension .md or
.markdown the result will be rendered as HTML on the file
viewer.
</p>
<p>
The text editor has been enhanced by Jacob Kelley's
<a href="https://jakiestfu.github.io/Behave.js/" target="_blank" rel="noreferrer">Behave.js</a>.
Many thanks to him for developing this plugin and putting it
under the MIT license.
</p>
</Modal>
<style>
.text_editor {
position: absolute;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
.headerbar {
flex: 0 0 auto;
display: flex;
flex-direction: row;
padding: 4px;
background: var(--background_color);
color: var(--background_text_color);
}
.headerbar > * {
flex: 0 0 auto;
margin-left: 6px;
margin-right: 6px;
}
.headerbar > .headerbar_spacer { flex: 1 1 auto; }
.textarea_container {
flex: 1 1 auto;
margin: 0;
z-index: 9;
}
.textarea {
position: relative;
display: block;
height: 100%;
width: 100%;
background: var(--body_background);
color: var(--body_text_color);
margin: 0;
border-radius: 0;
box-shadow: none;
}
.textarea:focus { box-shadow: none; }
</style>

View File

@@ -1,8 +0,0 @@
import App from './upload_history/UploadHistory.svelte';
const app = new App({
target: document.getElementById("page_body"),
props: {}
});
export default app;

View File

@@ -1,159 +0,0 @@
<script>
import { onMount } from "svelte";
import { formatDate } from "util/Formatting";
import Footer from "layout/Footer.svelte"
import Button from "layout/Button.svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte"
let files = []
let loading = true
const parse_file_list = () => {
// Get the file IDs from localstorage
let uploadsStr = localStorage.getItem("uploaded_files")
if (uploadsStr === null) {
uploadsStr = ""
}
return uploadsStr.split(",")
}
const save_file_list = () => {
if (loading) {
alert("Please wait for the file list to finish loading first")
return
}
const id_list = files.reduce((acc, val) => {
acc.push(val.id)
return acc
}, [])
localStorage.setItem("uploaded_files", id_list.join(","))
}
// index is the index of the file ID in localstorage, id is the public file ID
// of the file
const remove_file = (id) => {
// Remove the file from the rendered files list
for (let i = 0; i < files.length; i++) {
if (id === files[i].id) {
console.debug("Removing file", id, "at index", i)
files.splice(i, 1)
files = files
break
}
}
save_file_list()
}
const get_files = async () => {
const file_ids = parse_file_list()
for (const id of file_ids) {
if (id === "") {
continue
}
const resp = await fetch(window.api_endpoint + "/file/" + id + "/info")
if (resp.status === 404) {
continue
} else if (resp.status >= 400) {
throw new Error(await resp.json())
}
files.push(await resp.json())
files = files
}
loading = false
save_file_list()
}
onMount(() => get_files())
</script>
<LoadingIndicator loading={loading}/>
<header>
<h1>File upload history</h1>
</header>
<div id="page_content" class="page_content">
<section>
<p>
Here are all files you have previously uploaded to pixeldrain using this
computer. This data is saved locally in your web browser and gets updated
every time you upload a file through your current browser.
</p>
</section>
{#each files as file (file.id)}
<a class="file_button" href="/u/{file.id}" target="_blank">
<img src="/api/file/{file.id}/thumbnail?width=80&amp;height=80" alt="{file.name}">
<div>
<span class="file_button_title">
{file.name}
</span>
<br/>
<span class="file_button_subtitle">
{formatDate(file.date_upload, true, true, true)}
</span>
</div>
<Button
click={e => {
e.preventDefault()
e.stopPropagation()
remove_file(file.id)
}}
icon="cancel"
/>
</a>
{/each}
</div>
<Footer/>
<style>
.file_button {
display: inline-flex;
flex-direction: row;
position: relative;
width: 400px;
max-width: 90%;
height: 3.6em;
margin: 8px;
padding: 0;
overflow: hidden;
border-radius: 6px;
background: var(--input_background);
color: var(--body_text_color);
word-break: break-all;
text-align: left;
line-height: 1.2em;
transition: box-shadow 0.3s, opacity 2s, background 0.2s;
white-space: normal;
text-overflow: ellipsis;
text-decoration: none;
vertical-align: top;
cursor: pointer;
}
.file_button:hover {
text-decoration: none;
background: var(--input_hover_background);
}
.file_button>img {
flex: 0 0 auto;
max-height: 100%;
max-width: 25%;
margin-right: 5px;
display: block;
}
.file_button_title {
color: var(--link_color);
}
</style>

View File

@@ -1,285 +0,0 @@
<script>
import FilePicker from "file_viewer/FilePicker.svelte";
import CustomBanner from "file_viewer/CustomBanner.svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import SuccessMessage from "util/SuccessMessage.svelte";
import ThemePicker from "util/ThemePicker.svelte";
import { onMount } from "svelte";
import Persistence from "icons/Persistence.svelte";
import ToggleButton from "layout/ToggleButton.svelte";
let loading = false
let success_message
let file_picker
let currently_selecting = "" // header, background or footer
let theme = ""
let header_image = ""
let header_link = ""
let background_image = ""
let footer_image = ""
let footer_link = ""
let affiliate_prompt = false
let disable_download_button = false
let disable_share_button = false
let select_file = t => {
currently_selecting = t
file_picker.open()
}
let add_file = files => {
let type = files[0].type
if (type != "image/png" && type != "image/jpeg" && type != "image/gif" && type != "image/webp") {
success_message.set(false, "File must be an image type")
return
}
if (files[0].size > 10e6) {
success_message.set(false, "Files larger than 10 MB are not allowed. Recommended size is below 1 MB")
return
}
if (currently_selecting === "header") {
header_image = files[0].id
} else if (currently_selecting === "background") {
background_image = files[0].id
} else if (currently_selecting === "footer") {
footer_image = files[0].id
}
save()
}
let save = async () => {
loading = true
const form = new FormData()
form.append("theme", theme)
form.append("header_image", header_image)
form.append("header_link", header_link)
form.append("background_image", background_image)
form.append("footer_image", footer_image)
form.append("footer_link", footer_link)
form.append("disable_download_button", disable_download_button)
form.append("disable_share_button", disable_share_button)
if (affiliate_prompt) {
form.append("affiliate_prompt", window.user.username)
}
try {
const resp = await fetch(
window.api_endpoint+"/user/file_customization",
{ method: "PUT", body: form }
);
if(resp.status >= 400) {
let json = await resp.json()
console.debug(json)
throw json.message
}
success_message.set(true, "Changes saved")
} catch(err) {
success_message.set(false, err)
} finally {
loading = false
}
}
onMount(() => {
// The fields are undefined when they're empty. So we need to check if each
// field is defined before converting to a string
if (window.user.file_viewer_branding) {
let b = window.user.file_viewer_branding
theme = b.theme ? b.theme : ""
header_image = b.header_image ? b.header_image : ""
header_link = b.header_link ? b.header_link : ""
background_image = b.background_image ? b.background_image : ""
footer_image = b.footer_image ? b.footer_image : ""
footer_link = b.footer_link ? b.footer_link : ""
affiliate_prompt = b.affiliate_prompt === window.user.username ? true : false
disable_download_button = b.disable_download_button ? b.disable_download_button : false
disable_share_button = b.disable_share_button ? b.disable_share_button : false
}
})
</script>
<LoadingIndicator loading={loading}/>
<section>
<h2><Persistence/>File viewer branding</h2>
{#if !window.user.subscription.file_viewer_branding}
<div class="highlight_yellow">
Sharing settings are not available for your account. Subscribe to
the Persistence plan or higher to enable these features.
</div>
{/if}
<SuccessMessage bind:this={success_message}></SuccessMessage>
<p>
You can change the appearance of your file viewer pages. The images you
choose here will be loaded each time someone visits one of your files.
The data usage will also be subtracted from your account's data cap.
Keep in mind that large images can take a very long time to load over
cellular connections. I recommend keeping the header and footer images
below 100 kB, and the background image below 1 MB. Allowed image types
are PNG, JPEG, GIF and WebP. If you want to use an animated banner you
should use APNG or WebP. Avoid using animated GIFs as they are very slow
to load.
</p>
<fieldset>
<legend>Theme</legend>
<p>
Choose a theme for your download pages. This theme will override the
theme preference of the person viewing the file. Set to 'None' to let
the viewer choose their own theme.
</p>
<ThemePicker
theme={theme}
on:theme_change={e => {theme = e.detail; save()}}>
</ThemePicker>
</fieldset>
<fieldset>
<legend>Header image</legend>
<p>
Will be shown above the file. Maximum height is 90px. Will be shrunk if
larger. You can also add a link to open when the visitor clicks the
image. The link needs to start with 'https://'.
</p>
<button on:click={() => {select_file("header")}}>
<i class="icon">add_photo_alternate</i>
Select header image
</button>
<button on:click={() => {header_image = ""; save()}}>
<i class="icon">close</i>
Remove
</button>
<br/>
Header image link:<br/>
<form class="form_row" on:submit|preventDefault={save}>
<input class="grow" bind:value={header_link} type="text" placeholder="https://"/>
<button class="shrink" action="submit"><i class="icon">save</i> Save</button>
</form>
{#if header_image}
<div class="highlight_shaded">
<CustomBanner src={"/api/file/"+header_image} link={header_link}></CustomBanner>
</div>
{/if}
</fieldset>
<fieldset>
<legend>Background image</legend>
<p>
This image will be shown behind the file which is being viewed. I
recommend choosing something dark and not too distracting. Try to keep
the file below 1 MB to not harm page loading times. Using a JPEG image
with a quality value of 60 is usually good enough.
</p>
<button on:click={() => {select_file("background")}}>
<i class="icon">add_photo_alternate</i>
Select background image
</button>
<button on:click={() => {background_image = ""; save()}}>
<i class="icon">close</i>
Remove
</button>
{#if background_image}
<div class="highlight_shaded">
<img class="background_preview" src="/api/file/{background_image}" alt="Custom file viewer background"/>
</div>
{/if}
</fieldset>
<fieldset>
<legend>Footer image</legend>
<p>
Will be shown below the file. Maximum height is 90px. Will be shrunk if
larger.
</p>
<button on:click={() => {select_file("footer")}}>
<i class="icon">add_photo_alternate</i>
Select footer image
</button>
<button on:click={() => {footer_image = ""; save()}}>
<i class="icon">close</i>
Remove
</button>
<br/>
Footer image link:<br/>
<form class="form_row" on:submit|preventDefault={save}>
<input class="grow" bind:value={footer_link} type="text" placeholder="https://"/>
<button class="shrink" action="submit"><i class="icon">save</i> Save</button>
</form>
{#if footer_image}
<div class="highlight_shaded">
<CustomBanner src={"/api/file/"+footer_image} link={footer_link}></CustomBanner>
</div>
{/if}
</fieldset>
<fieldset>
<legend>Affiliate prompt</legend>
<p>
When this is enabled premium users on your download pages will be
asked to support you through pixeldrain's <a
href="/about#toc_12">affiliate program</a>.
</p>
<ToggleButton bind:on={affiliate_prompt} action={save}>
Enable affiliate prompt
</ToggleButton>
</fieldset>
<fieldset>
<legend>Toolbar buttons</legend>
<p>
If you don't want to make it obvious that your files can be downloaded
or shared while still allowing people to view them through the site you
can use these options.
</p>
<p>
The buttons will be hidden, however your files can still be downloaded
and shared through the API. The changes are purely cosmetic.
</p>
<p>
For convenience these options only apply when other people view your
files. The buttons are still available to you. If you want to see the
effects you can open your file in an incognito window.
</p>
<ToggleButton bind:on={disable_download_button} action={save}>
Disable download button
</ToggleButton>
<br/>
<ToggleButton bind:on={disable_share_button} action={save}>
Disable share button
</ToggleButton>
</fieldset>
</section>
<FilePicker
bind:this={file_picker}
on:files={e => {add_file(e.detail)}}
multi_select={false}
title="Select image file"
/>
<style>
.background_preview {
max-height: 200px;
max-width: 100%;
display: block;
margin: auto;
}
.form_row {
display: inline-flex;
flex-direction: row;
width: 100%;
align-items: center;
}
.grow {
flex: 1 1 auto;
}
.shrink {
flex: 0 0 auto;
}
</style>

View File

@@ -9,10 +9,8 @@ import DepositCredit from "./DepositCredit.svelte";
import TabMenu, { type Tab } from "util/TabMenu.svelte";
import BandwidthSharing from "./BandwidthSharing.svelte";
import EmbeddingControls from "./EmbeddingControls.svelte";
import PageBranding from "./PageBranding.svelte";
import Dashboard from "./dashboard/Dashboard.svelte";
import AffiliatePrompt from "./AffiliatePrompt.svelte";
import FileManager from "./filemanager/FileManager.svelte";
import { onMount } from "svelte";
import { get_user, type User } from "lib/PixeldrainAPI";
@@ -23,13 +21,6 @@ let pages: Tab[] = [
icon: "dashboard",
component: Dashboard,
hide_background: true,
}, {
path: "/user/filemanager",
title: "My Files",
icon: "",
component: FileManager,
hidden: true,
hide_frame: true,
}, {
path: "/user/settings",
title: "Settings",
@@ -67,11 +58,6 @@ let pages: Tab[] = [
title: "Sharing settings",
icon: "share",
component: BandwidthSharing,
}, {
path: "/user/sharing/branding",
title: "Page Branding",
icon: "palette",
component: PageBranding,
}, {
path: "/user/sharing/embedding",
title: "Embedding Controls",

View File

@@ -146,7 +146,6 @@ const update = async (plan) => {
<li>4 TB transfer limit (higher plans available)</li>
<li>Access to the <a href="/filesystem">filesystem</a></li>
<li>2 TB filesytem storage limit (higher plans available)</li>
<li>File expire after 240 days for Pro, and never on the other plans</li>
</ul>
</div>
</div>
@@ -185,12 +184,6 @@ const update = async (plan) => {
href="/user/sharing/bandwidth">hotlinking</a>
enabled)
</li>
<li>Access to the <a href="/filesystem">filesystem</a></li>
<li>Files never expire as long as subscription is active</li>
<li>
Download page <a href="/user/sharing/branding">branding
options</a>
</li>
<li>
File <a href="/user/sharing/embedding">embedding
control</a> options

View File

@@ -20,14 +20,6 @@
<h3>Quick navigation</h3>
<div class="button_row">
<a href="/user/filemanager#files" class="button">
<i class="icon">image</i>
My Files
</a>
<a href="/user/filemanager#lists" class="button">
<i class="icon">photo_library</i>
My Albums
</a>
{#if window.user.subscription.filesystem_access}
<a href="/d/me" class="button">
<i class="icon">folder</i>
@@ -40,19 +32,6 @@
</a>
</div>
<h3>Exports</h3>
<div class="button_row">
<a href="/api/user/files?format=csv" class="button">
<i class="icon">list</i>
Export files to CSV
</a>
<a href="/api/user/lists?format=csv" class="button">
<i class="icon">list</i>
Export albums to CSV
</a>
</div>
<style>
.button_row {
display: flex;

View File

@@ -1,7 +1,7 @@
<script>
import { onMount } from "svelte";
import { FSNavigator } from "filesystem/FSNavigator.ts"
import { fs_encode_path, fs_node_icon } from "filesystem/FilesystemAPI";
import { fs_encode_path, fs_node_icon, node_is_shared } from "filesystem/FilesystemAPI";
import Button from "layout/Button.svelte";
import CreateDirectory from "filesystem/filemanager/CreateDirectory.svelte";
import FSUploadWidget from "filesystem/upload_widget/FSUploadWidget.svelte";
@@ -66,7 +66,7 @@ onMount(() => nav.navigate("/me", false))
{child.name}
</div>
{#if child.id}
{#if node_is_shared(child)}
<a href="/d/{child.id}" class="button action_button">
<i class="icon" title="This file / directory is shared. Click to open public link">share</i>
</a>

View File

@@ -47,14 +47,12 @@ import { formatDataVolume } from "util/Formatting";
No data transfer limit
{/if}
</li>
<li>
{#if window.user.subscription.file_expiry_days > 0}
Files expire after {window.user.subscription.file_expiry_days} days
{:else}
Files never expire
{/if}
</li>
{#if window.user.subscription.id !== ""}
<li>
Support: For questions related to your account you can send a
message to support@pixeldrain.com
</li>
{/if}
</ul>
<style>

View File

@@ -1,47 +0,0 @@
<script>
import UploadLib from "./UploadLib.svelte";
import { drop_target } from "lib/DropTarget.ts"
let upload_widget
</script>
<div class="wrapper" use:drop_target={{upload: (files) => upload_widget.upload_files(files)}}>
<div class="upload_buttons">
<button on:click={() => upload_widget.pick_files() } class="big_button button_highlight">
<i class="icon small">cloud_upload</i>
<span><u>U</u>pload Files</span>
</button>
<a href="/t" id="upload_text_button" class="button big_button button_highlight">
<i class="icon small">text_fields</i>
<span>Upload <u>T</u>ext</span>
</a>
</div>
<div class="center">
<UploadLib bind:this={upload_widget}/>
</div>
</div>
<style>
.wrapper {
border-radius: 4px;
}
.center {
text-align: center;
}
.upload_buttons {
display: flex;
flex-direction: row;
justify-content: space-around;
gap: 8px;
}
.upload_buttons > * {
flex: 1 1 auto;
}
.big_button {
margin: 0;
max-width: 300px;
font-size: 1.4em;
justify-content: center;
}
</style>

View File

@@ -60,22 +60,3 @@ Total storage space used:
Premium data transfer:
(<a href="/user/sharing/bandwidth">set custom limit</a>)
<HotlinkProgressBar used={transfer_used} total={transfer_cap}></HotlinkProgressBar>
<br/>
File count (does not apply to filesystem)
<ProgressBar total={10000} used={window.user.file_count}></ProgressBar>
<div class="gauge_labels">
<div>{window.user.file_count}</div>
<div>10000</div>
</div>
<style>
.gauge_labels {
display: flex;
justify-content: space-between;
line-height: 1em;
}
.gauge_labels > div {
flex: 0 0 auto;
}
</style>

View File

@@ -6,7 +6,6 @@ import CardStatistics from "./CardStatistics.svelte";
import CardSubscription from "./CardSubscription.svelte";
import CardUsage from "./CardUsage.svelte";
import CardActivity from "./CardActivity.svelte";
import CardUpload from "./CardUpload.svelte";
import CardPrepaidTransactions from "./CardPrepaidTransactions.svelte";
import CardFsHome from "./CardFSHome.svelte";
import AddressReputation from "home_page/AddressReputation.svelte";
@@ -62,12 +61,6 @@ const swap_card = (idx1, idx2) => {
onMount(() => {
cards = []
cards.push({
id: "upload",
elem: CardUpload,
title: "Quick upload",
link: "/home",
})
if (window.user.subscription.filesystem_access === true) {
cards.push({
id: "filesystem_home",

View File

@@ -1,442 +0,0 @@
<script>
import { formatDataVolume, formatDate } from "util/Formatting";
// Main elements
let directoryArea
let nodeContainer
let statusBar = "Loading..."
// Internal state, contains a list of all files in the directory, visible
// files in the directory and the last scroll position. These are used for
// rendering the file list correctly
// type: {icon, name, href, type, size, sizeLabel, dateCreated, selected}
let allFiles = []
export const reset = () => {
allFiles = []
}
export const addFile = (id, icon, name, href, type, size, sizeLabel, dateCreated) => {
allFiles.push({
id: id,
icon: icon,
name: name,
href: href,
type: type,
size: size,
sizeLabel: sizeLabel,
dateCreated: dateCreated,
selected: false,
filtered: false,
visible: false,
})
}
export const renderFiles = () => {
search(lastSearchTerm)
}
export const getSelectedFiles = () => {
let selectedFiles = []
for (let i in allFiles) {
if (allFiles[i].selected) {
selectedFiles.push(allFiles[i])
}
}
return selectedFiles
}
// search filters the allFiles array on a search term. All files which match the
// search term will be put into visibleFiles. The visibleFiles array will then
// be rendered by render_visible_files
let lastSearchTerm = ""
export const search = (term) => {
term = term.toLowerCase()
lastSearchTerm = term
if (term === "") {
for (let i in allFiles) {
allFiles[i].filtered = false
}
sortBy("")
render_visible_files()
return
}
let fileName = ""
for (let i in allFiles) {
fileName = allFiles[i].name.toLowerCase()
if (fileName.includes(term)) {
// If a file name contains the search term we include it in the results
allFiles[i].filtered = false
} else {
allFiles[i].filtered = true
}
}
sortBy("")
render_visible_files()
}
// searchSubmit opens the first file in the search results
export const searchSubmit = () => {
for (let i in allFiles) {
if (allFiles[i].visible && !allFiles[i].filtered) {
window.open(allFiles[i].href, "_blank")
break
}
}
}
// Sorting internal state. By default we sort by dateCreated in descending
// order (new to old)
let currentSortField = "dateCreated"
let currentSortAscending = false
let tableColumns = [
{ name: "Name", field: "name", width: "" },
{ name: "Creation date", field: "dateCreated", width: "160px" },
{ name: "Size", field: "size", width: "90px" },
{ name: "Type", field: "type", width: "200px" },
]
const sortBy = (field) => {
if (field === "") {
// If no sort field is provided we use the last used sort field
field = currentSortField
} else {
// If a sort field is provided we check in which direction we have to
// sort
if (currentSortField !== field) {
// If this field is a different field than before we sort it in
// ascending order
currentSortAscending = true
currentSortField = field
} else if (currentSortField === field) {
// If it is the same field as before we reverse the sort order
currentSortAscending = !currentSortAscending
}
}
// Add the arrow to the sort label. First remove the arrow from all sort
// labels
let colIdx = 0
for (let i in tableColumns) {
if (tableColumns[i].field == field) {
colIdx = i
}
tableColumns[i].name = tableColumns[i].name.replace("▲ ", "").replace("▼ ", "")
}
// Then prepend the arrow to the current sort label
if (currentSortAscending) {
tableColumns[colIdx].name = "▼ " + tableColumns[colIdx].name
} else {
tableColumns[colIdx].name = "▲ " + tableColumns[colIdx].name
}
tableColumns = tableColumns
let fieldA, fieldB
allFiles.sort((a, b) => {
fieldA = a[currentSortField]
fieldB = b[currentSortField]
if (typeof (fieldA) === "number") {
if (currentSortAscending) {
return fieldA - fieldB
} else {
return fieldB - fieldA
}
} else {
if (currentSortAscending) {
return fieldA.localeCompare(fieldB, undefined, {numeric: true})
} else {
return fieldB.localeCompare(fieldA, undefined, {numeric: true})
}
}
})
render_visible_files()
}
// Scroll event for rendering new file nodes when they become visible. For
// performance reasons the files will only be rendered once every 100ms. If a
// scroll event comes in and we're not done with the previous frame yet the
// event will be ignored
let render_timeout = false;
const onScroll = (e) => {
if (render_timeout) {
return
}
render_timeout = true
setTimeout(() => {
render_visible_files()
render_timeout = false
}, 100)
}
const render_visible_files = () => {
const fileHeight = 40
let paddingTop = directoryArea.scrollTop - directoryArea.scrollTop % fileHeight
let start = Math.floor(paddingTop / fileHeight) - 5
if (start < 0) { start = 0 }
let end = Math.ceil((paddingTop + directoryArea.clientHeight) / fileHeight) + 5
if (end > allFiles.length) { end = allFiles.length - 1 }
nodeContainer.style.paddingTop = (start * fileHeight) + "px"
// All files which have not been filtered out by the search function. We
// pretend that files with filtered == true do not exist
let totalFiles = 0
let totalSize = 0
let selectedFiles = 0
let selectedSize = 0
for (let i in allFiles) {
if (totalFiles >= start && totalFiles <= end && !allFiles[i].filtered) {
allFiles[i].visible = true
} else {
allFiles[i].visible = false
}
if (!allFiles[i].filtered) {
totalFiles++
totalSize += allFiles[i].size
if (allFiles[i].selected) {
selectedFiles++
selectedSize += allFiles[i].size
}
}
}
nodeContainer.style.height = (totalFiles * fileHeight) + "px"
statusBar = totalFiles + " items ("+formatDataVolume(totalSize, 4)+")"
if (selectedFiles !== 0) {
statusBar += ", "+selectedFiles+" selected ("+formatDataVolume(selectedSize, 4)+")"
}
}
let selectionMode = false
export const setSelectionMode = (s) => {
selectionMode = s
// When selection mode is disabled we automatically deselect all files
if (!s) {
for (let i in allFiles) {
allFiles[i].selected = false
}
render_visible_files()
}
}
let shift_pressed = false
const detect_shift = (e) => {
if (e.key !== "Shift") {
return
}
shift_pressed = e.type === "keydown"
}
export let multi_select = true
let last_selected_node = -1
const node_click = (index) => {
if (selectionMode) {
if (multi_select && shift_pressed && last_selected_node != -1) {
let id_low = last_selected_node
let id_high = last_selected_node
if (last_selected_node < index) {
id_high = index
} else {
id_low = index
}
for (let i = id_low; i <= id_high && !allFiles[i].filtered; i++) {
if (i != last_selected_node) {
allFiles[i].selected = !allFiles[i].selected
}
}
} else {
// If multi select is disabled we deselect all other files before
// selecting this one
if (!multi_select) {
for (let i in allFiles) {
allFiles[i].selected = false
}
}
allFiles[index].selected = !allFiles[index].selected
}
last_selected_node = index
render_visible_files()
} else {
window.open(allFiles[index].href, "_blank")
}
}
</script>
<svelte:window on:keydown={detect_shift} on:keyup={detect_shift} />
<div id="directory_element">
<div class="directory_sorters">
{#each tableColumns as col}
<button style="min-width: {col.width}" on:click={sortBy(col.field)} class="sorter_button">
{col.name}
</button>
{/each}
</div>
<div bind:this={directoryArea} on:scroll={onScroll} id="directory_area" class="directory_area">
<div bind:this={nodeContainer} id="node_container" class="directory_node_container">
{#each allFiles as file, index}
{#if file.visible && !file.filtered}
<a class="node"
href={file.href}
target="_blank"
rel="noreferrer"
title="{file.name}"
class:node_selected={file.selected}
on:click|preventDefault={() => {node_click(index)}}
>
<div>
<img src={file.icon} alt="thumbnail" />
<span>{file.name}</span>
</div>
<div style="width: {tableColumns[1].width}">
<span>{formatDate(new Date(file.dateCreated), true, true, false)}</span>
</div>
<div style="width: {tableColumns[2].width}">
<span>{file.sizeLabel}</span>
</div>
<div style="width: {tableColumns[3].width}">
<span>{file.type}</span>
</div>
</a>
{/if}
{/each}
</div>
</div>
<div id="footer">
{statusBar}
</div>
</div>
<style>
#directory_element {
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow: auto;
text-align: left;
}
.directory_sorters {
flex: 0 0 auto;
display: flex;
flex-direction: row;
overflow: hidden;
background: var(--body_background);
min-width: 850px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
border-bottom: 1px solid var(--separator);
}
.sorter_button {
display: inline-block;
margin: 4px 10px;
text-align: initial;
background: none;
box-shadow: none;
}
.sorter_button:hover {
background: var(--input_hover_background);
}
.directory_sorters > :first-child,
.node > :first-child {
flex-shrink: 1;
flex-grow: 1;
}
.directory_sorters > :not(:first-child),
.node > :not(:first-child) {
flex-shrink: 0;
flex-grow: 0;
}
#directory_area {
flex: 1 1 auto;
margin: 0;
padding: 0;
overflow-x: auto;
background: var(--body_background);
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
#node_container {
display: block;
min-width: 850px;
}
#footer {
flex-shrink: 0;
color: var(--background_text_color);
padding: 4px;
}
.node {
display: flex;
flex-direction: row;
position: static;
height: 40px;
overflow: hidden;
/* I use padding instead of margin here because it goves me more precise
control over the size.
Check out https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Box_Model/Mastering_margin_collapsing */
margin: 0;
color: var(--body_text_color);
text-decoration: none;
transition: background 0.2s;
}
.node:hover:not(.node_selected) {
background: var(--input_hover_background);
color: var(--input_text);
text-decoration: none;
}
.node_selected {
background: var(--highlight_background);
color: var(--highlight_text_color);
}
.node > div {
height: 100%;
overflow: hidden;
margin: auto 10px;
padding: 4px;
display: inline-block;
text-overflow: ellipsis;
white-space: nowrap;
}
.node > div > span {
margin: auto;
display: block;
text-overflow: ellipsis;
white-space: nowrap;
}
.node > div > img {
max-height: 100%;
margin-right: 6px;
width: auto;
min-width: auto;
float: left;
display: block;
}
</style>

View File

@@ -1,393 +0,0 @@
<script>
import { onMount } from "svelte";
import { formatDataVolume } from "util/Formatting";
import Modal from "util/Modal.svelte";
import Spinner from "util/Spinner.svelte";
import UploadWidget from "util/upload_widget/UploadWidget.svelte";
import DirectoryElement from "./DirectoryElement.svelte"
let loading = true
let contentType = "" // files or lists
let inputSearch
let directoryElement
let downloadFrame
let help_modal
let help_modal_visible = false
let upload_widget
let getUserFiles = () => {
loading = true
fetch(window.api_endpoint + "/user/files").then(resp => {
if (!resp.ok) { Promise.reject("yo") }
return resp.json()
}).then(resp => {
directoryElement.reset()
for (let i in resp.files) {
directoryElement.addFile(
resp.files[i].id,
window.api_endpoint + "/file/" + resp.files[i].id + "/thumbnail?width=32&height=32",
resp.files[i].name,
"/u/" + resp.files[i].id,
resp.files[i].mime_type,
resp.files[i].size,
formatDataVolume(resp.files[i].size, 4),
resp.files[i].date_upload,
)
}
directoryElement.renderFiles()
}).catch((err) => {
throw (err)
}).finally(() => {
loading = false
})
}
let getUserLists = () => {
loading = true
fetch(window.api_endpoint + "/user/lists").then(resp => {
if (!resp.ok) { Promise.reject("yo") }
return resp.json()
}).then(resp => {
directoryElement.reset()
for (let i in resp.lists) {
directoryElement.addFile(
resp.lists[i].id,
window.api_endpoint + "/list/" + resp.lists[i].id + "/thumbnail?width=32&height=32",
resp.lists[i].title,
"/l/" + resp.lists[i].id,
"list",
resp.lists[i].file_count,
resp.lists[i].file_count + " files",
resp.lists[i].date_created,
)
}
directoryElement.renderFiles()
}).catch((err) => {
throw (err)
}).finally(() => {
loading = false
})
}
const searchHandler = (e) => {
if (e.keyCode === 27) { // Escape
e.preventDefault()
inputSearch.value = ""
inputSearch.blur()
} else if (e.keyCode === 13) { // Enter
e.preventDefault()
directoryElement.searchSubmit()
return
}
requestAnimationFrame(() => {
directoryElement.search(inputSearch.value)
})
}
let initialized = false
let hashChange = () => {
if (!initialized) {
return
}
if (window.location.hash === "#lists") {
contentType = "lists"
document.title = "My Albums"
getUserLists()
resetMenu()
} else {
contentType = "files"
document.title = "My Files"
getUserFiles()
resetMenu()
}
}
let selecting = false
const toggleSelecting = () => {
selecting = !selecting
directoryElement.setSelectionMode(selecting)
}
const bulkDelete = async () => {
let selected = directoryElement.getSelectedFiles()
if (selected.length === 0) {
alert("You have not selected any files")
return
}
if (contentType === "lists") {
if (!confirm(
"You are about to delete "+selected.length+" lists. "+
"This is not reversible!\n"+
"Are you sure?"
)){ return }
} else {
if (!confirm(
"You are about to delete "+selected.length+" files. "+
"This is not reversible!\n"+
"Are you sure?"
)){ return }
}
loading = true
let endpoint = window.api_endpoint+"/file/"
if (contentType === "lists") {
endpoint = window.api_endpoint+"/list/"
}
for (let i in selected) {
try {
const resp = await fetch(
endpoint+encodeURIComponent(selected[i].id),
{ method: "DELETE" }
);
if(resp.status >= 400) {
throw new Error(resp.text())
}
} catch (err) {
alert("Delete failed: "+err)
}
}
hashChange()
}
function createList() {
let selected = directoryElement.getSelectedFiles()
if (selected.length === 0) {
alert("You have not selected any files")
return
}
let title = prompt(
"You are creating a list containing " + selected.length + " files.\n"
+ "What do you want to call it?", "My New Album"
)
if (title === null) {
return
}
let files = selected.reduce(
(acc, curr) => {
acc.push({"id": curr.id})
return acc
},
[],
)
fetch(
window.api_endpoint+"/list",
{
method: "POST",
headers: { "Content-Type": "application/json; charset=UTF-8" },
body: JSON.stringify({
"title": title,
"files": files
})
}
).then(resp => {
if (!resp.ok) {
return Promise.reject("HTTP error: " + resp.status)
}
return resp.json()
}).then(resp => {
window.open('/l/' + resp.id, '_blank')
}).catch(err => {
alert("Failed to create list. Server says this:\n"+err)
})
}
function downloadFiles() {
let selected = directoryElement.getSelectedFiles()
if (selected.length === 0) {
alert("You have not selected any files")
return
}
// Create a list of file ID's separated by commas
let ids = selected.reduce((acc, curr) => acc + curr.id + ",", "")
// Remove the last comma
ids = ids.slice(0, -1)
downloadFrame.src = window.api_endpoint+"/file/"+ids+"?download"
}
const keydown = (e) => {
if (e.ctrlKey && e.key === "f" || !e.ctrlKey && e.keyCode === 191) {
e.preventDefault()
inputSearch.focus()
}
if (e.ctrlKey || e.altKey || e.metaKey) {
return // prevent custom shortcuts from interfering with system shortcuts
}
if (document.activeElement.type && document.activeElement.type === "text") {
return // Prevent shortcuts from interfering with input fields
}
if (e.key === "i") {
help_modal.toggle()
} else if (e.key === "/") {
inputSearch.focus()
} else {
return
}
// This will only be run if a custom shortcut was triggered
e.preventDefault()
}
onMount(() => {
initialized = true
hashChange()
})
</script>
<svelte:window on:keydown={keydown} on:hashchange={hashChange} />
<div id="file_manager" class="file_manager page_margins">
<div id="nav_bar" class="nav_bar">
<button id="btn_menu" onclick="toggleMenu()"><i class="icon">menu</i></button>
<button on:click={toggleSelecting} id="btn_select" class:button_highlight={selecting}>
<i class="icon">select_all</i> Select
</button>
<input
bind:this={inputSearch}
on:keyup={searchHandler}
id="input_search"
class="input_search"
type="text"
placeholder="press / to search"
/>
{#if contentType === "files"}
<button on:click={upload_widget.pick_files} id="btn_upload" title="Upload files">
<i class="icon">cloud_upload</i>
</button>
{/if}
<button on:click={hashChange} id="btn_reload" title="Refresh file list">
<i class="icon">refresh</i>
</button>
<button on:click={() => help_modal.toggle()} class:button_highlight={help_modal_visible} title="Help">
<i class="icon">info</i>
</button>
</div>
{#if selecting}
<div class="nav_bar">
{#if contentType === "files"}
<button on:click={createList}><i class="icon">list</i> Make album</button>
<button on:click={downloadFiles}><i class="icon">download</i> Download</button>
{/if}
<button on:click={bulkDelete}><i class="icon">delete</i> Delete</button>
</div>
{/if}
{#if loading}
<div class="spinner">
<Spinner></Spinner>
</div>
{/if}
<DirectoryElement bind:this={directoryElement}></DirectoryElement>
<Modal
bind:this={help_modal}
title="File manager help"
width="600px"
on:is_visible={e => {help_modal_visible = e.detail}}
>
<div class="indent">
<p>
In the file manager you can see the files you have uploaded and
the lists you have created.
</p>
<h3>Searching</h3>
<p>
By clicking the search bar or pressing the / button you can
search through your files or lists. Only the entries matching
your search term will be shown. Pressing Enter will open the
first search result in a new tab. Pressing Escape will cancel
the search and all files will be shown again.
</p>
<h3>Bulk actions</h3>
<p>
With the Select button you can click files to select them. Once
you have made a selection you can use the buttons on the toolbar
to either create a list containing the selected files or delete
them.
</p>
<p>
Holding Shift while selecting a file will select all the files
between the file you last selected and the file you just
clicked.
</p>
</div>
</Modal>
<!-- This frame will load the download URL when a download button is pressed -->
<iframe bind:this={downloadFrame} title="File download frame" style="display: none; width: 1px; height: 1px;"></iframe>
</div>
<UploadWidget bind:this={upload_widget} drop_upload on:uploads_finished={hashChange}/>
<style>
:global(#page_body) {
height: 100vh;
padding: 0;
background: none;
}
/* Override the menu button so it doesn't overlap the file manager when the menu
is collapsed */
:global(#button_toggle_navigation) {
display: none;
}
#file_manager {
position: absolute;
padding: 0;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
flex-direction: column;
}
.nav_bar {
flex-shrink: 0;
display: flex;
flex-direction: row;
padding: 2px;
}
.nav_bar > button {
flex-shrink: 0;
}
.input_search {
flex: 1 1 auto;
min-width: 100px;
}
.spinner {
position: absolute;
display: block;
margin: auto;
max-width: 100%;
max-height: 100%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100px;
height: 100px;
}
</style>

View File

@@ -1,8 +1,7 @@
<script>
import { createEventDispatcher } from "svelte";
import { fade } from "svelte/transition";
import ProgressBar from "util/ProgressBar.svelte";
import { upload_file } from "./UploadFunc";
import { upload_file } from "util/upload_widget/UploadFunc.js";
let dispatch = createEventDispatcher()
export let job = {
@@ -44,7 +43,7 @@ export const start = () => {
</script>
<div class="upload_progress" transition:fade={{duration: 200}} class:error={job.status === "error"}>
<div class="upload_progress" class:error={job.status === "error"}>
{job.name}<br/>
{#if error_code !== ""}
{error_message}<br/>