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