feat: add conversion page

This commit is contained in:
Louis Heredero 2025-05-04 18:25:23 +02:00
parent 4a4949b474
commit 460ae94925
Signed by: HEL
GPG Key ID: 8D83DE470F8544E7
7 changed files with 1147 additions and 26 deletions

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>Melies - Conversion</title>
<link rel="stylesheet" href="/static/css/base.css">
<link rel="stylesheet" href="/static/css/conversion.css">
<link rel="shortcut icon" href="/static/images/icon3.svg" type="image/svg+xml">
<script src="/static/js/conversion.js"></script>
</head>
<body>
<header>
<h1>Media Conversion</h1>
</header>
<main>
<button id="folder-template" class="template file folder">
<img src="/static/images/collection.svg">
<div class="name"></div>
<div class="children"><span class="num"></span> element(s)</div>
</button>
<button id="media-template" class="template file media">
<img src="/static/images/film.svg">
<div class="name"></div>
<div class="info"><span class="size"></span> / <span class="ext"></span></div>
</button>
<div id="selected-template" class="template file">
<button class="deselect">Deselect</button>
<div class="name"></div>
</div>
<label id="agent-template" class="template agent">
<input type="radio" name="selected-agent">
<div class="container">
<div class="icon"></div>
<div class="name"></div>
</div>
</label>
<div id="to-convert-panel" class="panel">
<h2 class="title">To convert</h2>
<button id="up">Parent directory</button>
<div id="no-file" class="empty-msg">Director is empty</div>
<div id="files"></div>
</div>
<div id="selected-panel" class="panel">
<h2 class="title">Selected</h2>
<div class="toolbar">
<button id="show-files" class="hidden"><img src="/static/images/prev.svg">Back to files</button>
<button id="deselect-all">Deselect All</button>
<button id="continue" disabled>
<div class="disabled">Select a media</div>
<div class="enabled">Continue<img src="/static/images/next.svg"></div>
</button>
</div>
<div id="selected"></div>
</div>
<div id="agents-panel" class="panel hidden">
<h2 class="title">Agents</h2>
<div id="no-agent" class="empty-msg show">No registered agent</div>
<div id="agents"></div>
<button id="convert" disabled>
<div class="disabled">Select an agent</div>
<div class="enabled">Convert</div>
</button>
</div>
</main>
</body>
</html>

View File

@ -6,6 +6,7 @@
<title>Melies - Metadata Editor</title> <title>Melies - Metadata Editor</title>
<link rel="stylesheet" href="/static/css/base.css"> <link rel="stylesheet" href="/static/css/base.css">
<link rel="stylesheet" href="/static/css/edit.css"> <link rel="stylesheet" href="/static/css/edit.css">
<link rel="shortcut icon" href="/static/images/icon3.svg" type="image/svg+xml">
<script src="/static/js/edit.mjs" type="module"></script> <script src="/static/js/edit.mjs" type="module"></script>
</head> </head>
<body> <body>

View File

