Fork pd_web, remove everything we don't need
This commit is contained in:
282
svelte/package-lock.json
generated
282
svelte/package-lock.json
generated
@@ -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",
|
||||
|
@@ -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`,
|
||||
|
@@ -1,8 +0,0 @@
|
||||
import App from './file_viewer/FileViewer.svelte';
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById("body"),
|
||||
props: {}
|
||||
});
|
||||
|
||||
export default app;
|
@@ -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>
|
@@ -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>
|
@@ -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 ←</div><div>Previous file</div></div>
|
||||
<div><div>d or →</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>
|
@@ -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>
|
@@ -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>
|
@@ -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, "&").
|
||||
replace(/</g, "<").
|
||||
replace(/>/g, ">").
|
||||
replace(/"/g, """).
|
||||
replace(/'/g, "'");
|
||||
}
|
||||
|
||||
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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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)}/>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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}
|
@@ -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>
|
@@ -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}
|
@@ -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}
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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}
|
@@ -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}>
|
||||
|
@@ -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) {
|
||||
|
@@ -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"
|
||||
|
@@ -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 () => {
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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})}
|
||||
|
@@ -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>
|
@@ -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%;
|
||||
|
@@ -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})}
|
||||
|
@@ -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}/>
|
||||
|
@@ -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>
|
||||
|
||||
|
@@ -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";
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
|
172
svelte/src/home_page/Pricing.svelte
Normal file
172
svelte/src/home_page/Pricing.svelte
Normal 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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -1,8 +0,0 @@
|
||||
import App from './text_upload/TextUpload.svelte';
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById("body"),
|
||||
props: {}
|
||||
});
|
||||
|
||||
export default app;
|
@@ -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>
|
@@ -1,8 +0,0 @@
|
||||
import App from './upload_history/UploadHistory.svelte';
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById("page_body"),
|
||||
props: {}
|
||||
});
|
||||
|
||||
export default app;
|
@@ -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&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>
|
@@ -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>
|
@@ -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",
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
@@ -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>
|
||||
|
@@ -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",
|
||||
|
@@ -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>
|
@@ -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>
|
@@ -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/>
|
||||
|
Reference in New Issue
Block a user