initial commit

This commit is contained in:
matth@ultra 2024-09-01 17:26:55 +02:00
parent ba419303ad
commit c06b80f204
10 changed files with 1142 additions and 2 deletions

View File

@ -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
View 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
View 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
View 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
View 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
View 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));
}
}

View 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
View 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
View 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
View 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 */