@ -0,0 +1,265 @@
body {
display: flex;
flex-direction: column;
}
main {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1em;
flex: 1;
overflow-y: hidden;
}
.panel {
display: flex;
flex-direction: column;
padding: 0.8em;
border: solid black 2px;
overflow: hidden;
&.hidden {
display: none;
}
.title {
margin: 0.4em 0;
}
}
button {
font-family: inherit;
font-size: inherit;
padding: 0.4em 0.8em;
border-radius: 0.4em;
background: none;
cursor: pointer;
border: solid #c5c5c5 2px;
text-align: center;
font-weight: bold;
&:hover {
background-color: #f8f8f8;
}
}
#up {
width: 100%;
padding: 0.8em 1.6em;
border-radius: 1.2em;
&:not(.show) {
display: none;
}
}
#files {
display: grid;
grid-template-columns: repeat(auto-fill, 12em);
grid-auto-rows: 12em;
gap: 0.8em;
place-items: center;
padding: 0.8em 0;
justify-content: space-evenly;
height: 100%;
overflow: auto;
.file {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 100%;
height: 100%;
text-decoration: none;
color: black;
font-family: inherit;
font-size: inherit;
padding: 0.4em;
border-radius: 1.2em;
background: none;
cursor: pointer;
gap: 0.2em;
border: solid transparent 2px;
&.selected {
border-color: rgb(153, 226, 153);
}
&:hover {
background-color: #f8f8f8;
}
img {
width: 5em;
height: 5em;
}
.name {
overflow-wrap: anywhere;
text-align: center;
font-weight: bold;
flex-shrink: 1;
overflow: hidden;
}
.info, .children {
font-size: 80%;
color: rgb(75, 75, 75);
font-style: italic;
font-weight: normal;
}
}
}
#selected {
display: flex;
flex-direction: column;
height: 100%;
overflow: auto;
.file {
display: flex;
padding: 0.4em 0.8em;
gap: 0.4em;
align-items: center;
&:nth-child(even) {
background-color: #fafafa;
}
.deselect {
background-color: #ffd8c6;
&:hover {
background-color: #ffc3a7;
}
}
.name {
overflow-wrap: anywhere;
}
}
}
.toolbar {
display: flex;
gap: 0.4em;
button, button .enabled {
display: flex;
gap: 0.2em;
align-items: center;
&.hidden {
display: none;
}
img {
width: 1.4em;
height: 1.4em;
}
}
}
.empty-msg {
color: #4d4d4d;
font-style: italic;
font-size: 120%;
text-align: center;
padding: 1.2em 0.4em;
&:not(.show) {
display: none;
}
}
#agents {
display: grid;
grid-template-columns: repeat(auto-fill, 12em);
grid-auto-rows: 12em;
gap: 0.8em;
place-items: center;
padding: 0.8em 0;
justify-content: space-evenly;
height: 100%;
overflow: auto;
.agent {
input {
display: none;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 100%;
height: 100%;
text-decoration: none;
color: black;
font-family: inherit;
font-size: inherit;
padding: 0.4em;
border-radius: 1.2em;
background: none;
cursor: pointer;
gap: 0.2em;
border: none;
&.selected {
border-color: #99e299;
}
&:hover {
background-color: #f8f8f8;
}
.icon {
width: 8em;
height: 8em;
mask-image: url("/static/images/agent.svg");
mask-size: contain;
background-color: black;
}
.name {
overflow-wrap: anywhere;
text-align: center;
font-weight: bold;
font-size: 120%;
flex-shrink: 1;
overflow: hidden;
}
}
input:checked + .container {
.icon {
background-color: #1bb11b;
}
.name {
color: #1bb11b;
}
}
}
}
#convert {
font-size: 120%;
}
#continue, #convert {
&:not(:disabled) {
.disabled {
display: none;
}
}
&:disabled {
.enabled {
display: none;
}
}
}

View File

