Compare commits

..

6 Commits

Author SHA1 Message Date
fb62fbccf7 add favicon 2024-09-02 14:28:29 +02:00
275bcf8351 restyle buttons 2024-09-02 14:28:06 +02:00
8cfba22090 update screenshots 2024-09-02 14:27:31 +02:00
400ae27ae0 add rename file 2024-09-02 13:27:59 +02:00
80858d71c7 improve style 2024-09-02 13:27:27 +02:00
c440214466 add load/save 2024-09-02 13:27:03 +02:00
14 changed files with 359 additions and 37 deletions

View File

@ -18,7 +18,7 @@ The web interface has mobile optimizations, letting you sort images/files on the
### Screenshots
<span style="display:inline-block; max-width: 70%; max-height: 510px ; margin-right: 4px">
<img style="display:block; max-width:100%; max-height: 250px; " src="resources/imgsort2-landscape.png" alt="Desktop interface"></img>
<img style="display:block; max-width:100%; max-height: 250px; margin-top: 10px" src="resources/configuration.png" alt="Configuration interface"></img>
<img style="display:block; max-width:100%; max-height: 250px; margin-top: 10px" src="resources/imgsort2-configuration.png" alt="Configuration interface"></img>
</span>
<img style="display:inline-block; max-width: 30%; max-height: 510px" src="resources/imgsort2-vertical.png" alt="Mobile interface"></img>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

BIN
resources/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 601 KiB

After

Width:  |  Height:  |  Size: 832 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 232 KiB

149
resources/logo.svg Normal file
View File

@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="256"
height="256"
viewBox="0 0 67.733332 67.733333"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
sodipodi:docname="logo.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="2.2173472"
inkscape:cx="146.34605"
inkscape:cy="134.39483"
inkscape:window-width="1896"
inkscape:window-height="1026"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Drop Shadow"
id="filter20"
x="0"
y="0"
width="1.0278316"
height="1.0385704">
<feFlood
result="flood"
in="SourceGraphic"
flood-opacity="1.000000"
flood-color="rgb(157,0,6)"
id="feFlood19" />
<feGaussianBlur
result="blur"
in="SourceGraphic"
stdDeviation="0.000000"
id="feGaussianBlur19" />
<feOffset
result="offset"
in="blur"
dx="1.000000"
dy="0.990099"
id="feOffset19" />
<feComposite
result="comp1"
operator="in"
in="flood"
in2="offset"
id="feComposite19" />
<feComposite
result="comp2"
operator="over"
in="SourceGraphic"
in2="comp1"
id="feComposite20" />
</filter>
<filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Drop Shadow"
id="filter23"
x="0"
y="0"
width="1.0529345"
height="1.0377">
<feFlood
result="flood"
in="SourceGraphic"
flood-opacity="1.000000"
flood-color="rgb(157,0,6)"
id="feFlood22" />
<feGaussianBlur
result="blur"
in="SourceGraphic"
stdDeviation="0.000000"
id="feGaussianBlur22" />
<feOffset
result="offset"
in="blur"
dx="1.000000"
dy="0.990099"
id="feOffset22" />
<feComposite
result="comp1"
operator="in"
in="flood"
in2="offset"
id="feComposite22" />
<feComposite
result="comp2"
operator="over"
in="SourceGraphic"
in2="comp1"
id="feComposite23" />
</filter>
</defs>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#ebdbb2;stroke-width:2.91042"
id="rect1"
width="67.733337"
height="67.733337"
x="0"
y="0"
ry="18.676485" />
<text
xml:space="preserve"
style="font-size:37.0417px;font-family:Impact;-inkscape-font-specification:Impact;text-align:center;text-anchor:middle;fill:#ffffff;stroke-width:2.28782;filter:url(#filter20)"
x="34.285786"
y="31.511433"
id="text1"><tspan
id="tspan1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:37.0417px;font-family:'Ubuntu Nerd Font';-inkscape-font-specification:'Ubuntu Nerd Font';text-align:center;text-anchor:middle;fill:#3c3836;stroke-width:2.28782"
x="34.285786"
y="31.511433"
sodipodi:role="line">Im</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:37.0417px;font-family:'Ubuntu Nerd Font';-inkscape-font-specification:'Ubuntu Nerd Font';text-align:center;text-anchor:middle;fill:#ebdbb2;stroke-width:2.91042;filter:url(#filter23)"
x="31.940487"
y="61.066525"
id="text2"><tspan
sodipodi:role="line"
id="tspan2"
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:37.0417px;font-family:'Ubuntu Nerd Font';-inkscape-font-specification:'Ubuntu Nerd Font Italic';fill:#3c3836;stroke-width:2.91042"
x="31.940487"
y="61.066525">2</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Imgsort 2 - Config</title>
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/x-icon" href="favicon.png">
</head>
<body>
@ -27,6 +28,9 @@
<button type="submit" name="submit">Add</button>
</div>
</form>
<h2>Save / Load</h2>
<button onclick="saveMappings()" id="save-button">Save mappings to file</button>
<button onclick="loadMappings()" id="load-button">Load mappings from file</button>
</div>
<div class="second-box" style="max-height: 100%">

