Embed resources into templates

This commit is contained in:
2019-12-23 23:56:57 +01:00
parent 269bf7eed1
commit 7b5723705c
87 changed files with 12669 additions and 7216 deletions

View File

@@ -4,8 +4,7 @@ type PixelWebConfig struct {
APIURLExternal string `toml:"api_url_external"`
APIURLInternal string `toml:"api_url_internal"`
SessionCookieDomain string `toml:"session_cookie_domain"`
StaticResourceDir string `toml:"static_resource_dir"`
TemplateDir string `toml:"template_dir"`
ResourceDir string `toml:"resource_dir"`
DebugMode bool `toml:"debug_mode"`
MaintenanceMode bool `toml:"maintenance_mode"`
}
@@ -15,8 +14,7 @@ const DefaultConfig = `# Pixeldrain Web UI server configuration
api_url_external = "/api" # Used in the web browser
api_url_internal = "http://127.0.0.1:8080" # Used for internal API requests to the pixeldrain server, not visible to users
session_cookie_domain = ".pixeldrain.com"
static_resource_dir = "res/static"
template_dir = "res/template"
resource_dir = "res"
debug_mode = false
maintenance_mode = false
`

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 295 B

View File

Before

Width:  |  Height:  |  Size: 252 B

After

Width:  |  Height:  |  Size: 252 B

View File

Before

Width:  |  Height:  |  Size: 751 B

After

Width:  |  Height:  |  Size: 751 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 770 B

After

Width:  |  Height:  |  Size: 770 B

View File

Before

Width:  |  Height:  |  Size: 523 B

After

Width:  |  Height:  |  Size: 523 B

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 556 B

After

Width:  |  Height:  |  Size: 556 B

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 912 B

After

Width:  |  Height:  |  Size: 912 B

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 966 B

After

Width:  |  Height:  |  Size: 966 B

View File

Before

Width:  |  Height:  |  Size: 885 B

After

Width:  |  Height:  |  Size: 885 B

View File

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 220 KiB

View File