@ -0,0 +1,498 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="128"
height="128"
viewBox="0 0 128 128"
version="1.1"
id="svg1"
sodipodi:docname="agent.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
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"
showgrid="true"
inkscape:zoom="5.7798541"
inkscape:cx="66.610678"
inkscape:cy="60.468655"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
id="grid1"
units="px"
originx="0"
originy="0"
spacingx="1"
spacingy="1"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="16"
enabled="true"
visible="true" />
</sodipodi:namedview>
<defs
id="defs1">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect17"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect16"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,2.3893809,0,1 @ F,0,1,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect15"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,0,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,1.9863443,0,1 @ F,0,1,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,3.1885909,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,3.1885909,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,3.1885909,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,3.1885909,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,3.1885909,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,3.1885909,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="copy_rotate"
starting_point="80,71.686292"
origin="64,71.686292"
id="path-effect8"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
method="fuse_paths"
num_copies="8"
starting_angle="0"
rotation_angle="45"
gap="-0.01"
copies_to_360="true"
mirror_copies="false"
split_items="false"
link_styles="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect7"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1 @ F,0,1,1,0,2.8348774,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect4"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,3,0,1 @ F,0,1,1,0,3,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect3"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,9,0,1 @ F,0,1,1,0,9,0,1 @ F,0,1,1,0,9,0,1 @ F,0,1,1,0,9,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect4-7"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,3,0,1 @ F,0,1,1,0,3,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect4-2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,3,0,1 @ F,0,1,1,0,3,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect4-0"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,3,0,1 @ F,0,1,1,0,3,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect4-7-9"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,3,0,1 @ F,0,1,1,0,3,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect4-2-3"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,3,0,1 @ F,0,1,1,0,3,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect4-79"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,3,0,1 @ F,0,1,1,0,3,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect4-7-2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,3,0,1 @ F,0,1,1,0,3,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect4-2-0"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,3,0,1 @ F,0,1,1,0,3,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect4-8"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,3,0,1 @ F,0,1,1,0,3,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect4-7-97"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,3,0,1 @ F,0,1,1,0,3,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect4-2-36"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,3,0,1 @ F,0,1,1,0,3,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 32,41 v 46 a 9,9 45 0 0 9,9 h 46 a 9,9 135 0 0 9,-9 V 41 A 9,9 45 0 0 87,32 H 41 a 9,9 135 0 0 -9,9 z"
id="path3"
inkscape:path-effect="#path-effect3"
inkscape:original-d="M 32,32 V 96 H 96 V 32 Z" />
<g
id="g4">
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 32,45 H 19 a 3,3 135 0 0 -3,3 3,3 45 0 0 3,3 h 13"
id="path4"
inkscape:path-effect="#path-effect4"
inkscape:original-d="M 32,45 H 16 v 6 h 16"
sodipodi:nodetypes="cccc" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 32,45 H 19 a 3,3 135 0 0 -3,3 3,3 45 0 0 3,3 h 13"
id="path4-3"
inkscape:path-effect="#path-effect4-7"
inkscape:original-d="M 32,45 H 16 v 6 h 16"
sodipodi:nodetypes="cccc"
transform="translate(0,16)" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 32,45 H 19 a 3,3 135 0 0 -3,3 3,3 45 0 0 3,3 h 13"
id="path4-1"
inkscape:path-effect="#path-effect4-2"
inkscape:original-d="M 32,45 H 16 v 6 h 16"
sodipodi:nodetypes="cccc"
transform="translate(0,32)" />
</g>
<g
id="g4-0"
transform="matrix(-1,0,0,1,128,0)">
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 32,45 H 19 a 3,3 135 0 0 -3,3 3,3 45 0 0 3,3 h 13"
id="path4-6"
inkscape:path-effect="#path-effect4-0"
inkscape:original-d="M 32,45 H 16 v 6 h 16"
sodipodi:nodetypes="cccc" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 32,45 H 19 a 3,3 135 0 0 -3,3 3,3 45 0 0 3,3 h 13"
id="path4-3-2"
inkscape:path-effect="#path-effect4-7-9"
inkscape:original-d="M 32,45 H 16 v 6 h 16"
sodipodi:nodetypes="cccc"
transform="translate(0,16)" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 32,45 H 19 a 3,3 135 0 0 -3,3 3,3 45 0 0 3,3 h 13"
id="path4-1-6"
inkscape:path-effect="#path-effect4-2-3"
inkscape:original-d="M 32,45 H 16 v 6 h 16"
sodipodi:nodetypes="cccc"
transform="translate(0,32)" />
</g>
<g
id="g4-3"
transform="rotate(90,64,64)">
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 32,45 H 19 a 3,3 135 0 0 -3,3 3,3 45 0 0 3,3 h 13"
id="path4-7"
inkscape:path-effect="#path-effect4-79"
inkscape:original-d="M 32,45 H 16 v 6 h 16"
sodipodi:nodetypes="cccc" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 32,45 H 19 a 3,3 135 0 0 -3,3 3,3 45 0 0 3,3 h 13"
id="path4-3-5"
inkscape:path-effect="#path-effect4-7-2"
inkscape:original-d="M 32,45 H 16 v 6 h 16"
sodipodi:nodetypes="cccc"
transform="translate(0,16)" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 32,45 H 19 a 3,3 135 0 0 -3,3 3,3 45 0 0 3,3 h 13"
id="path4-1-9"
inkscape:path-effect="#path-effect4-2-0"
inkscape:original-d="M 32,45 H 16 v 6 h 16"
sodipodi:nodetypes="cccc"
transform="translate(0,32)" />
</g>
<g
id="g4-2"
transform="rotate(-90,64,64)">
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 32,45 H 19 a 3,3 135 0 0 -3,3 3,3 45 0 0 3,3 h 13"
id="path4-9"
inkscape:path-effect="#path-effect4-8"
inkscape:original-d="M 32,45 H 16 v 6 h 16"
sodipodi:nodetypes="cccc" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 32,45 H 19 a 3,3 135 0 0 -3,3 3,3 45 0 0 3,3 h 13"
id="path4-3-3"
inkscape:path-effect="#path-effect4-7-97"
inkscape:original-d="M 32,45 H 16 v 6 h 16"
sodipodi:nodetypes="cccc"
transform="translate(0,16)" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 32,45 H 19 a 3,3 135 0 0 -3,3 3,3 45 0 0 3,3 h 13"
id="path4-1-1"
inkscape:path-effect="#path-effect4-2-36"
inkscape:original-d="M 32,45 H 16 v 6 h 16"
sodipodi:nodetypes="cccc"
transform="translate(0,32)" />
</g>
<path
id="path15"
style="baseline-shift:baseline;display:inline;overflow:visible;fill:#000000;fill-opacity:0.33900854;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;enable-background:accumulate;stop-color:#000000;stop-opacity:1;stroke-opacity:1;stroke-width:2;stroke-dasharray:none"
d="M 63.496094 38.013672 A 4.2237119 4.2237119 0 0 0 59.345703 41.453125 L 58.523438 45.792969 A 2.2921583 2.2921583 0 0 1 54.980469 47.261719 L 51.337891 44.777344 A 4.2259625 4.2259625 0 0 0 45.970703 45.28125 L 45.267578 45.982422 A 4.2239053 4.2239053 0 0 0 44.765625 51.349609 L 47.251953 54.998047 A 2.292144 2.292144 0 0 1 45.785156 58.541016 L 41.453125 59.363281 A 4.2252807 4.2252807 0 0 0 38.013672 63.513672 L 38.013672 64.505859 A 4.2247307 4.2247307 0 0 0 41.453125 68.65625 L 45.792969 69.478516 A 2.2921527 2.2921527 0 0 1 47.261719 73.021484 L 44.777344 76.664062 A 4.2252132 4.2252132 0 0 0 45.28125 82.03125 L 45.982422 82.732422 A 4.2246351 4.2246351 0 0 0 51.349609 83.234375 L 54.998047 80.748047 A 2.292138 2.292138 0 0 1 58.541016 82.214844 L 59.363281 86.546875 A 4.2263602 4.2263602 0 0 0 63.513672 89.986328 L 64.503906 89.988281 A 4.2236546 4.2236546 0 0 0 68.654297 86.548828 L 69.476562 82.208984 A 2.2921525 2.2921525 0 0 1 73.019531 80.740234 L 76.662109 83.224609 A 4.2259565 4.2259565 0 0 0 82.029297 82.720703 L 82.732422 82.019531 A 4.2239049 4.2239049 0 0 0 83.234375 76.652344 L 80.748047 73.003906 A 2.292144 2.292144 0 0 1 82.214844 69.460938 L 86.546875 68.638672 A 4.2252807 4.2252807 0 0 0 89.986328 64.488281 L 89.986328 63.496094 A 4.2247307 4.2247307 0 0 0 86.546875 59.345703 L 82.207031 58.523438 A 2.2921527 2.2921527 0 0 1 80.738281 54.980469 L 83.222656 51.337891 A 4.2259563 4.2259563 0 0 0 82.71875 45.970703 L 82.017578 45.267578 A 4.2239034 4.2239034 0 0 0 76.650391 44.765625 L 73.001953 47.251953 A 2.2922228 2.2922228 0 0 1 69.457031 45.785156 L 68.638672 41.455078 A 4.226441 4.226441 0 0 0 64.486328 38.015625 L 63.496094 38.013672 z M 64 54 A 10 10 0 0 1 74 64 A 10 10 0 0 1 64 74 A 10 10 0 0 1 54 64 A 10 10 0 0 1 64 54 z " />
<path
sodipodi:type="star"
style="display:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="path8"
inkscape:flatsided="true"
sodipodi:sides="8"
sodipodi:cx="64"
sodipodi:cy="64"
sodipodi:r1="16"
sodipodi:r2="14.782073"
sodipodi:arg1="0"
sodipodi:arg2="0.39269908"
inkscape:rounded="0"
inkscape:randomized="0"
d="M 80,64 75.313708,75.313708 64,80 52.686292,75.313708 48,64 52.686292,52.686292 64,48 75.313708,52.686292 Z"
transform="rotate(-22.5,64,64)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,231 @@
let selected = []
let currentPath = []
const SIZES = ["", "K", "M", "G", "T"]
function formatSize(bytes) {
let order = Math.floor(Math.log10(bytes) / 3)
if (bytes > Math.pow(10, (order + 1) * 3) / 2) {
order += 1
}
let size = bytes / Math.pow(10, order * 3)
size = Math.round(size * 10) / 10
const prefix = SIZES[order]
return `${size}${prefix}B`
}
function makeFolder(meta) {
const file = document.getElementById("folder-template").cloneNode(true)
file.querySelector(".name").innerText = meta.filename
file.querySelector(".children .num").innerText = meta.elements
file.addEventListener("dblclick", () => {
currentPath.push(meta.filename)
navigate()
})
return file
}
function makeMedia(meta) {
const file = document.getElementById("media-template").cloneNode(true)
file.querySelector(".name").innerText = meta.filename.split(".").slice(0, -1).join(".")
file.querySelector(".size").innerText = formatSize(meta.size)
file.querySelector(".ext").innerText = meta.filename.split(".").slice(-1)[0].toUpperCase()
return file
}
function makeFile(meta) {
let file
switch (meta.type) {
case "folder":
file = makeFolder(meta)
break
case "media":
file = makeMedia(meta)
break
default:
throw new Error(`Invalid file type '${meta.type}'`)
}
file.title = meta.filename
file.id = null
file.dataset.path = meta.path
file.classList.remove("template")
if (selected.includes(meta.path)) {
file.classList.add("selected")
}
file.addEventListener("click", e => {
if (e.ctrlKey) {
e.preventDefault()
toggleSelectFile(meta)
}
})
return file
}
function makeAgent(meta) {
const agent = document.getElementById("agent-template").cloneNode(true)
agent.classList.remove("template")
agent.removeAttribute("id")
agent.dataset.uuid = meta.uuid
agent.querySelector(".name").innerText = meta.name
const input = agent.querySelector("input[type='radio']")
input.value = meta.uuid
input.addEventListener("change", () => updateConvertBtn())
return agent
}
function toggleSelectFile(meta) {
if (selected.includes(meta.path)) {
deselectFile(meta.path)
} else {
selectFile(meta)
}
}
function selectFile(meta) {
selected.push(meta.path)
document.querySelector(`.file[data-path='${meta.path}']`)?.classList?.add("selected")
const list = document.getElementById("selected")
const line = document.getElementById("selected-template").cloneNode(true)
line.classList.remove("template")
line.id = null
line.querySelector(".name").innerText = meta.path
line.dataset.path = meta.path
line.querySelector(".deselect").addEventListener("click", () => {
deselectFile(meta.path)
})
list.appendChild(line)
const continueBtn = document.getElementById("continue")
continueBtn.disabled = false
}
function deselectFile(path) {
selected = selected.filter(p => p !== path)
document.querySelector(`#files .file[data-path='${path}']`)?.classList?.remove("selected")
document.querySelector(`#selected .file[data-path='${path}']`)?.remove()
const continueBtn = document.getElementById("continue")
continueBtn.disabled = selected.length === 0
}
function deselectAll() {
selected.forEach(path => {
deselectFile(path)
})
showFiles()
}
/**
*
* @param {object[]} files
*/
function addFiles(files) {
const emptyMsg = document.getElementById("no-file")
if (files.length === 0) {
emptyMsg.classList.add("show")
} else {
emptyMsg.classList.remove("show")
}
const list = document.getElementById("files")
const list2 = document.createElement("div")
list2.innerHTML = ""
const filenames = files.map(meta => meta.filename)
// Copy array because sort changes it in place
Array.from(filenames).sort().forEach(filename => {
const i = filenames.indexOf(filename)
const meta = files[i]
const file = makeFile(meta)
list2.appendChild(file)
})
list.replaceChildren(...list2.children)
const upBtn = document.getElementById("up")
if (currentPath.length === 0) {
upBtn.classList.remove("show")
} else {
upBtn.classList.add("show")
}
}
function navigate() {
const url = new URL("/api/files/to_convert", window.location.origin)
url.searchParams.set("f", currentPath.join("/"))
fetch(url.href).then(res => {
return res.json()
}).then(files => {
addFiles(files)
})
}
function showFiles() {
document.getElementById("show-files").classList.add("hidden")
document.getElementById("continue").classList.remove("hidden")
document.getElementById("agents-panel").classList.add("hidden")
document.getElementById("to-convert-panel").classList.remove("hidden")
}
function showAgents() {
document.getElementById("continue").classList.add("hidden")
document.getElementById("show-files").classList.remove("hidden")
document.getElementById("to-convert-panel").classList.add("hidden")
document.getElementById("agents-panel").classList.remove("hidden")
}
function updateConvertBtn() {
const agent = document.querySelector("#agents .agent input:checked")
const convertBtn = document.getElementById("convert")
if (agent) {
convertBtn.disabled = false
} else {
convertBtn.disabled = true
}
}
function addAgents(agents) {
const emptyMsg = document.getElementById("no-agent")
if (agents.length === 0) {
emptyMsg.classList.add("show")
} else {
emptyMsg.classList.remove("show")
}
const selectedAgent = document.querySelector("#agents .agent input:checked")?.parentElement
const list = document.getElementById("agents")
const list2 = document.createElement("div")
list2.innerHTML = ""
const agentNames = agents.map(meta => meta.name)
// Copy array because sort changes it in place
Array.from(agentNames).sort().forEach(name => {
const i = agentNames.indexOf(name)
const meta = agents[i]
const agent = makeAgent(meta)
if (selectedAgent?.dataset?.uuid === meta.uuid) {
agent.querySelector("input").checked = true
}
list2.appendChild(agent)
})
list.replaceChildren(...list2.children)
updateConvertBtn()
}
window.addEventListener("load", () => {
document.getElementById("up").addEventListener("dblclick", () => {
if (currentPath.length !== 0) {
currentPath = currentPath.slice(0, -1)
navigate()
}
})
document.getElementById("show-files").addEventListener("click", () => {
showFiles()
})
document.getElementById("deselect-all").addEventListener("click", () => {
deselectAll()
})
document.getElementById("continue").addEventListener("click", () => {
showAgents()
})
navigate()
})

