initial commit
This commit is contained in:
parent
ba419303ad
commit
c06b80f204
26
README.md
26
README.md
@ -1,3 +1,25 @@
|
||||
# imgsort2
|
||||
# Imgsort 2
|
||||
Cross platform, fast and efficient manual file sorting with keybinds.
|
||||
|
||||
Self-hosted image and file sorting application
|
||||
## Description
|
||||
**Imgsort 2** is web app that lets a client sort files from a "staging" directory on the server into other directories on the server.
|
||||
It is mainly intended for sorting media that was auto-uploaded from a phone into a self-hosted cloud, like Nextcloud.
|
||||
|
||||
If you are looking for a simple program the lets you the same thing locally, have a look at [imgsort](https://github.com/MatthiasQuintern/imgsort).
|
||||
|
||||
### Web Interface
|
||||
The web can show images, video and audio.
|
||||
The user can configure several directories into which the files can be sorted through the web interface.
|
||||
Each directory has a keybind and a button which, when either is pressed, moves the image into the according directory.
|
||||
|
||||
The web interface has mobile optimizations, letting you sort images/files on the go.
|
||||
|
||||
|
||||
## Installation
|
||||
You need a *php* enabled web server, for example *nginx* with *php-fpm*.
|
||||
The web server needs to serve the app as well as the directory containg all images, called `rootDir`.
|
||||
You need to configure the `$rootDir` and `$rootPath` as well as the `$stagingDirName` accordingly in the `imgsort.php` file.
|
||||
|
||||
If you a using nextcloud, make sure to have the `rootDir` directory as an "external" storage media and not in the main nextcloud `data` directory.
|
||||
|
||||
**The app has no authentication system. Only expose it locally or use something like nginx basic auth to restrict access!**
|
||||
|
56
src/config.html
Normal file
56
src/config.html
Normal file
@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Imgsort 2 - Config</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<main>
|
||||
<div class="main-box">
|
||||
<button onclick="window.location.href='index.html';" id="configure-button">Go back</button>
|
||||
<h2>Add mappings</h2>
|
||||
A key must be set for each directory, even if it will not be used.<br>
|
||||
<form class="add-mapping-form" id="addMappingForm">
|
||||
<div class="form-row">
|
||||
<label for="key">Key:</label>
|
||||
<input type="text" id="key" name="key" required minlength="1" maxlength="1">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="directory">Directory:</label>
|
||||
<input type="text" id="directory" name="directory" required>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="submit"></label>
|
||||
<button type="submit" name="submit">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="second-box">
|
||||
<h3>Mappings</h3>
|
||||
<table id="mappingTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Directory</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Mappings will be added here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
<footer>
|
||||
<div class="statusline">Status: <span id="status"></span></div>
|
||||
</footer>
|
||||
|
||||
<script src="config.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
84
src/config.js
Normal file
84
src/config.js
Normal file
@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Handle mappings
|
||||
* Mappings are stored in local storage as a Map (key: directory)
|
||||
*/
|
||||
|
||||
let statusLine = document.getElementById('status');
|
||||
|
||||
// these can not be mapped by the user
|
||||
const reservedMappings = ["u", "s"];
|
||||
|
||||
/*
|
||||
* Load mappings, create a table showing them with a "remove" button each
|
||||
*/
|
||||
function renderMappings() {
|
||||
console.log(localStorage.getItem('mappings'));
|
||||
const mappings = new Map(JSON.parse(localStorage.getItem('mappings')) || []);
|
||||
console.log(typeof mappings);
|
||||
const tbody = document.querySelector('#mappingTable tbody');
|
||||
tbody.innerHTML = ''; // clear table body
|
||||
mappings.forEach((directory, key) => {
|
||||
const row = document.createElement('tr');
|
||||
const keyCell = document.createElement('td');
|
||||
const directoryCell = document.createElement('td');
|
||||
|
||||
keyCell.innerHTML = `<span class="key">${key}</span>`;
|
||||
directoryCell.innerHTML = `<span class="path">${directory}</span>`;
|
||||
|
||||
row.appendChild(keyCell);
|
||||
row.appendChild(directoryCell);
|
||||
// remove mapping button
|
||||
const removeCell = document.createElement('td');
|
||||
const removeButton = document.createElement('button');
|
||||
removeButton.textContent = 'Remove';
|
||||
removeButton.onclick = function() {
|
||||
removeMapping(key);
|
||||
};
|
||||
removeCell.appendChild(removeButton);
|
||||
row.appendChild(removeCell)
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function removeMapping(key) {
|
||||
let mappings = new Map(JSON.parse(localStorage.getItem('mappings')) || []);
|
||||
if (!mappings.has(key)) {
|
||||
statusLine.innerHTML = `Can not remove mapping '${key}', it does not exist`;
|
||||
return;
|
||||
}
|
||||
statusLine.innerHTML = `Removed mapping '${key}'`;
|
||||
mappings.delete(key);
|
||||
localStorage.setItem('mappings', JSON.stringify(Array.from(mappings.entries())));
|
||||
renderMappings();
|
||||
}
|
||||
|
||||
function addMapping(key, directory) {
|
||||
if (reservedMappings.includes(key)) {
|
||||
statusLine.innerHTML = `'${key}' is reserved and can not be mapped`;
|
||||
return;
|
||||
}
|
||||
let mappings = new Map(JSON.parse(localStorage.getItem('mappings')) || []);
|
||||
statusLine.innerHTML = (mappings.has(key) ? "Changed" : "Added") + ` mapping '${key}' - '${directory}'`;
|
||||
mappings.set(key, directory);
|
||||
localStorage.setItem('mappings', JSON.stringify(Array.from(mappings.entries())));
|
||||
renderMappings();
|
||||
}
|
||||
|
||||
// event listener for add mapping form
|
||||
document.getElementById('addMappingForm').addEventListener('submit', function(event) {
|
||||
event.preventDefault();
|
||||
const key = document.getElementById('key').value;
|
||||
const directory = document.getElementById('directory').value;
|
||||
|
||||
addMapping(key, directory);
|
||||
|
||||
// clear input
|
||||
document.getElementById('key').value = '';
|
||||
document.getElementById('directory').value = '';
|
||||
});
|
||||
|
||||
// load existing mappings on page load
|
||||
window.onload = function() {
|
||||
renderMappings();
|
||||
};
|
||||
|
136
src/imgsort.php
Normal file
136
src/imgsort.php
Normal file
@ -0,0 +1,136 @@
|
||||
<?php
|
||||
// CONFIGURATION
|
||||
// Absolute path to the directory containing all image directories as well as the staging directory
|
||||
// All other paths (in the user interface) are relative to $rootDir
|
||||
$rootDir = __DIR__ . '/../test/';
|
||||
// The URI at which the rootDir will be served
|
||||
$rootPath = '/test/';
|
||||
// Path of the directory containing all the files to be sorted, relative to $rootDir
|
||||
$stagingDirName = 'staging/';
|
||||
|
||||
|
||||
// Not configuration anymore!
|
||||
$stagingPath = $rootPath . $stagingDirName . '/';
|
||||
$stagingDir = $rootDir . $stagingDirName . '/';
|
||||
|
||||
/**
|
||||
* @brief Return a list of all files in the staging directory
|
||||
*/
|
||||
function getFileList() {
|
||||
global $stagingDir;
|
||||
$images = array_values(array_filter(scandir($stagingDir), function($file) use ($stagingDir) {
|
||||
return is_file($stagingDir . $file); //&& preg_match('/\.(jpg|jpeg|png|gif)$/i', $file);
|
||||
}));
|
||||
return json_encode($images);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Return a list of all files in $dir and its sub directories
|
||||
* @param $displayDir The returned file paths will be appended to $displayDir instead of $dir
|
||||
* $dir and $displayDir must have trailing slashes!
|
||||
*/
|
||||
function getFileListRecursive($dir, $displayDir=null) {
|
||||
$filesAndDirs = scandir($dir);
|
||||
error_log(json_encode($filesAndDirs));
|
||||
$files = [];
|
||||
foreach ($filesAndDirs as $file) {
|
||||
error_log("getFileListRecursive: ".$dir);
|
||||
if ($file == "." || $file == "..") { continue; }
|
||||
if (is_dir($dir . $file)) {
|
||||
$sub_files = getFileListRecursive($dir . $file . '/', $displayDir . $file . '/');
|
||||
$files = array_merge($files, $sub_files);
|
||||
} else {
|
||||
if (is_null($displayDir)) {
|
||||
$files[] = $dir . $file;
|
||||
} else {
|
||||
$files[] = $displayDir . $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $files;
|
||||
}
|
||||
|
||||
|
||||
function saveMove($file, $dest) {
|
||||
$fullDir = dirname($dest);
|
||||
|
||||
if (!is_file($file)) {
|
||||
error_log("File not found: ".$file);
|
||||
return [false, "File not found"];
|
||||
}
|
||||
if (is_file($dest)) {
|
||||
error_log("File already exists: ".$dest);
|
||||
return [false, "File already exists"];
|
||||
}
|
||||
|
||||
// create non existing directories
|
||||
if (!is_dir($fullDir)) {
|
||||
// rw for use+group, recursive
|
||||
if (!mkdir($fullDir, 0770, true)) {
|
||||
return [false, "Failed to create directory"];
|
||||
}
|
||||
}
|
||||
if (!rename($file, $dest)) {
|
||||
return [false, "Failed to move file"];
|
||||
}
|
||||
return [true, ""];
|
||||
}
|
||||
/**
|
||||
* Move a file from staging to the destination directory
|
||||
* @param file relative to stagingDir
|
||||
* @param dir relative to rootDir
|
||||
*/
|
||||
function moveFile($file, $dir) {
|
||||
global $stagingDir, $rootDir;
|
||||
$fullFile = $stagingDir . $file;
|
||||
$fullDir = $rootDir . $dir . "/";
|
||||
$fullDest = $fullDir . basename($fullFile);
|
||||
return saveMove($fullFile, $fullDest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a file from destination directory back to the staging directory
|
||||
* @param dir relative to rootDir
|
||||
* @param file relative to rootDir/dir
|
||||
*/
|
||||
function undoFile($file, $dir) {
|
||||
global $stagingDir, $rootDir;
|
||||
$fullDir = $rootDir . $dir . "/";
|
||||
$fullFile = $fullDir . $file;
|
||||
$fullDest = $stagingDir . basename($fullFile);
|
||||
return saveMove($fullFile, $fullDest);
|
||||
}
|
||||
|
||||
|
||||
// Parse the request
|
||||
$action = $_GET["action"];
|
||||
$result_ok = true;
|
||||
switch ($action) {
|
||||
case "getFileList":
|
||||
/* $result = getFileList(); */
|
||||
$result = json_encode(getFileListRecursive($stagingDir, ''));
|
||||
break;
|
||||
case "getStagingPath":
|
||||
$result = $stagingPath;
|
||||
break;
|
||||
case "moveFile":
|
||||
$file = $_GET["file"];
|
||||
$dest = $_GET["dest"];
|
||||
list($result_ok, $result) = moveFile($file, $dest);
|
||||
break;
|
||||
case "undoFile":
|
||||
$file = $_GET["file"];
|
||||
$dest = $_GET["dest"];
|
||||
list($result_ok, $result) = undoFile($file, $dest);
|
||||
break;
|
||||
default:
|
||||
$result = "Invalid action";
|
||||
$result_ok = false;
|
||||
}
|
||||
if ($result_ok) {
|
||||
http_response_code(200);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
}
|
||||
echo $result;
|
||||
?>
|
67
src/index.html
Normal file
67
src/index.html
Normal file
@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Imgsort 2</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<!-- <style> -->
|
||||
<!-- body { -->
|
||||
<!-- font-family: Arial, sans-serif; -->
|
||||
<!-- text-align: center; -->
|
||||
<!-- margin: 50px; -->
|
||||
<!-- } -->
|
||||
<!-- img { -->
|
||||
<!-- max-width: 90%; -->
|
||||
<!-- height: auto; -->
|
||||
<!-- margin-bottom: 20px; -->
|
||||
<!-- } -->
|
||||
<!-- .button-group { -->
|
||||
<!-- margin-top: 20px; -->
|
||||
<!-- } -->
|
||||
<!-- button { -->
|
||||
<!-- padding: 10px 20px; -->
|
||||
<!-- margin: 5px; -->
|
||||
<!-- font-size: 16px; -->
|
||||
<!-- } -->
|
||||
<!-- .progress { -->
|
||||
<!-- /* float: left; */ -->
|
||||
<!-- } -->
|
||||
<!-- </style> -->
|
||||
</head>
|
||||
<body>
|
||||
<script src="/src/index.js" async></script>
|
||||
|
||||
<div id="preload-file" style="display: none; visibility: hidden"></div>
|
||||
<main>
|
||||
|
||||
<div class="main-box">
|
||||
<div class="current-file" id="current-file">
|
||||
<div class="imgsort2">Imgsort <i>2</i></div>
|
||||
<br>
|
||||
<button onclick="window.location.href='config.html';" id="configure-button">Configure</button>
|
||||
<button onclick="setCurrentFile()" id="start">Start</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="second-box">
|
||||
<div id="buttons">
|
||||
<button onclick="undo()" id="undo-button">Undo</button>
|
||||
<button onclick="skip()" id="skip-button">Skip</button>
|
||||
<span class="move-buttons" id="move-buttons"></span>
|
||||
<button onclick="window.location.href='config.html';" id="configure-button">Configure</button>
|
||||
</div>
|
||||
<div class="file-list">
|
||||
Files:
|
||||
<div class="file-list-content" id="file-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
<!-- <div class="keydebug">Key: <span id="keydebug"></span></div> -->
|
||||
<div class="statusline">Status: <span id="status"></span></div>
|
||||
<!-- Imgsort 2 by Matthias Quintern -->
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
252
src/index.js
Normal file
252
src/index.js
Normal file
@ -0,0 +1,252 @@
|
||||
// Handle keyboard input
|
||||
|
||||
const imageExtensions = ['gif','jpg','jpeg','png', 'webp'];
|
||||
const videoExtensions =['mpg', 'mp2', 'mpeg', 'mpe', 'mpv', 'mp4'];
|
||||
const audioExtensions =['mp3', 'ogg', 'wav', 'flac'];
|
||||
|
||||
|
||||
let statusLine = document.getElementById('status');
|
||||
|
||||
const mappings = new Map(JSON.parse(localStorage.getItem('mappings')) || []);
|
||||
|
||||
let initialContent = document.getElementById('current-file').innerHTML;
|
||||
|
||||
let history = [];
|
||||
|
||||
// get the path in which the images are located and can be loaded from, by appending to them to the stagingPath
|
||||
let stagingPath = "";
|
||||
async function setStagingPath() {
|
||||
const response = await fetch("imgsort.php?action=getStagingPath");
|
||||
if (response.ok) {
|
||||
stagingPath = await response.text();
|
||||
}
|
||||
else {
|
||||
statusLine.innerHTML = `getStagingPath: server returned ${response.status}`;
|
||||
console.error("Could not get stagingPath from server");
|
||||
console.error(response);
|
||||
}
|
||||
}
|
||||
|
||||
// list of files to be processed
|
||||
let fileList = [];
|
||||
async function setFileList() {
|
||||
const response = await fetch("imgsort.php?action=getFileList");
|
||||
const text = await response.text();
|
||||
// console.log(text);
|
||||
if (response.ok) {
|
||||
fileList = JSON.parse(text);
|
||||
// fileList.sort();
|
||||
}
|
||||
else {
|
||||
statusLine.innerHTML = `getFileList: server returned ${response.status}: '${text}'`;
|
||||
console.error("Could not get fileList from server");
|
||||
console.error(response);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Create button for each mapping
|
||||
*/
|
||||
function renderMoveButtons() {
|
||||
const buttons = document.getElementById('move-buttons');
|
||||
buttons.innerHTML = '';
|
||||
mappings.forEach((directory, key) => {
|
||||
const removeButton = document.createElement('button');
|
||||
removeButton.innerHTML = `<span class="key">${key}</span> <span class="directory">${directory}</span>`;
|
||||
removeButton.onclick = function() {
|
||||
moveFile(directory);
|
||||
};
|
||||
removeButton.class = "move-button";
|
||||
buttons.appendChild(removeButton);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function renderFileList() {
|
||||
const fileListDiv = document.getElementById('file-list');
|
||||
fileListDiv.innerHTML = '';
|
||||
fileList.forEach(file => {
|
||||
const p = document.createElement('p');
|
||||
classes = "file-list-item path";
|
||||
if (file === currentFile) {
|
||||
classes += " selected-file-list-item";
|
||||
}
|
||||
p.setAttribute("class", classes)
|
||||
p.innerHTML = file;
|
||||
fileListDiv.appendChild(p);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
let currentFile = "";
|
||||
let currentFileIdx = null;
|
||||
|
||||
function createMediaFileElement(file, autoplay=false) {
|
||||
const fileExt = file.split('.').pop();
|
||||
if (imageExtensions.includes(fileExt)) { // if image
|
||||
let img = document.createElement("img");
|
||||
img.setAttribute('src', stagingPath + file)
|
||||
return img;
|
||||
} else if (videoExtensions.includes(fileExt)) { // if video
|
||||
let vid = document.createElement("video");
|
||||
if (autoplay) vid.setAttribute('autoplay', true);
|
||||
vid.setAttribute('controls', true);
|
||||
src = document.createElement("source");
|
||||
src.setAttribute("src", stagingPath + file);
|
||||
src.setAttribute("type", `video/${fileExt}`);
|
||||
vid.appendChild(src);
|
||||
return vid;
|
||||
} else if (audioExtensions.includes(fileExt)) { // if audio
|
||||
let aud = document.createElement("audio");
|
||||
if (autoplay) aud.setAttribute('autoplay', true);
|
||||
aud.setAttribute('controls', true);
|
||||
src = document.createElement("source");
|
||||
src.setAttribute("src", stagingPath + file);
|
||||
src.setAttribute("type", `audio/${fileExt}`);
|
||||
aud.appendChild(src);
|
||||
return aud;
|
||||
} else { // if none of the above
|
||||
let p = document.createElement("p");
|
||||
p.innerHTML = 'No preview available';
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function renderCurrentFile() {
|
||||
// const currentFileNameDiv = document.getElementById('current-file-name');
|
||||
// if (!currentFile) {
|
||||
// currentFileNameDiv.innerHTML = "No files in staging";
|
||||
// }
|
||||
// else {
|
||||
// currentFileNameDiv.innerHTML = currentFile;
|
||||
// }
|
||||
const currentFileDiv = document.getElementById('current-file');
|
||||
currentFileDiv.innerHTML = "";
|
||||
let element = createMediaFileElement(currentFile, true);
|
||||
currentFileDiv.appendChild(element);
|
||||
}
|
||||
|
||||
|
||||
// load media tag of the next file in hidden div, without autoplay
|
||||
function preloadNextFile() {
|
||||
const nextFile = fileList[currentFileIdx+1];
|
||||
if (!nextFile) {
|
||||
return
|
||||
}
|
||||
const preloadFileDiv = document.getElementById('preload-file');
|
||||
preloadFileDiv.innerHTML = "";
|
||||
let element = createMediaFileElement(nextFile, false);
|
||||
preloadFileDiv.appendChild(element);
|
||||
console.log("Preloaded " + nextFile);
|
||||
}
|
||||
|
||||
|
||||
// set the currentFile variable according to currentFileIdx
|
||||
function setCurrentFile() {
|
||||
// if none set and
|
||||
if (currentFileIdx == null && fileList.length > 0) {
|
||||
currentFileIdx = 0;
|
||||
}
|
||||
// else if (currentFileIdx < fileList.length) {
|
||||
// }
|
||||
else if (currentFileIdx >= fileList.length) {
|
||||
currentFileIdx = fileList.length - 1;
|
||||
}
|
||||
// if index is valid
|
||||
if (typeof(currentFileIdx) == 'number' && currentFileIdx >= 0 && currentFileIdx < fileList.length) {
|
||||
currentFile = fileList[currentFileIdx];
|
||||
}
|
||||
else {
|
||||
currentFileIdx = null;
|
||||
currentFile = "";
|
||||
}
|
||||
renderCurrentFile();
|
||||
renderFileList();
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function moveFile(directory) {
|
||||
if (!currentFile) {
|
||||
statusLine.innerHTML = "No file to process!";
|
||||
return
|
||||
}
|
||||
const response = await fetch(`imgsort.php?action=moveFile&file=${escape(currentFile)}&dest=${directory}`);
|
||||
const text = await response.text();
|
||||
// console.log(text);
|
||||
if (response.ok) {
|
||||
statusLine.innerHTML = `Moved '${currentFile}' to '${directory}'`;
|
||||
history.push([currentFile, directory]);
|
||||
// remove file from list
|
||||
fileList.splice(currentFileIdx, 1);
|
||||
// set next file
|
||||
setCurrentFile();
|
||||
preloadNextFile();
|
||||
}
|
||||
else {
|
||||
statusLine.innerHTML = `moveFile: server returned ${response.status}: '${text}'`;
|
||||
console.error(`moveFile: server returned ${response.status}: '${text}'`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function undo() {
|
||||
if (!history.length > 0) {
|
||||
statusLine.innerHTML = "Nothing to undo"
|
||||
return;
|
||||
}
|
||||
const [file, directory] = history.pop();
|
||||
const response = await fetch(`imgsort.php?action=undoFile&file=${escape(file)}&dest=${directory}`);
|
||||
const text = await response.text();
|
||||
// console.log(text);
|
||||
if (response.ok) {
|
||||
statusLine.innerHTML = `Undo: move '${file}' to '${directory}'`;
|
||||
await setFileList()
|
||||
setCurrentFile();
|
||||
}
|
||||
else {
|
||||
statusLine.innerHTML = `undo: server returned ${response.status}: '${text}'`;
|
||||
console.error(`undo: server returned ${response.status}: '${text}'`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function skip() {
|
||||
currentFileIdx++;
|
||||
setCurrentFile();
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function initialize() {
|
||||
await setStagingPath();
|
||||
await setFileList();
|
||||
renderMoveButtons();
|
||||
renderFileList();
|
||||
// setCurrentFile();
|
||||
preloadNextFile();
|
||||
}
|
||||
|
||||
// load existing mappings on page load
|
||||
window.onload = function() {
|
||||
initialize();
|
||||
};
|
||||
|
||||
// handle keyboard
|
||||
document.onkeypress = function(e) {
|
||||
e = e || window.event;
|
||||
let keydebug = document.getElementById("keydebug");
|
||||
if (keydebug) {
|
||||
keydebug.innerHTML = e.key;
|
||||
}
|
||||
if (e.key == "u") {
|
||||
undo();
|
||||
}
|
||||
else if (e.key == "s") {
|
||||
skip();
|
||||
}
|
||||
else if (mappings.has(e.key)) {
|
||||
moveFile(mappings.get(e.key));
|
||||
}
|
||||
}
|
50
src/sass/gruvbox_light.sass
Normal file
50
src/sass/gruvbox_light.sass
Normal file
@ -0,0 +1,50 @@
|
||||
// ----------------------------------------------------------------------------
|
||||
// Gruvbox Light Mode
|
||||
// https://github.com/morhetz/gruvbox-contrib/blob/master/scss/gruvbox-light.scss
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Light Background
|
||||
$bg0: #fbf1c7
|
||||
$bg0-hard: #f9f5d7
|
||||
$bg0-soft: #f2e5bc
|
||||
$bg1: #ebdbb2
|
||||
$bg2: #d5c4a1
|
||||
$bg3: #bdae93
|
||||
$bg4: #a89984
|
||||
|
||||
// Light Foreground
|
||||
$fg0: #282828
|
||||
$fg0-hard: #1d2021
|
||||
$fg0-soft: #32302f
|
||||
$fg1: #3c3836
|
||||
$fg2: #504945
|
||||
$fg3: #665c54
|
||||
$fg4: #7c6f64
|
||||
|
||||
// Light Colors
|
||||
$dark-red: #cc241d
|
||||
$dark-green: #98971a
|
||||
$dark-yellow: #d79921
|
||||
$dark-blue: #458588
|
||||
$dark-purple: #b16286
|
||||
$dark-aqua: #689d6a
|
||||
$dark-orange: #d65d0e
|
||||
$dark-gray: #928374
|
||||
|
||||
$light-red: #9d0006
|
||||
$light-green: #79740e
|
||||
$light-yellow: #b57614
|
||||
$light-blue: #076678
|
||||
$light-purple: #8f3f71
|
||||
$light-aqua: #427b58
|
||||
$light-orange: #af3a03
|
||||
$light-gray: #7c6f64
|
||||
|
||||
$alt-red: #fb4934
|
||||
$alt-green: #b8bb26
|
||||
$alt-yellow: #fabd2f
|
||||
$alt-blue: #83a598
|
||||
$alt-purple: #d3869b
|
||||
$alt-aqua: #8ec07c
|
||||
$alt-orange: #f38019
|
||||
$alt-gray: #a89984
|
211
src/sass/main.sass
Normal file
211
src/sass/main.sass
Normal file
@ -0,0 +1,211 @@
|
||||
// this file should be imported into one that has the color definitions
|
||||
@import "gruvbox_light"
|
||||
$fg: $fg1 !default
|
||||
$fg-hl: $fg0 !default
|
||||
$fg-alt: $fg2 !default
|
||||
$fg-alt-hl: $fg1 !default
|
||||
$bg: $bg0-hard !default
|
||||
$bg-hl: $bg1 !default
|
||||
|
||||
$accent: $light-red !default
|
||||
|
||||
$button-bg: $bg !default
|
||||
$button-bg-hl: $bg-hl !default
|
||||
$button-fg: $fg !default
|
||||
$button-fg-hl: $fg-hl !default
|
||||
// use variables when they have to be overwriteable by inline style blocks
|
||||
|
||||
$monofont: "UbuntuMono", monospace
|
||||
|
||||
*
|
||||
font-family: "Ubuntu", Verdana, sans-serif
|
||||
color: $fg
|
||||
|
||||
|
||||
html, body // fill entire screen so that footer is always on bottom
|
||||
height: 100%
|
||||
margin: 0
|
||||
|
||||
body
|
||||
background-color: $bg
|
||||
// have main up top and footer at bottom
|
||||
display: flex
|
||||
flex-direction: column
|
||||
min-height: 100%
|
||||
|
||||
nav
|
||||
margin-top: 0.3rem
|
||||
flex-shrink: 0
|
||||
|
||||
main
|
||||
flex-grow: 1
|
||||
*
|
||||
margin-left: auto
|
||||
margin-right: auto
|
||||
text-align: center
|
||||
|
||||
|
||||
footer
|
||||
width: 100%
|
||||
padding: 0.2rem 1rem
|
||||
background: $fg
|
||||
color: $bg
|
||||
*
|
||||
color: $bg
|
||||
.statusline
|
||||
font-family: $monofont
|
||||
text-align: left
|
||||
*
|
||||
font-family: $monofont
|
||||
|
||||
|
||||
a
|
||||
color: $fg-hl
|
||||
|
||||
.path
|
||||
font-family: $monofont
|
||||
|
||||
.key
|
||||
padding: 3px 6px
|
||||
background: $fg1
|
||||
color: $bg0
|
||||
border-style: solid
|
||||
border-width: 1px
|
||||
border-color: $fg0-hard
|
||||
border-radius: 4px
|
||||
|
||||
// p
|
||||
// text-align: justify
|
||||
|
||||
code
|
||||
color: $fg1
|
||||
font-family: $monofont
|
||||
text-align: left
|
||||
|
||||
hr
|
||||
color: $accent
|
||||
|
||||
h1, h2
|
||||
text-align: center
|
||||
filter: drop-shadow(0.04em 0.04em 0 $accent)
|
||||
padding: 0px 20px
|
||||
|
||||
button, input
|
||||
color: $button-fg
|
||||
background-color: $button-bg
|
||||
font-size: 1rem
|
||||
padding: 0.5rem
|
||||
border-radius: 0
|
||||
filter: drop-shadow(0.1rem 0.1rem 0 $accent)
|
||||
font-family: $monofont
|
||||
margin: 0.2rem
|
||||
.move-buttons
|
||||
margin: 0
|
||||
button:hover
|
||||
color: $button-fg-hl
|
||||
background-color: $button-bg-hl
|
||||
|
||||
.add-mapping-form
|
||||
margin: auto
|
||||
width: 400px
|
||||
.form-row
|
||||
text-align: left
|
||||
*
|
||||
text-align: left
|
||||
label
|
||||
display: inline-block
|
||||
width: 100px
|
||||
text-align: right
|
||||
input
|
||||
margin-left: 0
|
||||
text-align: left
|
||||
button
|
||||
margin-left: 0
|
||||
// width: 50%
|
||||
|
||||
|
||||
|
||||
|
||||
#start-button
|
||||
filter: drop-shadow(0.1rem 0.1rem 0 $light-green)
|
||||
#configure-button
|
||||
filter: drop-shadow(0.1rem 0.1rem 0 $light-blue)
|
||||
#undo-button
|
||||
filter: drop-shadow(0.1rem 0.1rem 0 $light-gray)
|
||||
#skip-button
|
||||
filter: drop-shadow(0.1rem 0.1rem 0 $light-gray)
|
||||
#key
|
||||
width: 20px
|
||||
// #directory
|
||||
|
||||
.file-list
|
||||
width: 90%
|
||||
margin: auto
|
||||
margin-top: 1rem
|
||||
.file-list-content
|
||||
overflow: hidden
|
||||
border-color: $dark-blue
|
||||
border-style: solid
|
||||
border-width: 2px
|
||||
min-height: 0.2rem
|
||||
p
|
||||
margin: 0
|
||||
padding: 0.1rem
|
||||
text-align: left
|
||||
.file-list-item:nth-child(even)
|
||||
background: $bg1
|
||||
.file-list-item:nth-child(odd)
|
||||
background: $bg2
|
||||
|
||||
.selected-file-list-item
|
||||
background: $dark-blue !important
|
||||
color: $bg1
|
||||
|
||||
|
||||
.main-box
|
||||
background: $bg1
|
||||
.current-file
|
||||
width: 100%
|
||||
height: 300px
|
||||
position: relative
|
||||
overflow: scroll
|
||||
|
||||
.imgsort2
|
||||
font-size: 3rem
|
||||
line-height: 200px
|
||||
filter: drop-shadow(0.10rem 0.10rem 0 $accent)
|
||||
|
||||
img, video, audio
|
||||
max-width: 100%
|
||||
max-height: 100%
|
||||
width: auto
|
||||
height: auto
|
||||
position: absolute
|
||||
top: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
right: 0
|
||||
margin: auto
|
||||
|
||||
|
||||
|
||||
// Desktop optimierung
|
||||
@media only screen and (min-width: 1000px)
|
||||
main
|
||||
display: flex
|
||||
flex-direction: row
|
||||
.main-box
|
||||
width: 75%
|
||||
.imgsort2
|
||||
font-size: 5rem
|
||||
line-height: 300px
|
||||
filter: drop-shadow(0.20rem 0.20rem 0 $accent)
|
||||
|
||||
.second-box
|
||||
display: flex
|
||||
flex-direction: column
|
||||
width: 25%
|
||||
|
||||
.current-file
|
||||
height: 100%
|
||||
width: 100%
|
33
src/sass/table.sass
Normal file
33
src/sass/table.sass
Normal file
@ -0,0 +1,33 @@
|
||||
// Tabelle für Praktikumsseite
|
||||
$table-border-fg: $box-border-fg !default
|
||||
.midtable
|
||||
overflow-x: auto
|
||||
table
|
||||
border-collapse: collapse
|
||||
width: 75%
|
||||
margin: auto
|
||||
|
||||
caption
|
||||
font-size: $table-font-size
|
||||
padding: 10px
|
||||
|
||||
th, td, tr
|
||||
padding: 8px
|
||||
border: 2px solid $table-border-fg
|
||||
text-align: left
|
||||
font-size: $table-font-size
|
||||
margin: auto
|
||||
|
||||
td *
|
||||
vertical-align: middle
|
||||
|
||||
.flag
|
||||
height: 1.3em
|
||||
top: 0
|
||||
|
||||
tr:nth-child(even)
|
||||
background: rgba(255, 255, 255, 0.05)
|
||||
|
||||
@media only screen and (max-width: $max-width-medium)
|
||||
.midtable table
|
||||
width: 100%
|
229
src/style.css
Normal file
229
src/style.css
Normal file
@ -0,0 +1,229 @@
|
||||
* {
|
||||
font-family: "Ubuntu", Verdana, sans-serif;
|
||||
color: #3c3836;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f9f5d7;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
nav {
|
||||
margin-top: 0.3rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
flex-grow: 1;
|
||||
}
|
||||
main * {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
footer {
|
||||
width: 100%;
|
||||
padding: 0.2rem 1rem;
|
||||
background: #3c3836;
|
||||
color: #f9f5d7;
|
||||
}
|
||||
footer * {
|
||||
color: #f9f5d7;
|
||||
}
|
||||
|
||||
.statusline {
|
||||
font-family: "UbuntuMono", monospace;
|
||||
text-align: left;
|
||||
}
|
||||
.statusline * {
|
||||
font-family: "UbuntuMono", monospace;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #282828;
|
||||
}
|
||||
|
||||
.path {
|
||||
font-family: "UbuntuMono", monospace;
|
||||
}
|
||||
|
||||
.key {
|
||||
padding: 3px 6px;
|
||||
background: #3c3836;
|
||||
color: #fbf1c7;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: #1d2021;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
code {
|
||||
color: #3c3836;
|
||||
font-family: "UbuntuMono", monospace;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
hr {
|
||||
color: #9d0006;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
text-align: center;
|
||||
filter: drop-shadow(0.04em 0.04em 0 #9d0006);
|
||||
padding: 0px 20px;
|
||||
}
|
||||
|
||||
button, input {
|
||||
color: #3c3836;
|
||||
background-color: #f9f5d7;
|
||||
font-size: 1rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0;
|
||||
filter: drop-shadow(0.1rem 0.1rem 0 #9d0006);
|
||||
font-family: "UbuntuMono", monospace;
|
||||
margin: 0.2rem;
|
||||
}
|
||||
button .move-buttons, input .move-buttons {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
color: #282828;
|
||||
background-color: #ebdbb2;
|
||||
}
|
||||
|
||||
.add-mapping-form {
|
||||
margin: auto;
|
||||
width: 400px;
|
||||
}
|
||||
.add-mapping-form .form-row {
|
||||
text-align: left;
|
||||
}
|
||||
.add-mapping-form .form-row * {
|
||||
text-align: left;
|
||||
}
|
||||
.add-mapping-form .form-row label {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
text-align: right;
|
||||
}
|
||||
.add-mapping-form .form-row input {
|
||||
margin-left: 0;
|
||||
text-align: left;
|
||||
}
|
||||
.add-mapping-form .form-row button {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#start-button {
|
||||
filter: drop-shadow(0.1rem 0.1rem 0 #79740e);
|
||||
}
|
||||
|
||||
#configure-button {
|
||||
filter: drop-shadow(0.1rem 0.1rem 0 #076678);
|
||||
}
|
||||
|
||||
#undo-button {
|
||||
filter: drop-shadow(0.1rem 0.1rem 0 #7c6f64);
|
||||
}
|
||||
|
||||
#skip-button {
|
||||
filter: drop-shadow(0.1rem 0.1rem 0 #7c6f64);
|
||||
}
|
||||
|
||||
#key {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
width: 90%;
|
||||
margin: auto;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.file-list-content {
|
||||
overflow: hidden;
|
||||
border-color: #458588;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
min-height: 0.2rem;
|
||||
}
|
||||
.file-list-content p {
|
||||
margin: 0;
|
||||
padding: 0.1rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.file-list-item:nth-child(even) {
|
||||
background: #ebdbb2;
|
||||
}
|
||||
|
||||
.file-list-item:nth-child(odd) {
|
||||
background: #d5c4a1;
|
||||
}
|
||||
|
||||
.selected-file-list-item {
|
||||
background: #458588 !important;
|
||||
color: #ebdbb2;
|
||||
}
|
||||
|
||||
.main-box {
|
||||
background: #ebdbb2;
|
||||
}
|
||||
|
||||
.current-file {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
position: relative;
|
||||
}
|
||||
.current-file .imgsort2 {
|
||||
font-size: 3rem;
|
||||
line-height: 200px;
|
||||
filter: drop-shadow(0.1rem 0.1rem 0 #9d0006);
|
||||
}
|
||||
.current-file img, .current-file video, .current-file audio {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 1000px) {
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.main-box {
|
||||
width: 75%;
|
||||
}
|
||||
.imgsort2 {
|
||||
font-size: 5rem;
|
||||
line-height: 300px;
|
||||
filter: drop-shadow(0.2rem 0.2rem 0 #9d0006);
|
||||
}
|
||||
.second-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 25%;
|
||||
}
|
||||
.current-file {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=style.css.map */
|
Loading…
Reference in New Issue
Block a user