View File

@ -2,6 +2,8 @@
* Handle mappings
* Mappings are stored in local storage as a Map (key: directory)
*/
const invalidFileNameCharsRe =/[^\\\|:\*\?"<>]/; // dont allow \/|:*?<>
const invalidFileNameChars = "\\|:*?<>";
let statusLine = document.getElementById('status');
@ -63,6 +65,56 @@ function addMapping(key, directory) {
renderMappings();
}
/*
* Save/Load from file
*/
function saveMappings() {
const mappings = new Map(JSON.parse(localStorage.getItem('mappings')) || []);
let dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(Array.from(mappings)));
let downloadElement = document.createElement('a');
downloadElement.style = "display: none;";
downloadElement.setAttribute("href", dataStr);
downloadElement.setAttribute("download", "mappings.json");
document.body.appendChild(downloadElement); // required for firefox
downloadElement.click();
downloadElement.remove();
}
function loadMappings() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = async (event) => {
const file = event.target.files[0];
if (!file) {
statusLine.textContent = "No file selected";
return;
}
try {
const fileText = await file.text();
const jsonData = JSON.parse(fileText);
// check is valid
if (typeof jsonData !== 'object' || jsonData === null) {
statusLine.textContent = "Invalid JSON structure";
return;
}
// TODO possibly implement sanity checks
localStorage.setItem('mappings', JSON.stringify(jsonData));
renderMappings();
} catch (error) {
console.error(error);;
statusLine = "Error loading file";
}
};
input.click();
}
// event listener for add mapping form
document.getElementById('addMappingForm').addEventListener('submit', function(event) {
event.preventDefault();

View File

@ -5,7 +5,10 @@
// All paths require trailing slashes!
$rootDir = __DIR__ . '/../images/';
// The URI at which the rootDir will be served
$rootPath = 'images/';
// Use /images/ for testing
// When the app is not served at the root https://example.com/ but at a sublocation like https://example.com/imgsort2/,
// you might need to set it to just `images` without the /
$rootPath = '/images/';
// Path of the directory containing all the files to be sorted, relative to $rootDir
$stagingDirName = '.sort/';
@ -77,27 +80,23 @@
/**
* Move a file from staging to the destination directory
* @param file relative to stagingDir
* @param dir relative to rootDir
* @param dest new filename relative to rootDir
*/
function moveFile($file, $dir) {
function moveFile($file, $dest) {
global $stagingDir, $rootDir;
$fullFile = $stagingDir . $file;
$fullDir = $rootDir . $dir . "/";
$fullDest = $fullDir . basename($fullFile);
$fullDest = $rootDir . $dest;
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
* Like movefile, except reversed
*/
function undoFile($file, $dir) {
function undoFile($file, $dest) {
global $stagingDir, $rootDir;
$fullDir = $rootDir . $dir . "/";
$fullFile = $fullDir . $file;
$fullDest = $stagingDir . basename($fullFile);
return saveMove($fullFile, $fullDest);
$fullFile = $stagingDir . $file;
$fullDest = $rootDir . $dest;
return saveMove($fullDest, $fullFile);
}

View File

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Imgsort 2</title>
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/x-icon" href="favicon.png">
</head>
<body>
<script src="index.js" async></script>
@ -21,10 +22,12 @@
</div>
<div class="second-box">
<div id="buttons">
<button onclick="undo()" id="undo-button">Undo</button>
<button onclick="skip()" id="skip-button">Skip</button>
<div class="buttons" id="buttons">
<input id="filename" placeholder="filename" type="text" class="filename-input"></input>
<button onclick="undo()" class="move-button" id="undo-button">Undo</button>
<button onclick="skip()" class="move-button" id="skip-button">Skip</button>
<span class="move-buttons" id="move-buttons"></span>
<br>
<button onclick="window.location.href='config.html';" id="configure-button">Configure</button>
</div>
<div class="file-list">

View File

@ -1,7 +1,10 @@
const imageExtensions = ['gif','jpg','jpeg','png', 'webp'];
const imageExtensions = ['gif','jpg','jpeg','png', 'webp', 'svg', 'pdf'];
const videoExtensions =['mpg', 'mp2', 'mpeg', 'mpe', 'mpv', 'mp4'];
const audioExtensions =['mp3', 'ogg', 'wav', 'flac'];
const invalidFileNameCharsRe =/[\\/\|:\*\?"<>]/; // dont allow \/|:*?<>
const invalidFileNameChars = "\\/|:*?<>";
const mappings = new Map(JSON.parse(localStorage.getItem('mappings')) || []);
let statusLine = document.getElementById('status');
// store for when done with sorting TODO display
@ -75,13 +78,13 @@ 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() {
const moveButton = document.createElement('button');
moveButton.innerHTML = `<span class="key">${key}</span> <span class="directory">${directory}</span>`;
moveButton.onclick = function() {
moveFile(directory);
};
removeButton.class = "move-button";
buttons.appendChild(removeButton);
moveButton.className = "move-button";
buttons.appendChild(moveButton);
});
}
@ -203,23 +206,71 @@ function setCurrentFile() {
currentFile = "";
statusLine.textContent = "No more files to sort";
}
// set filename in input field
const fileNameNoExt = currentFile.replace(/\.[^/.]+$/, "");
document.getElementById('filename').value = fileNameNoExt;
renderCurrentFile();
renderFileList();
}
function setCurrentFileFromName(filename) {
let idx = fileList.indexOf(filename);
if (idx < 0) {
message = `Can not set file from name '${filename}': Not found in list`;
statusLine.textContent = message;
console.error(message);
return;
}
currentFileIdx = idx;
setCurrentFile();
}
// Event delegation
document.getElementById('file-list').addEventListener('click', function(event) {
if (event.target && event.target.tagName === 'P') {
// Call the function with the content of the clicked paragraph
if (event.target.textContent != "No files") {
setCurrentFileFromName(event.target.textContent);
}
}
});
async function moveFile(directory) {
if (!currentFile) {
statusLine.innerHTML = "No file to process!";
return
statusLine.textContent = "No file to process!";
return;
}
const response = await fetch(`imgsort.php?action=moveFile&file=${escape(currentFile)}&dest=${directory}`);
const userSetFilename = document.getElementById('filename').value;
if (!userSetFilename) {
statusLine.textContent = "Filename must not be empty";
return;
}
// file names invalid on windows not handled
if (invalidFileNameCharsRe.test(userSetFilename)) {
statusLine.textContent = `Filename must not contain ${invalidFileNameChars}`;
return;
}
// re-add the extension
const fileExt = currentFile.split('.').pop();
let filename = userSetFilename + '.' + fileExt;
let newFilePath = directory + '/' + filename;
const response = await fetch(`imgsort.php?action=moveFile&file=${escape(currentFile)}&dest=${escape(newFilePath)}`);
const text = await response.text();
// console.log(text);
if (response.ok) {
if (filename != currentFile) {
statusLine.textContent = `Moved '${currentFile}' to '${directory}'`;
history.push([currentFile, directory]);
} else {
statusLine.textContent = `Moved '${currentFile}' to '${directory}' as '${filename}'`;
}
history.push([currentFile, newFilePath]);
// remove file from list
fileList.splice(currentFileIdx, 1);
// set next file
@ -237,12 +288,12 @@ async function undo() {
statusLine.textContent = "Nothing to undo"
return;
}
const [file, directory] = history.pop();
const response = await fetch(`imgsort.php?action=undoFile&file=${escape(file)}&dest=${directory}`);
const [file, newFilePath] = history.pop();
const response = await fetch(`imgsort.php?action=undoFile&file=${escape(file)}&dest=${escape(newFilePath)}`);
const text = await response.text();
// console.log(text);
if (response.ok) {
statusLine.innerHTML = `Undo: move '${file}' to '${directory}'`;
statusLine.textContent = `Undo: move '${file}' to '${newFilePath}'`;
await setFileList()
setCurrentFile();
}
@ -260,13 +311,15 @@ function skip() {
async function initialize() {
// remove the value on reload
document.getElementById('filename').value = "";
await setStagingPath();
await setFileList();
renderMoveButtons();
renderFileList();
// setCurrentFile();
preloadNextFile();
statusLine.textContent = "Loaded";
statusLine.textContent = "Ready";
}
// load existing mappings on page load
@ -276,6 +329,11 @@ window.onload = function() {
// handle keyboard
document.onkeypress = function(e) {
// dont handle keys while user is typing in filename box
if (document.querySelector('input') === document.activeElement) {
return;
}
e = e || window.event;
let keydebug = document.getElementById("keydebug");
if (keydebug) {

View File

@ -24,10 +24,13 @@ $file-height: 300px
$splash-font-size: 40px
$button-font-size: $font-size
$button-padding: 8px
$move-button-font-size: $font-size - 2px
$move-button-padding: $button-padding - 2px
*
font-family: "Ubuntu", Verdana, sans-serif
color: $fg
// font-size: $font-size
html, body // fill entire screen so that footer is always on bottom
@ -94,6 +97,7 @@ footer
min-height: $footer-height // idk why but this is required
overflow-x: scroll
overflow-y: hidden
white-space: nowrap
*
color: $bg
.statusline
@ -117,6 +121,7 @@ a
border-width: 1px
border-color: $fg0-hard
border-radius: 4px
font-family: $monofont
// p
// text-align: justify
@ -145,6 +150,11 @@ button, input
margin: 0.2rem
.move-buttons
margin: 0
.move-button
font-size: $move-button-font-size
padding: $move-button-padding
button:hover
color: $button-fg-hl
background-color: $button-bg-hl
@ -161,6 +171,11 @@ button:hover
width: 20px
text-align: center
// #directory
#save-button
filter: drop-shadow(0.1rem 0.1rem 0 $light-red)
#load-button
filter: drop-shadow(0.1rem 0.1rem 0 $light-blue)
.add-mapping-form
@ -183,6 +198,10 @@ button:hover
// width: 50%
.filename-input
width: 90%
margin: auto
padding: 0.1rem
.file-list
width: 90%
margin: auto
@ -197,13 +216,20 @@ button:hover
margin: 0
padding: 0.1rem
text-align: left
.file-list-item:hover
background: $dark-blue !important
color: $bg1
cursor: pointer
.file-list-item:nth-child(even)
background: $bg1
.file-list-item:nth-child(odd)
background: $bg2
.selected-file-list-item
background: $dark-blue !important
background: $light-blue !important
color: $bg1
@ -212,6 +238,7 @@ button:hover
main
display: flex
flex-direction: row
padding-bottom: $footer-height + 2 * $footer-padding-y
.main-box
width: 75%
max-height: calc(100vh - $footer-height - 2 * $footer-padding-y)
@ -232,6 +259,8 @@ button:hover
width: 100%
footer
width: 100%
position: fixed

View File

@ -102,6 +102,7 @@ a {
border-width: 1px;
border-color: #1d2021;
border-radius: 4px;
font-family: "UbuntuMono", monospace;
}
code {
@ -130,10 +131,16 @@ button, input {
font-family: "UbuntuMono", monospace;
margin: 0.2rem;
}
button .move-buttons, input .move-buttons {
.move-buttons {
margin: 0;
}
.move-button {
font-size: 14px;
padding: 6px;
}
button:hover {
color: #282828;
background-color: #ebdbb2;
@ -160,6 +167,14 @@ button:hover {
text-align: center;
}
#save-button {
filter: drop-shadow(0.1rem 0.1rem 0 #9d0006);
}
#load-button {
filter: drop-shadow(0.1rem 0.1rem 0 #076678);
}
.add-mapping-form {
margin: auto;
padding-bottom: 0.4rem;
@ -184,6 +199,12 @@ button:hover {
margin-left: 0;
}
.filename-input {
width: 90%;
margin: auto;
padding: 0.1rem;
}
.file-list {
width: 90%;
margin: auto;
@ -203,6 +224,12 @@ button:hover {
text-align: left;
}
.file-list-item:hover {
background: #458588 !important;
color: #ebdbb2;
cursor: pointer;
}
.file-list-item:nth-child(even) {
background: #ebdbb2;
}
@ -212,7 +239,7 @@ button:hover {
}
.selected-file-list-item {
background: #458588 !important;
background: #076678 !important;
color: #ebdbb2;
}
@ -220,6 +247,7 @@ button:hover {
main {
display: flex;
flex-direction: row;
padding-bottom: 24px;
}
.main-box {
width: 75%;