@@ -0,0 +1,33 @@
<svg version="1.1"
class="svg-loader"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 80 80"
xml:space="preserve">
<path
fill="#D43B11"
d="M10,40c0,0,0-0.4,0-1.1c0-0.3,0-0.8,0-1.3c0-0.3,0-0.5,0-0.8c0-0.3,0.1-0.6,0.1-0.9c0.1-0.6,0.1-1.4,0.2-2.1
c0.2-0.8,0.3-1.6,0.5-2.5c0.2-0.9,0.6-1.8,0.8-2.8c0.3-1,0.8-1.9,1.2-3c0.5-1,1.1-2,1.7-3.1c0.7-1,1.4-2.1,2.2-3.1
c1.6-2.1,3.7-3.9,6-5.6c2.3-1.7,5-3,7.9-4.1c0.7-0.2,1.5-0.4,2.2-0.7c0.7-0.3,1.5-0.3,2.3-0.5c0.8-0.2,1.5-0.3,2.3-0.4l1.2-0.1
l0.6-0.1l0.3,0l0.1,0l0.1,0l0,0c0.1,0-0.1,0,0.1,0c1.5,0,2.9-0.1,4.5,0.2c0.8,0.1,1.6,0.1,2.4,0.3c0.8,0.2,1.5,0.3,2.3,0.5
c3,0.8,5.9,2,8.5,3.6c2.6,1.6,4.9,3.4,6.8,5.4c1,1,1.8,2.1,2.7,3.1c0.8,1.1,1.5,2.1,2.1,3.2c0.6,1.1,1.2,2.1,1.6,3.1
c0.4,1,0.9,2,1.2,3c0.3,1,0.6,1.9,0.8,2.7c0.2,0.9,0.3,1.6,0.5,2.4c0.1,0.4,0.1,0.7,0.2,1c0,0.3,0.1,0.6,0.1,0.9
c0.1,0.6,0.1,1,0.1,1.4C74,39.6,74,40,74,40c0.2,2.2-1.5,4.1-3.7,4.3s-4.1-1.5-4.3-3.7c0-0.1,0-0.2,0-0.3l0-0.4c0,0,0-0.3,0-0.9
c0-0.3,0-0.7,0-1.1c0-0.2,0-0.5,0-0.7c0-0.2-0.1-0.5-0.1-0.8c-0.1-0.6-0.1-1.2-0.2-1.9c-0.1-0.7-0.3-1.4-0.4-2.2
c-0.2-0.8-0.5-1.6-0.7-2.4c-0.3-0.8-0.7-1.7-1.1-2.6c-0.5-0.9-0.9-1.8-1.5-2.7c-0.6-0.9-1.2-1.8-1.9-2.7c-1.4-1.8-3.2-3.4-5.2-4.9
c-2-1.5-4.4-2.7-6.9-3.6c-0.6-0.2-1.3-0.4-1.9-0.6c-0.7-0.2-1.3-0.3-1.9-0.4c-1.2-0.3-2.8-0.4-4.2-0.5l-2,0c-0.7,0-1.4,0.1-2.1,0.1
c-0.7,0.1-1.4,0.1-2,0.3c-0.7,0.1-1.3,0.3-2,0.4c-2.6,0.7-5.2,1.7-7.5,3.1c-2.2,1.4-4.3,2.9-6,4.7c-0.9,0.8-1.6,1.8-2.4,2.7
c-0.7,0.9-1.3,1.9-1.9,2.8c-0.5,1-1,1.9-1.4,2.8c-0.4,0.9-0.8,1.8-1,2.6c-0.3,0.9-0.5,1.6-0.7,2.4c-0.2,0.7-0.3,1.4-0.4,2.1
c-0.1,0.3-0.1,0.6-0.2,0.9c0,0.3-0.1,0.6-0.1,0.8c0,0.5-0.1,0.9-0.1,1.3C10,39.6,10,40,10,40z">
<animateTransform
attributeType="xml"
attributeName="transform"
type="rotate"
from="0 40 40"
to="360 40 40"
dur="0.6s"
repeatCount="indefinite"/>
</path>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -182,7 +182,7 @@ var ListNavigator = {
btnDownloadList.setAttribute("onClick", "Toolbar.downloadList();");
var btnDownloadListImg = document.createElement("img");
btnDownloadListImg.setAttribute("src", "/res/img/floppy_small.png");
btnDownloadListImg.setAttribute("src", "{{template `floppy_small.png`}}");
btnDownloadListImg.setAttribute("alt", "Download List");
var btnDownloadListText = document.createElement("span");
@@ -199,7 +199,7 @@ var ListNavigator = {
btnShuffle.setAttribute("onClick", "ListNavigator.toggleShuffle();");
var btnShuffleImg = document.createElement("img");
btnShuffleImg.setAttribute("src", "/res/img/shuffle_small.png");
btnShuffleImg.setAttribute("src", "{{template `shuffle_small.png`}}");
btnShuffleImg.setAttribute("alt", "Shuffle playback order");
var btnShuffleText = document.createElement("span");

View File

@@ -0,0 +1,385 @@
var FinishedUpload = /** @class */ (function () {
function FinishedUpload() {
}
return FinishedUpload;
}());
var uploader = null;
var finishedUploads = new Array();
var totalUploads = 0;
var queueDiv = document.getElementById("uploads_queue");
var UploadProgressBar = /** @class */ (function () {
function UploadProgressBar(file) {
this.file = file;
this.name = file.name;
this.queueNum = totalUploads;
this.uploadDiv = document.createElement("a");
totalUploads++;
this.uploadDiv.classList.add("file_button");
this.uploadDiv.style.opacity = "0";
this.uploadDiv.innerText = "Queued\n" + this.file.name;
queueDiv.appendChild(this.uploadDiv);
// Browsers don't render the transition if the opacity is set and
// updated in the same frame. So we have to wait a frame (or more)
// before changing the opacity to make sure the transition triggers
var d = this.uploadDiv; // `this` stops working after constructor ends
window.setTimeout(function () { d.style.opacity = "1"; }, 100);
}
UploadProgressBar.prototype.onProgress = function (progress) {
this.uploadDiv.innerText = "Uploading... " + Math.round(progress * 1000) / 10 + "%\n" + this.file.name;
this.uploadDiv.style.background = 'linear-gradient('
+ 'to right, '
+ 'var(--file_background_color) 0%, '
+ 'var(--highlight_color) ' + ((progress * 100)) + '%, '
+ 'var(--file_background_color) ' + ((progress * 100) + 1) + '%)';
};
UploadProgressBar.prototype.onFinished = function (id) {
finishedUploads[this.queueNum] = {
id: id,
name: this.file.name
};
this.uploadDiv.style.background = 'var(--file_background_color)';
this.uploadDiv.href = '/u/' + id;
this.uploadDiv.target = "_blank";
var fileImg = document.createElement("img");
fileImg.src = apiEndpoint + '/file/' + id + '/thumbnail';
fileImg.alt = this.file.name;
var linkSpan = document.createElement("span");
linkSpan.style.color = "var(--highlight_color)";
linkSpan.innerText = window.location.hostname + "/u/" + id;
this.uploadDiv.innerHTML = ""; // Remove uploading progress
this.uploadDiv.appendChild(fileImg);
this.uploadDiv.appendChild(document.createTextNode(this.file.name));
this.uploadDiv.appendChild(document.createElement("br"));
this.uploadDiv.appendChild(linkSpan);
};
UploadProgressBar.prototype.onFailure = function (error) {
this.uploadDiv.innerHTML = ""; // Remove uploading progress
this.uploadDiv.style.background = 'var(--danger_color)';
this.uploadDiv.appendChild(document.createTextNode(this.file.name));
this.uploadDiv.appendChild(document.createElement("br"));
this.uploadDiv.appendChild(document.createTextNode("Upload failed after three tries:"));
this.uploadDiv.appendChild(document.createElement("br"));
this.uploadDiv.appendChild(document.createTextNode(error));
};
return UploadProgressBar;
}());
function handleUploads(files) {
if (uploader === null) {
uploader = new UploadManager();
}
for (var i = 0; i < files.length; i++) {
uploader.uploadFile(new UploadProgressBar(files.item(i)));
}
}
// List creation
function createList(title, anonymous) {
if (uploader.uploading()) {
var cont = confirm("Some files have not finished uploading yet. Creating a list now " +
"will exclude those files.\n\nContinue?");
if (!cont) {
return;
}
}
var postData = {
"title": title,
"anonymous": anonymous,
"files": new Array()
};
for (var i = 0; i < finishedUploads.length; i++) {
postData.files.push({
"id": finishedUploads[i].id
});
}
var xhr = new XMLHttpRequest();
xhr.open("POST", apiEndpoint + "/list");
xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) {
return;
}
var resp = JSON.parse(xhr.response);
if (xhr.status < 400) {
// Request is a success
var div = document.createElement("div");
div.className = "file_button";
div.innerHTML = '<img src="' + apiEndpoint + '/list/' + resp.id + '/thumbnail"/>'
+ "List creation finished!<br/>"
+ title + "<br/>"
+ '<a href="/l/' + resp.id + '" target="_blank">' + window.location.hostname + '/l/' + resp.id + '</a>';
document.getElementById("uploads_queue").appendChild(div);
window.open('/l/' + resp.id, '_blank');
}
else {
console.log("status: " + xhr.status + " response: " + xhr.response);
var div = document.createElement("div");
div.className = "file_button";
div.innerHTML = "List creation failed<br/>"
+ "The server responded with:<br/>"
+ resp.message;
document.getElementById("uploads_queue").append(div);
}
};
xhr.send(JSON.stringify(postData));
}
// Form upload handlers
// Relay click event to hidden file field
document.getElementById("select_file_button").onclick = function () {
document.getElementById("file_input_field").click();
};
document.getElementById("file_input_field").onchange = function (evt) {
handleUploads(evt.target.files);
// This resets the file input field
document.getElementById("file_input_field").nodeValue = "";
};
/*
* Drag 'n Drop upload handlers
*/
document.ondragover = function (e) {
e.preventDefault();
e.stopPropagation();
};
document.ondragenter = function (e) {
e.preventDefault();
e.stopPropagation();
};
document.addEventListener('drop', function (e) {
if (e.dataTransfer && e.dataTransfer.files.length > 0) {
e.preventDefault();
e.stopPropagation();
handleUploads(e.dataTransfer.files);
}
});
function copyText(text) {
// Create a textarea to copy the text from
var ta = document.createElement("textarea");
ta.setAttribute("readonly", "readonly");
ta.style.position = "absolute";
ta.style.left = "-9999px";
ta.value = text; // Put the text in the textarea
// Add the textarea to the DOM so it can be seleted by the user
document.body.appendChild(ta);
ta.select(); // Select the contents of the textarea
var success = document.execCommand("copy"); // Copy the selected text
document.body.removeChild(ta); // Remove the textarea
return success;
}
// Create list button
document.getElementById("btn_create_list").addEventListener("click", function (evt) {
var title = prompt("You are creating a list containing " + finishedUploads.length + " files.\n"
+ "What do you want to call it?", "My New Album");
if (title === null) {
return;
}
createList(title, false);
});
var btnCopyLinks = document.getElementById("btn_copy_links");
btnCopyLinks.addEventListener("click", function () {
var text = "";
// Add the text to the textarea
for (var i = 0; i < finishedUploads.length; i++) {
// Example: https://pixeldrain.com/u/abcd1234: Some_file.png
text += window.location.protocol + "//" + window.location.hostname + "/u/" + finishedUploads[i].id +
" " + finishedUploads[i].name + "\n";
}
var defaultButtonText = btnCopyLinks.innerHTML;
// Copy the selected text
if (copyText(text)) {
btnCopyLinks.classList.add("button_highlight");
btnCopyLinks.innerHTML = "Links copied to clipboard!";
// Return to normal
setTimeout(function () {
btnCopyLinks.innerHTML = defaultButtonText;
btnCopyLinks.classList.remove("button_highlight");
}, 60000);
}
else {
btnCopyLinks.classList.add("button_red");
btnCopyLinks.innerHTML = "Copying links failed";
setTimeout(function () {
btnCopyLinks.innerHTML = defaultButtonText;
btnCopyLinks.classList.remove("button_red");
}, 60000);
}
});
var btnCopyBBCode = document.getElementById("btn_copy_bbcode");
btnCopyBBCode.addEventListener("click", function () {
var text = "";
// Add the text to the textarea
for (var i = 0; i < finishedUploads.length; i++) {
// Example: [url=https://pixeldrain.com/u/abcd1234]Some_file.png[/url]
text += "[url=" + window.location.protocol + "//" + window.location.hostname +
"/u/" + finishedUploads[i].id + "]" +
finishedUploads[i].name + "[/url]\n";
}
var defaultButtonText = btnCopyBBCode.innerHTML;
// Copy the selected text
if (copyText(text)) {
btnCopyBBCode.classList.add("button_highlight");
btnCopyBBCode.innerHTML = "BBCode copied to clipboard!";
// Return to normal
setTimeout(function () {
btnCopyBBCode.innerHTML = defaultButtonText;
btnCopyBBCode.classList.remove("button_highlight");
}, 60000);
}
else {
btnCopyBBCode.classList.add("button_red");
btnCopyBBCode.innerHTML = "Copying links failed";
setTimeout(function () {
btnCopyBBCode.innerHTML = defaultButtonText;
btnCopyBBCode.classList.remove("button_red");
}, 60000);
}
});
var Cookie;
(function (Cookie) {
function read(name) {
var result = new RegExp('(?:^|; )' + encodeURIComponent(name) + '=([^;]*)').exec(document.cookie);
return result ? result[1] : null;
}
Cookie.read = read;
function write(name, value, days) {
if (!days) {
days = 365 * 20;
}
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
var expires = "; expires=" + date.toUTCString();
document.cookie = name + "=" + value + expires + "; path=/";
}
Cookie.write = write;
function remove(name) {
write(name, "", -1);
}
Cookie.remove = remove;
})(Cookie || (Cookie = {}));
var UploadManager = /** @class */ (function () {
function UploadManager() {
this.uploadQueue = new Array();
this.uploadThreads = new Array();
this.maxThreads = 3;
}
UploadManager.prototype.uploadFile = function (file) {
console.debug("Adding upload to queue");
this.uploadQueue.push(file);
if (this.uploadThreads.length < this.maxThreads) {
console.debug("Starting upload thread");
var thread_1 = new UploadWorker(this);
this.uploadThreads.push(thread_1);
setTimeout(function () { thread_1.start(); }, 0); // Start a new upload thread
}
else {
for (var i = 0; i < this.uploadThreads.length; i++) {
this.uploadThreads[i].start();
}
}
};
UploadManager.prototype.uploading = function () {
for (var i = 0; i < this.uploadThreads.length; i++) {
if (this.uploadThreads[i].isUploading()) {
return true;
}
}
return false;
};
UploadManager.prototype.grabFile = function () {
if (this.uploadQueue.length > 0) {
return this.uploadQueue.shift();
}
else {
return undefined;
}
};
return UploadManager;
}());
var UploadWorker = /** @class */ (function () {
function UploadWorker(manager) {
this.tries = 0;
this.uploading = false;
this.manager = manager;
}
UploadWorker.prototype.isUploading = function () { return this.uploading; };
UploadWorker.prototype.start = function () {
if (!this.uploading) {
this.newFile();
}
};
UploadWorker.prototype.newFile = function () {
var file = this.manager.grabFile();
if (file === undefined) { // No more files in the queue. We're finished
this.uploading = false;
console.debug("No files left in queue");
return; // Stop the thread
}
this.uploading = true;
this.tries = 0;
this.upload(file);
};
UploadWorker.prototype.upload = function (file) {
console.debug("Starting upload of " + file.name);
var that = this; // jquery changes the definiton of "this"
var formData = new FormData();
formData.append("name", file.name);
formData.append('file', file.file);
var xhr = new XMLHttpRequest();
xhr.open("POST", apiEndpoint + "/file");
xhr.timeout = 21600000; // 6 hours, to account for slow connections
// Update progess bar on progress
xhr.upload.addEventListener("progress", function (evt) {
if (evt.lengthComputable) {
file.onProgress(evt.loaded / evt.total);
}
});
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) {
return;
}
console.log("status: " + xhr.status);
if (xhr.status >= 100 && xhr.status < 400) {
var resp = JSON.parse(xhr.response);
// Request is a success
file.onFinished(resp.id);
that.setHistoryCookie(resp.id);
that.newFile(); // Continue uploading on this thread
}
else {
var value, message;
if (xhr.status >= 400) {
var resp = JSON.parse(xhr.response);
value = resp.value;
message = resp.message;
}
console.log("Upload error. status: " + xhr.status + " response: " + xhr.response);
if (that.tries === 3) {
file.onFailure(value, message);
setTimeout(function () { that.newFile(); }, 2000); // Try to continue
return; // Upload failed
}
// Try again
that.tries++;
setTimeout(function () { that.upload(file); }, that.tries * 5000);
}
};
xhr.send(formData);
};
UploadWorker.prototype.setHistoryCookie = function (id) {
// Make sure the user is not logged in, for privacy. This keeps the
// files uploaded while logged in and anonymously uploaded files
// separated
if (Cookie.read("pd_auth_key") !== null) {
return;
}
var uc = Cookie.read("pduploads");
// First upload in this browser
if (uc === null) {
Cookie.write("pduploads", id + ".", undefined);
return;
}
if (uc.length > 2000) {
// Cookie is becoming too long, drop the oldest two files
uc = uc.substring(uc.indexOf(".") + 1).substring(uc.indexOf(".") + 1);
}
Cookie.write("pduploads", uc + id + ".", undefined);
};
return UploadWorker;
}());

View File