View File

@ -94,7 +94,7 @@ function sortFiles() {
} }
window.addEventListener("load", () => { window.addEventListener("load", () => {
fetch("/api/files").then(res => { fetch("/api/files/metadata").then(res => {
return res.json() return res.json()
}).then(files => { }).then(files => {
addFiles(files) addFiles(files)

View File

@ -41,11 +41,14 @@ class EnvDefault(argparse.Action):
class HTTPHandler(SimpleHTTPRequestHandler): class HTTPHandler(SimpleHTTPRequestHandler):
SERVER: MeliesServer = None SERVER: MeliesServer = None
CACHE = {} METADATA_CACHE = {}
TO_CONVERT_CACHE = {}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.MAX_PAYLOAD_SIZE: int = self.SERVER.max_payload_size self.MAX_PAYLOAD_SIZE: int = self.SERVER.max_payload_size
self.DATA_DIR: str = self.SERVER.metadata_dir self.TO_CONVERT_DIR: str = self.SERVER.to_convert_dir
self.CONVERTED_DIR: str = self.SERVER.converted_dir
self.METADATA_DIR: str = self.SERVER.metadata_dir
super().__init__( super().__init__(
*args, *args,
@ -78,16 +81,20 @@ class HTTPHandler(SimpleHTTPRequestHandler):
return True return True
def do_GET(self): def do_GET(self):
self.path = unquote(self.path) parsed = urlparse(unquote(self.path))
self.query = parse_qs(urlparse(self.path).query) self.path = parsed.path
self.query = parse_qs(parsed.query)
if self.path.startswith("/api/"): if self.path.startswith("/api/"):
self.handle_api_get(self.path.removeprefix("/api/").removesuffix("/")) self.handle_api_get(self.path.removeprefix("/api/").removesuffix("/"))
return return
super().do_GET() super().do_GET()
def do_POST(self): def do_POST(self):
self.path = unquote(self.path) parsed = urlparse(unquote(self.path))
self.query = parse_qs(urlparse(self.path).query) self.path = parsed.path
self.query = parse_qs(parsed.query)
if self.path.startswith("/api/"): if self.path.startswith("/api/"):
self.handle_api_post(self.path.removeprefix("/api/").removesuffix("/")) self.handle_api_post(self.path.removeprefix("/api/").removesuffix("/"))
return return
@ -95,9 +102,14 @@ class HTTPHandler(SimpleHTTPRequestHandler):
def handle_api_get(self, path: str): def handle_api_get(self, path: str):
self.log_message(f"API request at {path}") self.log_message(f"API request at {path}")
if path == "files": if path == "files/to_convert":
files: list[str] = self.get_files_meta() files: list[str] = self.get_to_convert_files_meta(self.query.get("f", [""])[0])
self.send_json(files) self.send_json(files)
elif path == "files/metadata":
files: list[str] = self.get_metadata_files_meta()
self.send_json(files)
elif path.startswith("file"): elif path.startswith("file"):
filename: str = path.split("/", 1)[1] filename: str = path.split("/", 1)[1]
data = self.read_file(filename) data = self.read_file(filename)
@ -126,49 +138,78 @@ class HTTPHandler(SimpleHTTPRequestHandler):
self.end_headers() self.end_headers()
self.wfile.write(json.dumps(data).encode("utf-8")) self.wfile.write(json.dumps(data).encode("utf-8"))
def get_files(self): def get_to_convert_files(self, base_path: str):
return os.listdir(self.DATA_DIR) root_path: str = os.path.abspath(self.TO_CONVERT_DIR)
full_path: str = os.path.join(root_path, base_path)
full_path = os.path.abspath(full_path)
common_prefix: str = os.path.commonprefix([full_path, root_path])
if common_prefix != root_path:
return []
return os.listdir(full_path)
def get_metadata_files(self):
return os.listdir(self.METADATA_DIR)
def read_file(self, filename: str) -> Optional[dict|list]: def read_file(self, filename: str) -> Optional[dict|list]:
if filename not in self.get_files(): if filename not in self.get_metadata_files():
return None return None
with open(os.path.join(self.DATA_DIR, filename), "r") as f: with open(os.path.join(self.METADATA_DIR, filename), "r") as f:
data = json.load(f) data = json.load(f)
return data return data
def write_file(self, filename: str, data: dict|list) -> bool: def write_file(self, filename: str, data: dict|list) -> bool:
if filename not in self.get_files(): if filename not in self.get_metadata_files():
self.send_error(HTTPStatus.NOT_FOUND) self.send_error(HTTPStatus.NOT_FOUND)
return False return False
try: try:
with open(os.path.join(self.DATA_DIR, filename), "w", encoding="utf-8") as f: with open(os.path.join(self.METADATA_DIR, filename), "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False) json.dump(data, f, indent=2, ensure_ascii=False)
except: except:
self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR) self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR)
return False return False
return True return True
def get_files_meta(self): def get_to_convert_files_meta(self, base_path: str):
files: list[str] = self.get_files() files: list[str] = self.get_to_convert_files(base_path)
files = [os.path.join(self.TO_CONVERT_DIR, base_path, f) for f in files]
files_meta: list[dict] = [] files_meta: list[dict] = []
deleted = set(self.CACHE.keys()) - set(files) deleted = set(self.TO_CONVERT_CACHE.keys()) - set(files)
for path in deleted:
del self.TO_CONVERT_CACHE[path]
for path in files:
last_modified: float = os.path.getmtime(path)
if path not in self.TO_CONVERT_CACHE or self.TO_CONVERT_CACHE[path]["ts"] < last_modified:
self.update_to_convert_file_meta(path)
files_meta.append(self.TO_CONVERT_CACHE[path])
return files_meta
def get_metadata_files_meta(self):
files: list[str] = self.get_metadata_files()
files_meta: list[dict] = []
deleted = set(self.METADATA_CACHE.keys()) - set(files)
for filename in deleted: for filename in deleted:
del self.CACHE[deleted] del self.METADATA_CACHE[filename]
for filename in files: for filename in files:
path: str = os.path.join(self.DATA_DIR, filename) path: str = os.path.join(self.METADATA_DIR, filename)
last_modified: float = os.path.getmtime(path) last_modified: float = os.path.getmtime(path)
if filename not in self.CACHE or self.CACHE[filename]["ts"] < last_modified: if filename not in self.METADATA_CACHE or self.METADATA_CACHE[filename]["ts"] < last_modified:
self.update_file_meta(filename) self.update_metadata_file_meta(filename)
files_meta.append(self.CACHE[filename]) files_meta.append(self.METADATA_CACHE[filename])
return files_meta return files_meta
def update_file_meta(self, filename: str): def update_metadata_file_meta(self, filename: str):
path: str = os.path.join(self.DATA_DIR, filename) path: str = os.path.join(self.METADATA_DIR, filename)
meta = { meta = {
"filename": filename, "filename": filename,
@ -185,7 +226,25 @@ class HTTPHandler(SimpleHTTPRequestHandler):
else: else:
meta["title"] = data["title"] meta["title"] = data["title"]
self.CACHE[filename] = meta self.METADATA_CACHE[filename] = meta
def update_to_convert_file_meta(self, path: str):
filename: str = os.path.basename(path)
is_dir: bool = os.path.isdir(path)
meta = {
"path": os.path.relpath(path, self.TO_CONVERT_DIR),
"filename": filename,
"ts": os.path.getmtime(path),
"size": os.path.getsize(path),
"type": "folder" if is_dir else "media"
}
if is_dir:
meta["elements"] = len(os.listdir(path))
if not meta["path"].endswith("/"):
meta["path"] += "/"
self.TO_CONVERT_CACHE[path] = meta
class MeliesServer(FileSystemEventHandler): class MeliesServer(FileSystemEventHandler):