@@ -10,44 +10,29 @@
/* Fonts */
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 400;
font-display: fallback;
text-rendering: optimizeLegibility;
src:
local('Ubuntu'),
local('Ubuntu Regular'),
local('Ubuntu, Regular'),
local('Ubuntu-Regular'),
url(/res/misc/Ubuntu-R.ttf) format('truetype');
}
@font-face {
font-family: 'Lato Thin';
font-family: 'default';
font-display: fallback;
font-style: normal;
font-weight: normal;
text-rendering: optimizeLegibility;
src:
local('Lato Thin'),
local('Lato, Thin'),
local('Lato-Thin'),
local('Lato Hairline'),
local('Lato, Hairline'),
local('Lato-Hairline'),
url(/res/misc/LatoLatin-Thin.ttf) format('truetype');
local('Cantarell'),
local('Cantarell Regular'),
local('Cantarell, Regular'),
local('Cantarell-Regular'),
url("/res/misc/Cantarell-Regular.otf") format("opentype");
}
@font-face {
font-family: 'Lato';
font-family: 'light';
font-display: fallback;
font-style: normal;
font-weight: normal;
text-rendering: optimizeLegibility;
src:
local('Lato Regular'),
local('Lato, Regular'),
local('Lato-Regular'),
url(/res/misc/Lato-Regular.ttf) format('truetype');
local('Cantarell Light'),
local('Cantarell, Light'),
local('Cantarell-Light'),
url("/res/misc/Cantarell-Light.otf") format("opentype");
}
/* Page rendering configuration */
@@ -58,7 +43,7 @@ body{
background-color: #0d0d0d; /* Fallback */
background-color: var(--body_color);
background-repeat: repeat;
font-family: 'Ubuntu';
font-family: "default";
margin: 0;
line-height: 1.5em;
color: #bfbfbf; /* Fallback */
@@ -174,11 +159,10 @@ body{
color: #bfbfbf; /* Fallback */
color: var(--text_color);
text-align: center;
padding: 4px 6px;
padding: 6px 6px;
margin: 0.3em 15px 0.3em 15px;
font-family: "Lato Thin", sans-serif;
font-weight: bold;
font-size: 1.5em;
font-family: "light";
font-size: 1.6em;
overflow: hidden;
text-overflow: ellipsis;
transition: background-color 0.5s;
@@ -197,6 +181,9 @@ body{
text-shadow: 0 0 20px #000000;
padding: 30px 10px 30px 10px;
}
body, .checkers {
background-image: url("{{bgPattern}}");
}
.highlight_dark,
.highlight_middle,
@@ -255,12 +242,12 @@ h1, h2, h3, h4, h5, h6 {
margin-left: 10px;
margin-right: 10px;
}
h1{font-size: 2em; font-family: "Lato Thin";}
h2{font-size: 1.75em; font-family: "Lato Thin";}
h3{font-size: 1.5em; font-family: "Lato Thin";}
h4{font-size: 1.25em; font-family: "Lato";}
h5{font-size: 1em; font-family: "Lato";}
h6{font-size: .75em; font-family: "Lato";}
h1{font-size: 2em; font-family: "light"; font-weight: normal;}
h2{font-size: 1.75em; font-family: "light"; font-weight: normal;}
h3{font-size: 1.5em; font-family: "light"; font-weight: normal;}
h4{font-size: 1.25em; font-family: "default"; font-weight: normal;}
h5{font-size: 1em; font-family: "default"; font-weight: normal;}
h6{font-size: .75em; font-family: "default"; font-weight: normal;}
h3, h2{border-bottom: 1px var(--layer_3_color_border) solid;} /* Differentiate it a bit, else it just looks like bold text */
p, .indent {
@@ -325,7 +312,6 @@ pre{
margin: 10px !important;
border-radius: 5px;
font-size: 1.8em;
font-weight: normal;
}
.progress_bar{
@@ -416,7 +402,6 @@ select{
line-height: 1em;
overflow: hidden;
text-decoration: none;
font-family: inherit;
color: #bfbfbf; /* Fallback */
color: var(--input_text_color);
outline: 0;
@@ -479,7 +464,6 @@ input[type="number"]{
padding: 3px 5px;
color: var(--input_text_color);
height: 26px;
font-family: 'Ubuntu', sans-serif;
font-size: 1em;
vertical-align: middle;
outline: 0;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,4 +0,0 @@
All files in this directory are compiled typescript files. You can find the
sources in /res/typescript.
Have fun typing!

View File

@@ -89,39 +89,68 @@ function createList(title, anonymous) {
"id": finishedUploads[i].id
});
}
$.ajax({
url: "/api/list",
contentType: "application/json",
method: "POST",
data: JSON.stringify(postData),
dataType: "json",
success: function (response) {
var xhr = new XMLHttpRequest();
xhr.open("POST", apiEndpoint + "/list");
xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) {
return;
}
if (xhr.status == 200 || xhr.status == 0) {
// Request is a success
var resultString = "<div class=\"file_button\">"
+ '<img src="' + apiEndpoint + '/list/' + response.id + '/thumbnail"/>'
+ '<img src="' + apiEndpoint + '/list/' + xhr.response.id + '/thumbnail"/>'
+ "List creation finished!<br/>"
+ title + "<br/>"
+ "<a href=\"/l/" + response.id + "\" target=\"_blank\">" + window.location.hostname + "/l/" + response.id + "</a>"
+ "<a href=\"/l/" + xhr.response.id + "\" target=\"_blank\">" + window.location.hostname + "/l/" + xhr.response.id + "</a>"
+ "</div>";
$('#uploads_queue').append($(resultString).hide().fadeIn('slow').css("display", ""));
$("#uploads_queue").animate({
scrollTop: $("#uploads_queue").prop("scrollHeight")
}, 1000);
window.open('/l/' + response.id, '_blank');
},
error: function (xhr, status, error) {
console.log("xhr:");
console.log(xhr);
console.log("status:");
console.log(status);
console.log("error:");
console.log(error);
document.getElementById("uploads_queue").append(resultString);
window.open('/l/' + xhr.response.id, '_blank');
}
else {
console.log("status: " + xhr.status + " response: " + xhr.response);
var resultString = "<div class=\"file_button\">List creation failed<br/>"
+ "The server responded with this: <br/>"
+ xhr.responseJSON.message
+ xhr.response.message
+ "</div>";
$('#uploads_queue').append($(resultString).hide().fadeIn('slow').css("display", ""));
document.getElementById("uploads_queue").append(resultString);
}
});
};
xhr.send(JSON.stringify(postData));
// $.ajax({
// url: "/api/list",
// contentType: "application/json",
// method: "POST",
// data: JSON.stringify(postData),
// dataType: "json",
// success: function(response) {
// var resultString = "<div class=\"file_button\">"
// + '<img src="'+apiEndpoint+'/list/'+response.id+'/thumbnail"/>'
// + "List creation finished!<br/>"
// + title + "<br/>"
// + "<a href=\"/l/" + response.id + "\" target=\"_blank\">"+window.location.hostname+"/l/" + response.id + "</a>"
// + "</div>";
// $('#uploads_queue').append(
// $(resultString).hide().fadeIn('slow').css("display", "")
// );
// window.open('/l/'+response.id, '_blank');
// },
// error: function(xhr, status, error) {
// console.log("xhr:");
// console.log(xhr);
// console.log("status:");
// console.log(status);
// console.log("error:");
// console.log(error);
// var resultString = "<div class=\"file_button\">List creation failed<br/>"
// + "The server responded with this: <br/>"
// + xhr.responseJSON.message
// + "</div>";
// $('#uploads_queue').append(
// $(resultString).hide().fadeIn('slow').css("display", "")
// );
// }
// });
}
// Form upload handlers
// Relay click event to hidden file field
@@ -319,39 +348,35 @@ var UploadWorker = /** @class */ (function () {
};
UploadWorker.prototype.upload = function (file) {
console.debug("Starting upload of " + file.name);
var that = this; // jquery changes the definiton of "this"
var formData = new FormData();
formData.append("name", file.name);
formData.append('file', file.file);
var that = this; // jquery changes the definiton of "this"
$.ajax({
type: 'POST',
url: apiEndpoint + "/file",
data: formData,
timeout: 21600000,
cache: false,
async: true,
crossDomain: false,
contentType: false,
processData: false,
xhr: function () {
var xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", function (evt) {
xhr.open("POST", apiEndpoint + "/file");
xhr.timeout = 21600000; // 6 hours, to account for slow connections
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
// Update progess bar on progress
xhr.onprogress = function (evt) {
if (evt.lengthComputable) {
file.onProgress(evt.loaded / evt.total);
}
}, false);
return xhr;
},
success: function (data) {
file.onFinished(data.id);
that.setHistoryCookie(data.id);
console.log("Done: " + data.id);
};
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) {
return;
}
if (xhr.status == 200 || xhr.status == 0) {
// Request is a success
file.onFinished(xhr.response.id);
that.setHistoryCookie(xhr.response.id);
console.log("Done: " + xhr.response.id);
that.newFile(); // Continue uploading on this thread
},
error: function (xhr, status, error) {
console.log("status: " + status + " error: " + error);
}
else {
console.log("status: " + xhr.status + " response: " + xhr.response);
if (that.tries === 3) {
file.onFailure(status, error);
file.onFailure(xhr.response.value, xhr.response.message);
setTimeout(function () { that.newFile(); }, 2000); // Try to continue
return; // Upload failed
}
@@ -359,7 +384,45 @@ var UploadWorker = /** @class */ (function () {
that.tries++;
setTimeout(function () { that.upload(file); }, that.tries * 3000);
}
});
};
xhr.send(formData);
// $.ajax({
// type: 'POST',
// url: apiEndpoint+"/file",
// data: formData,
// timeout: 21600000, // 6 hours, to account for slow connections
// cache: false,
// async: true,
// crossDomain: false,
// contentType: false,
// processData: false,
// xhr: function () {
// var xhr = new XMLHttpRequest();
// xhr.upload.addEventListener("progress", function (evt) {
// if (evt.lengthComputable) {
// file.onProgress(evt.loaded / evt.total)
// }
// }, false);
// return xhr;
// },
// success: function (data) {
// file.onFinished(data.id)
// that.setHistoryCookie(data.id)
// console.log("Done: " + data.id)
// that.newFile() // Continue uploading on this thread
// },
// error: function (xhr, status, error){
// console.log("status: "+status+" error: "+error)
// if (that.tries === 3) {
// file.onFailure(status, error)
// setTimeout(function(){that.newFile()}, 2000) // Try to continue
// return; // Upload failed
// }
// // Try again
// that.tries++
// setTimeout(function(){that.upload(file)}, that.tries*3000)
// }
// });
};
UploadWorker.prototype.setHistoryCookie = function (id) {
// Make sure the user is not logged in, for privacy. This keeps the

View File

@@ -68,6 +68,7 @@ class UploadProgressBar implements FileUpload {
this.uploadDiv.appendChild(linkSpan)
}
public onFailure(error: string) {
this.uploadDiv.innerHTML = "" // Remove uploading progress
this.uploadDiv.style.background = 'var(--danger_color)'
this.uploadDiv.appendChild(document.createTextNode(this.file.name))
this.uploadDiv.appendChild(document.createElement("br"))
@@ -109,43 +110,35 @@ function createList(title: string, anonymous: boolean){
});
}
$.ajax({
url: "/api/list",
contentType: "application/json",
method: "POST",
data: JSON.stringify(postData),
dataType: "json",
success: function(response) {
var resultString = "<div class=\"file_button\">"
+ '<img src="'+apiEndpoint+'/list/'+response.id+'/thumbnail"/>'
var xhr = new XMLHttpRequest()
xhr.open("POST", apiEndpoint+"/list")
xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8")
xhr.onreadystatechange = function(){
if (xhr.readyState !== 4) {return;}
var resp = JSON.parse(xhr.response);
if (xhr.status < 400) {
// Request is a success
var div = document.createElement("div")
div.className = "file_button";
div.innerHTML = '<img src="'+apiEndpoint+'/list/'+resp.id+'/thumbnail"/>'
+ "List creation finished!<br/>"
+ title + "<br/>"
+ "<a href=\"/l/" + response.id + "\" target=\"_blank\">"+window.location.hostname+"/l/" + response.id + "</a>"
+ "</div>";
$('#uploads_queue').append(
$(resultString).hide().fadeIn('slow').css("display", "")
);
$("#uploads_queue").animate({
scrollTop: $("#uploads_queue").prop("scrollHeight")
}, 1000);
window.open('/l/'+response.id, '_blank');
},
error: function(xhr, status, error) {
console.log("xhr:");
console.log(xhr);
console.log("status:");
console.log(status);
console.log("error:");
console.log(error);
var resultString = "<div class=\"file_button\">List creation failed<br/>"
+ "The server responded with this: <br/>"
+ xhr.responseJSON.message
+ "</div>";
$('#uploads_queue').append(
$(resultString).hide().fadeIn('slow').css("display", "")
);
+ '<a href="/l/'+resp.id+'" target="_blank">'+window.location.hostname+'/l/'+resp.id+'</a>';
document.getElementById("uploads_queue").appendChild(div);
window.open('/l/'+resp.id, '_blank');
} else {
console.log("status: "+xhr.status+" response: "+xhr.response)
var div = document.createElement("div")
div.className = "file_button";
div.innerHTML = "List creation failed<br/>"
+ "The server responded with:<br/>"
+ resp.message;
document.getElementById("uploads_queue").append(div);
}
});
}
xhr.send(JSON.stringify(postData));
}
// Form upload handlers

View File

@@ -1,11 +1,10 @@
{
"compilerOptions": {
"outFile": "../../script/compiled/home.js"
"outFile": "../../../include/script/compiled/home.js"
},
"files": [
"home.ts",
"../lib/cookie.ts",
"../lib/jquery.d.ts",
"../lib/uploader.ts"
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ interface FileUpload {
name: string
onProgress(progress: number)
onFinished(id: string)
onFailure(response: JQuery.Ajax.ErrorTextStatus, error: string)
onFailure(errorID: string, errorMessage: string)
}
class UploadManager {
@@ -84,37 +84,38 @@ class UploadWorker {
formData.append("name", file.name)
formData.append('file', file.file)
$.ajax({
type: 'POST',
url: apiEndpoint+"/file",
data: formData,
timeout: 21600000, // 6 hours, to account for slow connections
cache: false,
async: true,
crossDomain: false,
contentType: false,
processData: false,
xhr: function () {
var xhr = new XMLHttpRequest();
var xhr = new XMLHttpRequest()
xhr.open("POST", apiEndpoint+"/file")
xhr.timeout = 21600000 // 6 hours, to account for slow connections
// Update progess bar on progress
xhr.upload.addEventListener("progress", function (evt) {
if (evt.lengthComputable) {
file.onProgress(evt.loaded / evt.total)
}
}, false);
return xhr;
},
success: function (data) {
file.onFinished(data.id)
that.setHistoryCookie(data.id)
console.log("Done: " + data.id)
});
xhr.onreadystatechange = function(){
if (xhr.readyState !== 4) {return;}
console.log("status: "+xhr.status)
if (xhr.status >= 100 && xhr.status < 400) {
var resp = JSON.parse(xhr.response);
// Request is a success
file.onFinished(resp.id)
that.setHistoryCookie(resp.id)
that.newFile() // Continue uploading on this thread
},
error: function (xhr, status, error){
console.log("status: "+status+" error: "+error)
} else {
var value, message
if (xhr.status >= 400) {
var resp = JSON.parse(xhr.response);
value = resp.value
message = resp.message
}
console.log("Upload error. status: "+xhr.status+" response: "+xhr.response)
if (that.tries === 3) {
file.onFailure(status, error)
file.onFailure(value, message)
setTimeout(function(){that.newFile()}, 2000) // Try to continue
return; // Upload failed
@@ -122,9 +123,11 @@ class UploadWorker {
// Try again
that.tries++
setTimeout(function(){that.upload(file)}, that.tries*3000)
setTimeout(function(){that.upload(file)}, that.tries*5000)
}
});
}
xhr.send(formData)
}
private setHistoryCookie(id: string){

View File

@@ -1 +0,0 @@
yo

View File

@@ -4,7 +4,6 @@
<head>
{{template "meta_tags" "Administrator panel"}}
{{template "user_style" .}}
<script type="text/javascript">var apiEndpoint = '{{.APIEndpoint}}';</script>
</head>
<body>
{{$isAdmin := .PixelAPI.UserIsAdmin}}
@@ -24,9 +23,11 @@
<canvas id="bandwidth_chart"></canvas>
</div>
<script src="/res/script/Chart.min.js"></script>
<script src="/res/script/jquery.js"></script>
<script src="/res/misc/chartjs/Chart.min.js"></script>
<script>
var apiEndpoint = '{{.APIEndpoint}}';
Chart.defaults.global.defaultFontColor = "#b3b3b3";
Chart.defaults.global.defaultFontSize = 15;
Chart.defaults.global.defaultFontFamily = "Ubuntu";

View File

@@ -6,19 +6,17 @@
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
{{template "user_style" .}}
<link rel="stylesheet" href="/res/style/viewer.css?v5"/>
<link rel="stylesheet" href="/res/style/layout.css?v5"/>
<link rel="shortcut icon" href="/res/img/tray32.png"/>
<link rel="icon" sizes="180x180" href="/res/img/pixeldrain.png"/>
<link rel="icon" sizes="256x256" href="/res/img/pixeldrain_big.png"/>
<meta name="theme-color" content="#75AD38"/>
{{.OGData}}
<style>
{{template `viewer.css`}}
{{template `layout.css`}}
</style>
<script type="text/javascript">
var apiEndpoint = '{{.APIEndpoint}}';
var captchaKey = '{{.Other.CaptchaKey}}';
</script>
{{.OGData}}
</head>
<body>
@@ -26,7 +24,7 @@
<div id="file_viewer_headerbar" class="highlight_1 file_viewer_headerbar">
<button id="button_toggle_toolbar" class="button_toggle_toolbar" onClick="Toolbar.toggle();"></button>
<a href="/" id="button_home" class="button button_home">
<img src="/res/img/pixeldrain_transparent.png"
<img src="{{template `pixeldrain_icon.png`}}"
alt="Back to the Home page"
style="height: 1.5em; margin: -0.2em; margin-right: 0.2em;"/>
</a>
@@ -49,19 +47,19 @@
<div id="stat_downloads" style="text-align: center;">N/A</div>
<button id="btnDownload" class="toolbar_button button_full_width" onClick="Toolbar.download();">
<img src="/res/img/floppy_small.png" alt="Download this file"/>
<img src="{{template `floppy_small.png`}}" alt="Download this file"/>
<span>Download</span>
</button>
<button id="btnCopy" class="toolbar_button button_full_width" onClick="Toolbar.copyUrl();">
<img src="/res/img/clipboard_small.png" alt="Copy file URL to clipboard"/>
<img src="{{template `clipboard_small.png`}}" alt="Copy file URL to clipboard"/>
<span><u>C</u>opy Link</span>
</button>
<button id="btnShare" class="toolbar_button button_full_width" onClick="Sharebar.toggle();">
<img src="/res/img/share_small.png" alt="Share this file on social media"/>
<img src="{{template `share_small.png`}}" alt="Share this file on social media"/>
<span>Share</span>
</button>
<button id="btnDetails" class="toolbar_button button_full_width" onClick="DetailsWindow.toggle();">
<img src="/res/img/info_small.png" alt="Help"/>
<img src="{{template `info_small.png`}}" alt="Help"/>
<span>Deta<u>i</u>ls</span>
</button>
{{template "advertisement" .}}
@@ -75,33 +73,34 @@
<div id="sharebar" class="file_viewer_sharebar">
Share on:<br/>
<button class="sharebar-button button_full_width" onClick="window.open('mailto:please@set.address?subject=File%20on%20PixelDrain&body=' + window.location.href);">
<img src="/res/img/social_email.png" alt="Share on E-Mail" style="width:40px; height: 40px;"/>
<img src="{{template `social_email.png`}}" alt="Share on E-Mail" style="width:40px; height: 40px;"/>
<br/>E-Mail
</button>
<button class="sharebar-button button_full_width" onclick="window.open('https://www.reddit.com/submit?url=' + window.location.href);">
<img src="/res/img/social_reddit.png" alt="Share on Reddit" style="width:40px; height: 40px;"/>
<img src="{{template `social_reddit.png`}}" alt="Share on Reddit" style="width:40px; height: 40px;"/>
<br/>Reddit
</button>
<button class="sharebar-button button_full_width" onClick="window.open('https://twitter.com/share?url=' + window.location.href);">
<img src="/res/img/social_twitter.png" alt="Share on Twitter" style="width:40px; height: 40px;"/>
<img src="{{template `social_twitter.png`}}" alt="Share on Twitter" style="width:40px; height: 40px;"/>
<br/>Twitter
</button>
<button class="sharebar-button button_full_width" onClick="window.open('http://www.facebook.com/sharer.php?u=' + window.location.href);">
<img src="/res/img/social_facebook.png" alt="Share on Facebook" style="width:40px; height: 40px;"/>
<img src="{{template `social_facebook.png`}}" alt="Share on Facebook" style="width:40px; height: 40px;"/>
<br/>Facebook
</button>
<button class="sharebar-button button_full_width" onClick="window.open('http://www.tumblr.com/share/link?url=' + window.location.href);">
<img src="/res/img/social_tumblr.png" alt="Share on Tumblr" style="width:40px; height: 40px;"/>
<img src="{{template `social_tumblr.png`}}" alt="Share on Tumblr" style="width:40px; height: 40px;"/>
<br/>Tumblr
</button>
<button class="sharebar-button button_full_width" onClick="window.open('https://voat.co/submit?linkpost=true&url=' + window.location.href);">
<img src="/res/img/social_voat.png" alt="Share on Voat" style="width:40px; height: 40px;"/>
<img src="{{template `social_voat.png`}}" alt="Share on Voat" style="width:40px; height: 40px;"/>
<br/>Voat
</button>
</div>
<div id="filepreview" class="file_viewer_file_preview">
<img src="/res/img/misc/loadthink.gif" style="margin-top: 20%; width: 200px; height: 200px;" />
<!-- <img src="{{template `loadthink.gif`}}" style="margin-top: 20%; width: 200px; height: 200px;" /> -->
<div class="image" style="margin-top: 20%; width: 100px; height: 100px;">{{template "spinner.svg"}}</div>
</div>
</div>
</div>
@@ -162,12 +161,16 @@
<div id="captcha_popup_captcha" style="text-align: center;"></div>
</div>
<script src="/res/misc/chartjs/Chart.min.js"></script>
<script src="/res/script/Chart.min.js"></script>
<script src="/res/script/jquery.js"></script>
<script src="/res/script/Toolbar.js?v7"></script>
<script src="/res/script/Viewer.js?v7"></script>
<script src="/res/script/ListNavigator.js?v7"></script>
<script>
<script type="text/javascript">
var apiEndpoint = '{{.APIEndpoint}}';
var captchaKey = '{{.Other.CaptchaKey}}';
{{template `Toolbar.js`}}
{{template `Viewer.js`}}
{{template `ListNavigator.js`}}
// This info gets filled in on the server side to prevent having to make an API call right after the page loads.
// Just to slice another few milliseconds from the load time :)
window.addEventListener("load", function(){

View File

@@ -4,14 +4,14 @@
{{if ne (isBrave .UserAgent) true}}
Use the Brave web browser for a faster and safer web!<br/>
<a href="https://brave.com/pix009" id="btnBrave" class="button toolbar_button button_full_width button_highlight">
<img src="/res/img/brave_lion.png" alt="Brave lion"/>
<img src="{{template `brave_lion.png`}}" alt="Brave lion"/>
<span>Get Brave</span>
</a>
<br/>
or <a href="https://medium.com/pixeldrain/advertising-on-pixeldrain-a-more-honest-approach-d5e00e3f0c29">learn why pixeldrain supports Brave</a>
{{else}}
Thank you for using Brave! Please consider supporting pixeldrain with a tip
<img src="/res/img/bat_logo_color.png" style="height: 1em; width: 1em;" />
<img src="{{template `bat_logo_color.png`}}" style="height: 1em; width: 1em;" />
{{end}}
</div>
<br/>

View File

@@ -29,7 +29,7 @@
{{if eq $field.Type "textarea"}}
<td colspan="2">
{{$field.Label}}<br/>
<textarea id="input_{{$field.Name}}" name="{{$field.Name}}" class="form_input" style="width: 100%; height: 5em; resize: vertical;">{{$field.DefaultValue}}</textarea>
<textarea id="input_{{$field.Name}}" name="{{$field.Name}}" class="form_input" style="width: 100%; height: 10em; resize: vertical;">{{$field.DefaultValue}}</textarea>
</td>
{{else}}
<td>{{$field.Label}}</td>

View File

@@ -2,7 +2,6 @@
<title>{{.}} ~ PixelDrain</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<link rel="stylesheet" href="/res/style/layout.css?v4" />
<link rel="shortcut icon" href="/res/img/tray32.png" />
<meta name="theme-color" content="#75AD38" />
<link rel="icon" sizes="180x180" href="/res/img/pixeldrain.png" />

View File

@@ -1,10 +1,6 @@
{{define "user_style"}}
<style>
{{.UserStyle}}
body,
.checkers {
background-image: url("/res/img/{{bgPattern}}");
}
{{template "layout.css"}}
</style>
{{end}}

View File

@@ -5,8 +5,6 @@
{{template "user_style" .}}
<script src="res/script/jquery.js"></script>
<script src="res/script/jquery-cookie.js"></script>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css"/>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>
<script type="text/javascript">var apiEndpoint = '{{.APIEndpoint}}';</script>
</head>
<body>
@@ -23,7 +21,7 @@
<div id="uploadedFiles" class="highlight_dark"></div>
</div></div>
{{template "page_bottom" .}}
<script src="/res/script/history.js"></script>
<script>{{template `history.js`}}</script>
{{template "analytics"}}
</body>
</html>{{end}}

View File

@@ -4,7 +4,6 @@
<head>
{{template "meta_tags" "Free file sharing service"}}
{{template "user_style" .}}
<script type="text/javascript">var apiEndpoint = '{{.APIEndpoint}}';</script>
</head>
<body>
{{template "page_top" .}}
@@ -65,9 +64,12 @@
{{template "page_bottom"}}
<script src="/res/script/jquery-2.1.4.min.js"></script>
<script type="text/javascript">var API_URL = "/api";</script>
<script src="/res/script/compiled/home.js"></script>
<script type="text/javascript">
var apiEndpoint = '{{.APIEndpoint}}';
var API_URL = "/api";
{{template "home.js"}}
</script>
{{template "analytics"}}
</body>
</html>

View File

@@ -4,7 +4,6 @@
<head>
{{template "meta_tags" "Text Upload"}}
{{template "user_style" .}}
<script src="/res/script/jquery-2.1.4.min.js"></script>
<style>
#toolbar {
@@ -46,11 +45,11 @@
<body>
<div id="toolbar">
<button class="toolbar_button button_full_width" onClick="uploadText();">
<img src="/res/img/upload_small.png" alt="Start Upload"/>
<img src="{{template `upload_small.png`}}" alt="Start Upload"/>
<span>Upload</span>
</button>
<a href="/" class="button toolbar_button button_full_width">
<img src="/res/img/pixeldrain_transparent.png" alt="Back to the Home page"/>
<img src="{{template `pixeldrain_icon.png`}}" alt="Back to the Home page"/>
<span>Home</span>
</a>
<br/><br/>
@@ -60,7 +59,10 @@
<div class="textarea_container">
<textarea id="textarea" class="textarea" placeholder="Your text here..." autofocus="autofocus"></textarea>
</div>
<script src="/res/script/compiled/textupload.js"></script>
<script src="/res/script/jquery.js"></script>
<script>{{template "textupload.js"}}</script>
{{template "analytics"}}
</body>
</html>

View File

@@ -2,16 +2,14 @@
<!DOCTYPE html>
<html>
<head>
{{template "meta_tags" "Widget showcase"}}
{{template "meta_tags" "Free file sharing service"}}
{{template "user_style" .}}
<script type="text/javascript">var apiEndpoint = '{{.APIEndpoint}}';</script>
</head>
<body>
{{template "page_top" .}}
<img id="header_image" class="header_image" src="/res/img/header_neuropol.png" alt="Header image"/>
<br/>
<div id="body" class="body">
{{template "menu" .}}
<div class="page_content"><div class="limit_width">
<h1>Widget showcase</h1>
<h2>Size 2 header</h2>
<h3>Size 3 header</h3>
@@ -23,10 +21,6 @@
<div class="highlight_middle">Middle highlight</div>
<div class="highlight_dark">Dark highlight</div>
<br/>
<div class="highlight_light">Light highlight with borders</div>
<div class="highlight_middle">Middle highlight with borders</div>
<div class="highlight_dark ">Dark highlight with borders</div>
<br/>
Link <a href="#">A link to someplace</a>.
<hr/>
Buttons <button>Regular ol' button!</button>
@@ -67,8 +61,8 @@
Color <input type="color" name="favcolor" value="#ff0000">
<br/>
{{template "footer"}}
</div>
</div></div>
{{template "page_bottom"}}
</body>
</html>
{{end}}

View File

@@ -6,21 +6,20 @@ import (
"net/http"
"fornaxian.com/pixeldrain-web/pixelapi"
"fornaxian.com/pixeldrain-web/webcontroller/forms"
"github.com/Fornaxian/log"
)
func (wc *WebController) adminGlobalsForm(td *TemplateData, r *http.Request) (f forms.Form) {
func (wc *WebController) adminGlobalsForm(td *TemplateData, r *http.Request) (f Form) {
if isAdmin, err := td.PixelAPI.UserIsAdmin(); err != nil {
td.Title = err.Error()
return forms.Form{Title: td.Title}
return Form{Title: td.Title}
} else if !isAdmin.IsAdmin {
td.Title = ";)"
return forms.Form{Title: td.Title}
return Form{Title: td.Title}
}
td.Title = "Pixeldrain global configuration"
f = forms.Form{
f = Form{
Name: "admin_globals",
Title: td.Title,
PreFormHTML: template.HTML("<p>Careful! The slightest typing error could bring the whole website down</p>"),
@@ -35,16 +34,17 @@ func (wc *WebController) adminGlobalsForm(td *TemplateData, r *http.Request) (f
}
var globalsMap = make(map[string]string)
for _, v := range globals.Globals {
f.Fields = append(f.Fields, forms.Field{
f.Fields = append(f.Fields, Field{
Name: v.Key,
DefaultValue: v.Value,
Label: v.Key,
Type: func() forms.FieldType {
Type: func() FieldType {
switch v.Key {
case
"email_address_change_body",
"email_password_reset_body":
return forms.FieldTypeTextarea
"email_password_reset_body",
"email_register_user_body":
return FieldTypeTextarea
case
"api_ratelimit_limit",
"api_ratelimit_rate",
@@ -52,9 +52,9 @@ func (wc *WebController) adminGlobalsForm(td *TemplateData, r *http.Request) (f
"file_inactive_expiry_days",
"max_file_size",
"pixelstore_min_redundancy":
return forms.FieldTypeNumber
return FieldTypeNumber
default:
return forms.FieldTypeText
return FieldTypeText
}
}(),
})

View File

@@ -1,4 +1,4 @@
package forms
package webcontroller
import (
"fmt"

View File

@@ -1,77 +0,0 @@
package webcontroller
import (
"html/template"
"net/http"
"net/url"
"time"
"fornaxian.com/pixeldrain-web/pixelapi"
"fornaxian.com/pixeldrain-web/webcontroller/forms"
"github.com/Fornaxian/log"
)
// TemplateData is a struct that every template expects when being rendered. In
// the field Other you can pass your own template-specific variables.
type TemplateData struct {
Authenticated bool
Username string
Email string
UserAgent string
UserStyle template.CSS
APIEndpoint template.URL
PixelAPI *pixelapi.PixelAPI
// Only used on file viewer page
Title string
OGData template.HTML
Other interface{}
URLQuery url.Values
// Only used for pages containing forms
Form forms.Form
}
func (wc *WebController) newTemplateData(w http.ResponseWriter, r *http.Request) *TemplateData {
var t = &TemplateData{
Authenticated: false,
Username: "",
UserAgent: r.UserAgent(),
UserStyle: userStyle(r),
APIEndpoint: template.URL(wc.conf.APIURLExternal),
URLQuery: r.URL.Query(),
}
if key, err := wc.getAPIKey(r); err == nil {
t.PixelAPI = pixelapi.New(wc.conf.APIURLInternal, key)
uinf, err := t.PixelAPI.UserInfo()
if err != nil {
// This session key doesn't work, or the backend is down, user
// cannot be authenticated
log.Debug("Session check for key '%s' failed: %s", key, err)
if err.Error() == "authentication_required" || err.Error() == "authentication_failed" {
// This key is invalid, delete it
log.Debug("Deleting invalid API key")
http.SetCookie(w, &http.Cookie{
Name: "pd_auth_key",
Value: "",
Path: "/",
Expires: time.Unix(0, 0),
Domain: wc.conf.SessionCookieDomain,
})
}
return t
}
// Authentication succeeded
t.Authenticated = true
t.Username = uinf.Username
t.Email = uinf.Email
} else {
t.PixelAPI = pixelapi.New(wc.conf.APIURLInternal, "")
}
return t
}

View File

@@ -1,147 +0,0 @@
package webcontroller
import (
"fmt"
"html/template"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"fornaxian.com/pixeldrain-api/util"
"github.com/Fornaxian/log"
)
// TemplateManager parses templates and provides utility functions to the
// templates' scripting language
type TemplateManager struct {
templates *template.Template
// Config
templateDir string
externalAPIEndpoint string
debugModeEnabled bool
}
// NewTemplateManager creates a new template manager
func NewTemplateManager(templateDir, externalAPIEndpoint string, debugMode bool) *TemplateManager {
return &TemplateManager{
templateDir: templateDir,
externalAPIEndpoint: externalAPIEndpoint,
debugModeEnabled: debugMode,
}
}
// ParseTemplates parses the templates in the template directory which is
// defined in the config file.
// If silent is false it will print an info log message for every template found
func (tm *TemplateManager) ParseTemplates(silent bool) {
var templatePaths []string
filepath.Walk(tm.templateDir, func(path string, f os.FileInfo, err error) error {
if f.IsDir() {
return nil
}
templatePaths = append(templatePaths, path)
if !silent {
log.Info("Template found: %s", path)
}
return nil
})
tpl := template.New("")
// Import template functions from funcs.go
tpl = tpl.Funcs(tm.funcMap())
var err error
tpl, err = tpl.ParseFiles(templatePaths...)
if err != nil {
log.Error("Template parsing failed: %v", err)
}
// Swap out the old templates with the new templates, to minimize
// modifications to the original variable.
tm.templates = tpl
}
// Get returns the templates, so they can be used to render views
func (tm *TemplateManager) Get() *template.Template {
if tm.debugModeEnabled {
tm.ParseTemplates(true)
}
return tm.templates
}
func (tm *TemplateManager) funcMap() template.FuncMap {
return template.FuncMap{
"bgPattern": tm.bgPattern,
"isBrave": tm.isBrave,
"debugMode": tm.debugMode,
"apiUrl": tm.apiURL,
"pageNr": tm.pageNr,
"add": tm.add,
"sub": tm.sub,
"formatData": tm.formatData,
}
}
func (tm *TemplateManager) bgPattern() string {
var now = time.Now()
if now.Weekday() == time.Wednesday && now.UnixNano()%10 == 0 {
return "checker_wednesday.png"
}
return fmt.Sprintf("checker%d.png", now.UnixNano()%17)
}
func (tm *TemplateManager) isBrave(useragent string) bool {
return strings.Contains(useragent, "Brave")
}
func (tm *TemplateManager) debugMode() bool {
return tm.debugModeEnabled
}
func (tm *TemplateManager) apiURL() string {
return tm.externalAPIEndpoint
}
func (tm *TemplateManager) pageNr(s string) (nr int) {
// Atoi returns 0 on error, which is fine for page numbers
if nr, _ = strconv.Atoi(s); nr < 0 {
return 0
}
return nr
}
func (tm *TemplateManager) add(a, b interface{}) int {
return detectInt(a) + detectInt(b)
}
func (tm *TemplateManager) sub(a, b interface{}) int {
return detectInt(a) - detectInt(b)
}
func (tm *TemplateManager) formatData(i int) string {
return util.FormatData(uint64(i))
}
func detectInt(i interface{}) int {
switch v := i.(type) {
case int:
return int(v)
case int8:
return int(v)
case int16:
return int(v)
case int32:
return int(v)
case int64:
return int(v)
case uint:
return int(v)
case uint8:
return int(v)
case uint16:
return int(v)
case uint32:
return int(v)
case uint64:
return int(v)
}
panic(fmt.Sprintf("%v is not an int", i))
}

258
webcontroller/templates.go Normal file
View File

@@ -0,0 +1,258 @@
package webcontroller
import (
"bytes"
"encoding/base64"
"fmt"
"html/template"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"fornaxian.com/pixeldrain-api/util"
"fornaxian.com/pixeldrain-web/pixelapi"
"github.com/Fornaxian/log"
)
// TemplateData is a struct that every template expects when being rendered. In
// the field Other you can pass your own template-specific variables.
type TemplateData struct {
Authenticated bool
Username string
Email string
UserAgent string
Style pixeldrainStyleSheet
UserStyle template.CSS
APIEndpoint template.URL
PixelAPI *pixelapi.PixelAPI
// Only used on file viewer page
Title string
OGData template.HTML
Other interface{}
URLQuery url.Values
// Only used for pages containing forms
Form Form
}
func (wc *WebController) newTemplateData(w http.ResponseWriter, r *http.Request) *TemplateData {
var t = &TemplateData{
Authenticated: false,
Username: "",
UserAgent: r.UserAgent(),
Style: userStyle(r),
UserStyle: template.CSS(userStyle(r).String()),
APIEndpoint: template.URL(wc.conf.APIURLExternal),
URLQuery: r.URL.Query(),
}
if key, err := wc.getAPIKey(r); err == nil {
t.PixelAPI = pixelapi.New(wc.conf.APIURLInternal, key)
uinf, err := t.PixelAPI.UserInfo()
if err != nil {
// This session key doesn't work, or the backend is down, user
// cannot be authenticated
log.Debug("Session check for key '%s' failed: %s", key, err)
if err.Error() == "authentication_required" || err.Error() == "authentication_failed" {
// This key is invalid, delete it
log.Debug("Deleting invalid API key")
http.SetCookie(w, &http.Cookie{
Name: "pd_auth_key",
Value: "",
Path: "/",
Expires: time.Unix(0, 0),
Domain: wc.conf.SessionCookieDomain,
})
}
return t
}
// Authentication succeeded
t.Authenticated = true
t.Username = uinf.Username
t.Email = uinf.Email
} else {
t.PixelAPI = pixelapi.New(wc.conf.APIURLInternal, "")
}
return t
}
// TemplateManager parses templates and provides utility functions to the
// templates' scripting language
type TemplateManager struct {
tpl *template.Template
// Config
resourceDir string
externalAPIEndpoint string
debugModeEnabled bool
}
// NewTemplateManager creates a new template manager
func NewTemplateManager(resourceDir, externalAPIEndpoint string, debugMode bool) *TemplateManager {
return &TemplateManager{
resourceDir: resourceDir,
externalAPIEndpoint: externalAPIEndpoint,
debugModeEnabled: debugMode,
}
}
// ParseTemplates parses the templates in the template directory which is
// defined in the config file.
// If silent is false it will print an info log message for every template found
func (tm *TemplateManager) ParseTemplates(silent bool) {
var err error
var templatePaths []string
tpl := template.New("")
// Import template functions
tpl.Funcs(template.FuncMap{
"bgPattern": tm.bgPattern,
"isBrave": tm.isBrave,
"debugMode": tm.debugMode,
"apiUrl": tm.apiURL,
"pageNr": tm.pageNr,
"add": tm.add,
"sub": tm.sub,
"formatData": tm.formatData,
})
// Parse dynamic templates
if err = filepath.Walk(tm.resourceDir+"/template", func(path string, f os.FileInfo, err error) error {
if f == nil || f.IsDir() {
return nil
}
templatePaths = append(templatePaths, path)
if !silent {
log.Info("Template found: %s", path)
}
return nil
}); err != nil {
log.Error("Failed to parse templates: %s", err)
}
if _, err = tpl.ParseFiles(templatePaths...); err != nil {
log.Error("Template parsing failed: %v", err)
}
// Parse static resources
var file []byte
if err = filepath.Walk(tm.resourceDir+"/include", func(path string, f os.FileInfo, err error) error {
if f == nil || f.IsDir() {
return nil
}
if file, err = ioutil.ReadFile(path); err != nil {
return err
}
if strings.HasSuffix(path, ".png") {
file = []byte("data:image/png;base64," + base64.StdEncoding.EncodeToString(file))
} else if strings.HasSuffix(path, ".gif") {
file = []byte("data:image/gif;base64," + base64.StdEncoding.EncodeToString(file))
}
// Wrap the resources in a template definition
if _, err = tpl.Parse(
`{{define "` + f.Name() + `"}}` + string(file) + `{{end}}`,
); err != nil {
return err
}
if !silent {
log.Info("Template parsed: %s", path)
}
return nil
}); err != nil {
log.Error("Failed to parse templates: %s", err)
}
tm.tpl = tpl
}
// Get returns the templates, so they can be used to render views
func (tm *TemplateManager) Get() *template.Template {
if tm.debugModeEnabled {
tm.ParseTemplates(true)
}
return tm.tpl
}
// Templace functions. These can be called from within the template to execute
// more specialized actions
func (tm *TemplateManager) bgPattern() template.URL {
var now = time.Now()
var file string
if now.Weekday() == time.Wednesday && now.UnixNano()%10 == 0 {
file = "checker_wednesday.png"
} else {
file = fmt.Sprintf("checker%d.png", now.UnixNano()%17)
}
var buf = bytes.Buffer{}
if err := tm.tpl.ExecuteTemplate(&buf, file, nil); err != nil {
panic(err)
}
return template.URL(buf.String())
}
func (tm *TemplateManager) isBrave(useragent string) bool {
return strings.Contains(useragent, "Brave")
}
func (tm *TemplateManager) debugMode() bool {
return tm.debugModeEnabled
}
func (tm *TemplateManager) apiURL() string {
return tm.externalAPIEndpoint
}
func (tm *TemplateManager) pageNr(s string) (nr int) {
// Atoi returns 0 on error, which is fine for page numbers
if nr, _ = strconv.Atoi(s); nr < 0 {
return 0
}
return nr
}
func (tm *TemplateManager) add(a, b interface{}) int {
return detectInt(a) + detectInt(b)
}
func (tm *TemplateManager) sub(a, b interface{}) int {
return detectInt(a) - detectInt(b)
}
func (tm *TemplateManager) formatData(i int) string {
return util.FormatData(uint64(i))
}
func detectInt(i interface{}) int {
switch v := i.(type) {
case int:
return int(v)
case int8:
return int(v)
case int16:
return int(v)
case int32:
return int(v)
case int64:
return int(v)
case uint:
return int(v)
case uint8:
return int(v)
case uint16:
return int(v)
case uint32:
return int(v)
case uint64:
return int(v)
}
panic(fmt.Sprintf("%v is not an int", i))
}

View File

@@ -6,7 +6,6 @@ import (
"time"
"fornaxian.com/pixeldrain-web/pixelapi"
"fornaxian.com/pixeldrain-web/webcontroller/forms"
"github.com/Fornaxian/log"
"github.com/julienschmidt/httprouter"
)
@@ -26,7 +25,7 @@ func (wc *WebController) serveLogout(
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func (wc *WebController) registerForm(td *TemplateData, r *http.Request) (f forms.Form) {
func (wc *WebController) registerForm(td *TemplateData, r *http.Request) (f Form) {
// This only runs on the first request
if wc.captchaSiteKey == "" {
capt, err := td.PixelAPI.GetRecaptcha()
@@ -47,16 +46,16 @@ func (wc *WebController) registerForm(td *TemplateData, r *http.Request) (f form
// Construct the form
td.Title = "Register a new Pixeldrain account"
f = forms.Form{
f = Form{
Name: "register",
Title: td.Title,
Fields: []forms.Field{
Fields: []Field{
{
Name: "username",
Label: "Username",
Description: "used for logging into your account",
Separator: true,
Type: forms.FieldTypeUsername,
Type: FieldTypeUsername,
}, {
Name: "e-mail",
Label: "E-mail address",
@@ -64,11 +63,11 @@ func (wc *WebController) registerForm(td *TemplateData, r *http.Request) (f form
"used for password resets and important account " +
"notifications",
Separator: true,
Type: forms.FieldTypeEmail,
Type: FieldTypeEmail,
}, {
Name: "password1",
Label: "Password",
Type: forms.FieldTypeNewPassword,
Type: FieldTypeNewPassword,
}, {
Name: "password2",
Label: "Password verification",
@@ -76,7 +75,7 @@ func (wc *WebController) registerForm(td *TemplateData, r *http.Request) (f form
"can verify that no typing errors were made, which would " +
"prevent you from logging into your new account",
Separator: true,
Type: forms.FieldTypeNewPassword,
Type: FieldTypeNewPassword,
}, {
Name: "recaptcha_response",
Label: "Turing test (click the white box)",
@@ -84,7 +83,7 @@ func (wc *WebController) registerForm(td *TemplateData, r *http.Request) (f form
"are not an evil robot that is trying to flood the " +
"website with fake accounts",
Separator: true,
Type: forms.FieldTypeCaptcha,
Type: FieldTypeCaptcha,
CaptchaSiteKey: wc.captchaKey(),
},
},
@@ -131,20 +130,20 @@ func (wc *WebController) registerForm(td *TemplateData, r *http.Request) (f form
return f
}
func (wc *WebController) loginForm(td *TemplateData, r *http.Request) (f forms.Form) {
func (wc *WebController) loginForm(td *TemplateData, r *http.Request) (f Form) {
td.Title = "Login"
f = forms.Form{
f = Form{
Name: "login",
Title: "Log in to your pixeldrain account",
Fields: []forms.Field{
Fields: []Field{
{
Name: "username",
Label: "Username / e-mail",
Type: forms.FieldTypeUsername,
Type: FieldTypeUsername,
}, {
Name: "password",
Label: "Password",
Type: forms.FieldTypeCurrentPassword,
Type: FieldTypeCurrentPassword,
},
},
BackLink: "/",
@@ -186,26 +185,26 @@ func (wc *WebController) loginForm(td *TemplateData, r *http.Request) (f forms.F
return f
}
func (wc *WebController) passwordResetForm(td *TemplateData, r *http.Request) (f forms.Form) {
func (wc *WebController) passwordResetForm(td *TemplateData, r *http.Request) (f Form) {
td.Title = "Recover lost password"
f = forms.Form{
f = Form{
Name: "password_reset",
Title: td.Title,
Fields: []forms.Field{
Fields: []Field{
{
Name: "email",
Label: "E-mail address",
Description: "we will send a password reset link to this " +
"e-mail address",
Separator: true,
Type: forms.FieldTypeEmail,
Type: FieldTypeEmail,
}, {
Name: "recaptcha_response",
Label: "Turing test (click the white box)",
Description: "the reCaptcha turing test verifies that you " +
"are not an evil robot that is trying hijack accounts",
Separator: true,
Type: forms.FieldTypeCaptcha,
Type: FieldTypeCaptcha,
CaptchaSiteKey: wc.captchaKey(),
},
},
@@ -223,7 +222,62 @@ func (wc *WebController) passwordResetForm(td *TemplateData, r *http.Request) (f
}
} else {
f.SubmitSuccess = true
f.SubmitMessages = []template.HTML{"Success! E-mail sent"}
f.SubmitMessages = []template.HTML{
"Success! Check your inbox for instructions to reset your password",
}
}
}
return f
}
func (wc *WebController) passwordResetConfirmForm(td *TemplateData, r *http.Request) (f Form) {
td.Title = "Reset lost password"
f = Form{
Name: "password_reset_confirm",
Title: td.Title,
Fields: []Field{
{
Name: "password1",
Label: "password",
Type: FieldTypeNewPassword,
}, {
Name: "password2",
Label: "password again",
Description: "you need to enter your password twice so we " +
"can verify that no typing errors were made, which would " +
"prevent you from logging into your new account",
Separator: true,
Type: FieldTypeNewPassword,
},
},
SubmitLabel: "Submit",
}
var resetKey = r.FormValue("key")
if resetKey == "" {
f.SubmitSuccess = false
f.SubmitMessages = []template.HTML{"Password reset key required"}
return f
}
if f.ReadInput(r) {
if f.FieldVal("password1") != f.FieldVal("password2") {
f.SubmitMessages = []template.HTML{
"Password verification failed. Please enter the same " +
"password in both password fields"}
return f
}
if err := td.PixelAPI.UserPasswordResetConfirm(resetKey, f.FieldVal("password1")); err != nil {
if err.Error() == "not_found" {
f.SubmitMessages = []template.HTML{template.HTML("Password reset key not found")}
} else {
log.Error("%s", err)
f.SubmitMessages = []template.HTML{"Internal Server Error"}
}
} else {
f.SubmitSuccess = true
f.SubmitMessages = []template.HTML{"Success! You can now log in with your new password"}
}
}
return f

View File

@@ -5,7 +5,6 @@ import (
"net/http"
"fornaxian.com/pixeldrain-web/pixelapi"
"fornaxian.com/pixeldrain-web/webcontroller/forms"
"github.com/Fornaxian/log"
"github.com/julienschmidt/httprouter"
)
@@ -24,9 +23,9 @@ func (wc *WebController) serveUserSettings(
td.Title = "Account settings"
td.Other = struct {
PasswordForm forms.Form
EmailForm forms.Form
UsernameForm forms.Form
PasswordForm Form
EmailForm Form
UsernameForm Form
}{
PasswordForm: wc.passwordForm(td, r),
EmailForm: wc.emailForm(td, r),
@@ -35,26 +34,26 @@ func (wc *WebController) serveUserSettings(
wc.templates.Get().ExecuteTemplate(w, "user_settings", td)
}
func (wc *WebController) passwordForm(td *TemplateData, r *http.Request) (f forms.Form) {
f = forms.Form{
func (wc *WebController) passwordForm(td *TemplateData, r *http.Request) (f Form) {
f = Form{
Name: "password_change",
Title: "Change password",
Fields: []forms.Field{
Fields: []Field{
{
Name: "old_password",
Label: "Old Password",
Type: forms.FieldTypeCurrentPassword,
Type: FieldTypeCurrentPassword,
}, {
Name: "new_password1",
Label: "New Password",
Type: forms.FieldTypeNewPassword,
Type: FieldTypeNewPassword,
}, {
Name: "new_password2",
Label: "New Password again",
Description: "we need you to repeat your password so you " +
"won't be locked out of your account if you make a " +
"typing error",
Type: forms.FieldTypeNewPassword,
Type: FieldTypeNewPassword,
},
},
SubmitLabel: "Submit",
@@ -89,11 +88,11 @@ func (wc *WebController) passwordForm(td *TemplateData, r *http.Request) (f form
return f
}
func (wc *WebController) emailForm(td *TemplateData, r *http.Request) (f forms.Form) {
f = forms.Form{
func (wc *WebController) emailForm(td *TemplateData, r *http.Request) (f Form) {
f = Form{
Name: "email_change",
Title: "Change e-mail address",
Fields: []forms.Field{
Fields: []Field{
{
Name: "new_email",
Label: "New e-mail address",
@@ -101,7 +100,7 @@ func (wc *WebController) emailForm(td *TemplateData, r *http.Request) (f forms.F
"verify that it's real. The address will be saved once " +
"the link in the message is clicked. If the e-mail " +
"doesn't arrive right away please check your spam box too",
Type: forms.FieldTypeEmail,
Type: FieldTypeEmail,
},
},
SubmitLabel: "Submit",
@@ -150,11 +149,11 @@ func (wc *WebController) serveEmailConfirm(
wc.templates.Get().ExecuteTemplate(w, "email_confirm", td)
}
func (wc *WebController) usernameForm(td *TemplateData, r *http.Request) (f forms.Form) {
f = forms.Form{
func (wc *WebController) usernameForm(td *TemplateData, r *http.Request) (f Form) {
f = Form{
Name: "username_change",
Title: "Change username",
Fields: []forms.Field{
Fields: []Field{
{
Name: "new_username",
Label: "New username",
@@ -162,7 +161,7 @@ func (wc *WebController) usernameForm(td *TemplateData, r *http.Request) (f form
"used to log in. If you forget your username you can " +
"still log in using your e-mail address if you have one " +
"configured",
Type: forms.FieldTypeUsername,
Type: FieldTypeUsername,
},
},
SubmitLabel: "Submit",

View File

@@ -2,11 +2,10 @@ package webcontroller
import (
"fmt"
"html/template"
"net/http"
)
func userStyle(r *http.Request) (style template.CSS) {
func userStyle(r *http.Request) (style pixeldrainStyleSheet) {
var selectedStyle pixeldrainStyleSheet
if cookie, err := r.Cookie("style"); err != nil {
@@ -34,7 +33,40 @@ func userStyle(r *http.Request) (style template.CSS) {
}
}
return template.CSS(fmt.Sprintf(
return selectedStyle
}
type pixeldrainStyleSheet struct {
TextColor hsl
InputColor hsl // Buttons, text fields
InputTextColor hsl
HighlightColor hsl // Links, highlighted buttons, list navigation
HighlightTextColor hsl // Text on buttons
DangerColor hsl
FileBackgroundColor hsl
ScrollbarForegroundColor hsl
ScrollbarHoverColor hsl
ScrollbarBackgroundColor hsl
BackgroundColor hsl
BodyColor hsl
Layer1Color hsl // Deepest and darkest layer
Layer1Shadow int // Deep layers have little shadow
Layer2Color hsl
Layer2Shadow int
Layer3Color hsl
Layer3Shadow int
Layer4Color hsl // Highest and brightest layer
Layer4Shadow int // High layers have lots of shadow
ShadowColor hsl
ShadowSpread int // Pixels
ShadowIntensity int // Pixels
}
func (s pixeldrainStyleSheet) String() string {
return fmt.Sprintf(
`:root {
--text_color: %s;
--input_color: %s;
@@ -66,63 +98,35 @@ func userStyle(r *http.Request) (style template.CSS) {
--shadow_spread: %s;
--shadow_intensity: %s;
}`,
selectedStyle.TextColor.cssString(),
selectedStyle.InputColor.cssString(),
selectedStyle.InputColor.add(0, 0, -.03).cssString(),
selectedStyle.InputTextColor.cssString(),
selectedStyle.HighlightColor.cssString(),
selectedStyle.HighlightColor.add(0, 0, -.03).cssString(),
selectedStyle.HighlightTextColor.cssString(),
selectedStyle.DangerColor.cssString(),
selectedStyle.DangerColor.add(0, 0, -.03).cssString(),
selectedStyle.FileBackgroundColor.cssString(),
selectedStyle.ScrollbarForegroundColor.cssString(),
selectedStyle.ScrollbarHoverColor.cssString(),
selectedStyle.ScrollbarBackgroundColor.cssString(),
selectedStyle.BackgroundColor.cssString(),
selectedStyle.BodyColor.cssString(),
selectedStyle.Layer1Color.cssString(),
fmt.Sprintf("%dpx", selectedStyle.Layer1Shadow),
selectedStyle.Layer2Color.cssString(),
fmt.Sprintf("%dpx", selectedStyle.Layer2Shadow),
selectedStyle.Layer3Color.cssString(),
fmt.Sprintf("%dpx", selectedStyle.Layer3Shadow),
selectedStyle.Layer4Color.cssString(),
fmt.Sprintf("%dpx", selectedStyle.Layer4Shadow),
selectedStyle.ShadowColor.cssString(),
fmt.Sprintf("%dpx", selectedStyle.ShadowSpread),
fmt.Sprintf("%dpx", selectedStyle.ShadowIntensity),
))
s.TextColor.cssString(),
s.InputColor.cssString(),
s.InputColor.add(0, 0, -.03).cssString(),
s.InputTextColor.cssString(),
s.HighlightColor.cssString(),
s.HighlightColor.add(0, 0, -.03).cssString(),
s.HighlightTextColor.cssString(),
s.DangerColor.cssString(),
s.DangerColor.add(0, 0, -.03).cssString(),
s.FileBackgroundColor.cssString(),
s.ScrollbarForegroundColor.cssString(),
s.ScrollbarHoverColor.cssString(),
s.ScrollbarBackgroundColor.cssString(),
s.BackgroundColor.cssString(),
s.BodyColor.cssString(),
s.Layer1Color.cssString(),
fmt.Sprintf("%dpx", s.Layer1Shadow),
s.Layer2Color.cssString(),
fmt.Sprintf("%dpx", s.Layer2Shadow),
s.Layer3Color.cssString(),
fmt.Sprintf("%dpx", s.Layer3Shadow),
s.Layer4Color.cssString(),
fmt.Sprintf("%dpx", s.Layer4Shadow),
s.ShadowColor.cssString(),
fmt.Sprintf("%dpx", s.ShadowSpread),
fmt.Sprintf("%dpx", s.ShadowIntensity),
)
}
type pixeldrainStyleSheet struct {
TextColor hsl
InputColor hsl // Buttons, text fields
InputTextColor hsl
HighlightColor hsl // Links, highlighted buttons, list navigation
HighlightTextColor hsl // Text on buttons
DangerColor hsl
FileBackgroundColor hsl
ScrollbarForegroundColor hsl
ScrollbarHoverColor hsl
ScrollbarBackgroundColor hsl
BackgroundColor hsl
BodyColor hsl
Layer1Color hsl // Deepest and darkest layer
Layer1Shadow int // Deep layers have little shadow
Layer2Color hsl
Layer2Shadow int
Layer3Color hsl
Layer3Shadow int
Layer4Color hsl // Highest and brightest layer
Layer4Shadow int // High layers have lots of shadow
ShadowColor hsl
ShadowSpread int // Pixels
ShadowIntensity int // Pixels
}
type hsl struct {
Hue int
Saturation float64

View File

@@ -9,7 +9,6 @@ import (
"fornaxian.com/pixeldrain-web/init/conf"
"fornaxian.com/pixeldrain-web/pixelapi"
"fornaxian.com/pixeldrain-web/webcontroller/forms"
"github.com/Fornaxian/log"
"github.com/julienschmidt/httprouter"
)
@@ -19,7 +18,6 @@ import (
type WebController struct {
conf *conf.PixelWebConfig
templates *TemplateManager
staticResourceDir string
// page-specific variables
captchaSiteKey string
@@ -30,10 +28,9 @@ type WebController struct {
func New(r *httprouter.Router, prefix string, conf *conf.PixelWebConfig) *WebController {
var wc = &WebController{
conf: conf,
staticResourceDir: conf.StaticResourceDir,
}
wc.templates = NewTemplateManager(
conf.TemplateDir,
conf.ResourceDir,
conf.APIURLExternal,
conf.DebugMode,
)
@@ -42,7 +39,7 @@ func New(r *httprouter.Router, prefix string, conf *conf.PixelWebConfig) *WebCon
var p = prefix
// Serve static files
var fs = http.FileServer(http.Dir(wc.staticResourceDir))
var fs = http.FileServer(http.Dir(wc.conf.ResourceDir + "/static"))
r.GET(p+"/res/*filepath", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
w.Header().Set("Cache-Control", "public, max-age=86400") // Cache for one day
r.URL.Path = p.ByName("filepath")
@@ -92,6 +89,8 @@ func New(r *httprouter.Router, prefix string, conf *conf.PixelWebConfig) *WebCon
r.GET(p+"/user/settings" /* */, wc.serveUserSettings)
r.POST(p+"/user/settings" /* */, wc.serveUserSettings)
r.GET(p+"/user/confirm_email" /* */, wc.serveEmailConfirm)
r.GET(p+"/user/password_reset_confirm" /* */, wc.serveForm(wc.passwordResetConfirmForm, false))
r.POST(p+"/user/password_reset_confirm" /**/, wc.serveForm(wc.passwordResetConfirmForm, false))
// Admin settings
r.GET(p+"/admin" /* */, wc.serveTemplate("admin_panel", true))
@@ -130,12 +129,12 @@ func (wc *WebController) serveFile(path string) httprouter.Handle {
r *http.Request,
p httprouter.Params,
) {
http.ServeFile(w, r, wc.staticResourceDir+path)
http.ServeFile(w, r, wc.conf.ResourceDir+"/static"+path)
}
}
func (wc *WebController) serveForm(
handler func(*TemplateData, *http.Request) forms.Form,
handler func(*TemplateData, *http.Request) Form,
requireAuth bool,
) httprouter.Handle {
return func(
@@ -167,7 +166,7 @@ func (wc *WebController) serveForm(
// Remove the recaptcha field if captcha is disabled
if wc.captchaKey() == "none" {
for i, field := range td.Form.Fields {
if field.Type == forms.FieldTypeCaptcha {
if field.Type == FieldTypeCaptcha {
td.Form.Fields = append(
td.Form.Fields[:i],
td.Form.Fields[i+1:]...,