Author SHA1 Message Date
tris 0bf7841345 Adding icons to sync buttons 2026-06-19 22:35:42 +10:00
tris 965573c03b Merged from master 2026-06-19 22:29:44 +10:00
tris b5d8d2d775 Basic web sync working 2026-06-19 22:27:03 +10:00
15 changed files with 268 additions and 445 deletions
+3 -5
View File
@@ -1,10 +1,8 @@
FROM alpine:latest
ARG VERSION=0.8.4
ENV TARGET=/opt/polyphonic
#ENV RELEASE=polyphonic-${VERSION}-py3-none-any.whl
ENV RELEASE=git+https://gitea.tfconsulting.com.au/projects/polyphonic.git@${VERSION}
ENV RELEASE=polyphonic-0.8.4-py3-none-any.whl
#ENV RELEASE=git+https://gitea.tfconsulting.com.au/projects/polyphonic.git
RUN apk add --no-cache python3 py3-pip git ghostscript sqlite
@@ -13,7 +11,7 @@ WORKDIR /root
RUN python3 -m venv ${TARGET}
ENV PATH="${TARGET}/bin:$PATH"
#COPY dist/${RELEASE} .
COPY dist/${RELEASE} .
RUN pip3 install ${RELEASE} --no-cache-dir
RUN pip3 install gunicorn whitenoise
+1 -2
View File
@@ -1,7 +1,7 @@
services:
polyphonic:
image: "polyphonic:0.8.4"
build: .
build: "."
ports:
- "8001:8000"
volumes:
@@ -12,4 +12,3 @@ services:
DJANGO_SETTINGS_MODULE: local_settings
PYTHONPATH: /opt/polyphonic
WORK_DIR: /var/polyphonic
VERSION: 0.8.4
@@ -32,11 +32,10 @@
.menu-label {
color: var(--primary);
}
/*
.button.is-primary, .button.is-primary:hover {
background-color: var(--primary);
}
*/
A.admin-link:after {
content: "*";
@@ -61,4 +60,4 @@ TEXTAREA.input {
padding: 10px 10px;
margin-top: 30px;
border-top-left-radius: 6px;
}
}
@@ -26,4 +26,31 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
});
});
async function background_api(el) {
console.log(el.dataset["api"]);
let text = el.innerHTML;
el.disabled = true;
el.innerHTML = "Running..."
const response = await fetch(el.dataset.api);
console.log(response);
el.innerHTML = text;
if(!response.ok) {
el.disabled = false;
throw new Error(`Response: ${response.status}`);
}
if (el.dataset.success) {
text = el.dataset.success;
}
el.innerHTML = text;
const data = await response.json();
console.log(data);
}
+2 -3
View File
@@ -1,13 +1,12 @@
{% load static %}
<!DOCTYPE html>
<html lang="en" data-theme="light">
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="{% static 'interface/icon.png' %}" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
<!--link rel="stylesheet" href="{% static 'interface/css/bulmaswatch.min.css' %}"></link-->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
<link rel="stylesheet" href="{% static 'interface/css/polyphonic.css' %}"></link>
<script src="{% static 'interface/js/interface.js' %}"></script>
<script src="//unpkg.com/alpinejs" defer></script>
+30 -4
View File
@@ -14,6 +14,13 @@ def sync_work(work: Work):
logger.info("Syncing '%s' from %r", work.name, folder_id)
results = {
"folder_id": folder_id,
"added": [],
"skipped": [],
"missing": [],
}
storage = UserStorage.objects.get(name="gdrive").instance()
existing = set(
@@ -27,6 +34,8 @@ def sync_work(work: Work):
_, files = storage.listdir(folder_id)
logger.debug("Remote files: %r", files)
results["files"] = len(files)
for file in files:
if file.id in existing:
logger.debug("%30s: Skipping existing (%s)", file.name, file.id)
@@ -35,22 +44,29 @@ def sync_work(work: Work):
if not file.name.lower().endswith(".pdf"):
logger.debug("%40s: Not a PDF", file.name)
results["skipped"].append({"file": file.name, "file_id": file.id})
continue
logger.info("%40s: Adding", file.name)
results["added"].append({"file": file.name, "file_id": file.id})
doc = work.docs.create(upload=f"gdrive:{file}", doctype=Document.DOCTYPE_PDF)
doc.auto_tag()
for uri in existing:
results["missing"].append({"uri": uri})
logger.warning("Local entry not in folder: %s", uri)
return results
def sync_partial_collection(collection: Collection, sync_existing: bool = True):
works = Work.objects.filter(collection=collection, meta_info__name="folderid")
result = []
for work in works:
sync_work(work)
result.append(sync_work(work))
return result
def sync_collection(collection: Collection, sync_existing: bool = False):
@@ -69,6 +85,12 @@ def sync_collection(collection: Collection, sync_existing: bool = False):
storage = collection.storage.instance()
folders, _ = storage.listdir(folder)
results = {
"folders": len(folders),
"added": [],
"missing": [],
}
for folder in folders:
if folder[0] == "_":
continue
@@ -84,7 +106,11 @@ def sync_collection(collection: Collection, sync_existing: bool = False):
work = Work(name=folder.name, collection=collection)
work.save()
work.meta_info.create(name="folderid", value=folder.id)
results["added"].append({"work": work.pk, "folder_id": folder.id})
sync_work(work)
for folderid, work in existing:
for folderid, work in existing.items():
results["missing"].append({"work": work, "folder_id": folderid})
logger.warning("Folder for work %d no longer in drive (%s)", work, folderid)
return results
+17 -11
View File
@@ -69,20 +69,26 @@ class GDriveLinkStorage(Storage):
if path == "":
return [], []
folder = self.parse_resource(path)
url = f"{FILES_API}?q='{folder.id}'+in+parents&key={self.api_key}"
data = self.get_json(url, folder)
files = []
folders = []
for x in data["files"]:
if x["mimeType"] == "application/vnd.google-apps.folder":
# folders.append(f"{x['id']}/{x['name']}")
folders.append(DriveObject(x["id"], x.get("resourceKey"), x["name"]))
else:
# files.append(f"{x['id']}/{x['name']}")
files.append(DriveObject(x["id"], x.get("resourceKey"), x["name"]))
return folders, files
folder = self.parse_resource(path)
url = f"{FILES_API}?q='{folder.id}'+in+parents&key={self.api_key}"
data = self.get_json(url, folder)
while True:
for x in data["files"]:
if x["mimeType"] == "application/vnd.google-apps.folder":
folders.append(
DriveObject(x["id"], x.get("resourceKey"), x["name"])
)
else:
files.append(DriveObject(x["id"], x.get("resourceKey"), x["name"]))
token = data.get("nextPageToken")
if token is None:
return folders, files
data = self.get_json(f"{url}&pageToken={token}", folder)
def get_meta(self, name):
file_resource = self.parse_resource(name)
+2 -2
View File
@@ -366,8 +366,8 @@ class Work(models.Model):
def music_tags(self):
tags = dict(self.orchestration.as_list())
# for section in Section.objects.filter(doc__work_id=self.pk):
# tags.setdefault(section.tag, section.name)
for section in Section.objects.filter(doc__work_id=self.pk):
tags.setdefault(section.tag, section.name)
return tags.items()
+2 -3
View File
@@ -4,10 +4,9 @@ import re
GENERAL = """
mvmt Movement
ex Excerpt
sect Section
sec Section
pce Piece
no No.
page Page
no Number
"""
# taken from https://imslp.org/wiki/IMSLP:Abbreviations_for_MusicTags
@@ -14,97 +14,42 @@
{% block media %}
<style>
#unassigned-area {
max-width: 200px;
}
#unassigned-area .tag {
cursor: pointer;
}
#area-left h3 {
margin-top: 10px;
text-align: center;
}
.tag-grid {
display: flex;
margin-bottom: 5px;
overflow-y: auto;
}
.grid-column {
margin-left: 0;
margin-right: 0;
margin-left: 1em;
margin-right: 1em;
}
.grid-page {
height: 25px;
border: none;
border-top: 1px solid #DDD;
margin-bottom: 5px;
border: 1px solid #999;
border-radius: 5px;
text-align: center;
width: 50px;
cursor: pointer;
}
.grid-page:last-child {
border-bottom: 1px solid #DDD;
height: 26px;
}
.grid-page.is-active {
/*background-color: var(--bulma-link-light);*/
color: var(--bulma-link);
background-color: #EEE;
border-radius: 0px 6px 6px 0px;
background-color: var(--primary);
color: white;
}
.grid-tag {
background: #fff;
border: 1px solid #DDD;
border-radius: 0px 4px 4px 0px;
border-left: none;
padding-left: 0.5em;
min-width: 100px;
border: 2px solid var(--primary);
background-color: rgba(240, 240, 240, 0.5);
border-radius: 5px;
padding-left: 1em;
padding-right: 1em;
margin-bottom: 5px;
min-width: 200px;
position: absolute;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: top;
z-index: 1;
margin: 0;
box-shadow: 4px 0 6px -4px rgba(0,0,0,0.12);
align-items: start;
}
.grid-tag:hover {
z-index: 100 !important;
}
.tag-handle {
position: relative;
width: 20px;
background-color: var(--bulma-warning-light);
margin-left: 10px;
cursor: move;
}
.tag-name {
flex: 1;
text-align: right;
}
.tag-handle-resize {
position: absolute;
height: 10px;
width: 10px;
bottom: 0px;
right: 0px;
border-radius: 3px 0px 0px 0px;
background-color: var(--bulma-warning);
cursor: ns-resize;
}
{% for tag, colour in tag_colours %}
.tag-type-{{ tag }} .tag-handle {
background-color: var(--bulma-{{ colour }}-light);
}
.tag-type-{{ tag }} .tag-handle-resize {
background-color: var(--bulma-{{ colour }});
}
{% endfor %}
#tag-area {
min-width: 220px;
position: relative;
font-size: 9pt;
}
.instrument {
display: list-item !important;
@@ -119,34 +64,32 @@
{% block page %}
<h3 class="subtitle"><a href="{% url 'work_detail' collection.pk document.work_id %}">{{ document.work.name }}</a></h3>
<div id="annotation-area" class="columns is-centered">
<div class="column is-narrow" id="area-left">
<h3>Page control</h3>
<div class="columns">
<div class="column is-narrow">
<button class="button is-small is-primary" id="prev">&lt;</button>
</div>
<div class="column has-text-centered">
<span id="page-num">-</span> / <span id="page-count">-</span>
</div>
<div class="column is-narrow">
<button class="button is-small is-primary" id="next">&gt;</button>
</div>
<div class="column is-narrow">
<div class="columns">
<div class="column is-narrow">
<button class="button is-small is-primary" id="prev">&lt;</button>
</div>
<h3>Quick Tags</h3>
<ul id="unassigned-area">
{% for tag, inst in document.work.music_tags %}
<li class="tag is-warning" onclick="assignTag('{{tag}}', this)">{{ inst }}</li>
{% endfor %}
</ul>
<h3>Actions</h3>
<div>
<a class="button is-small is-primary" onclick="showAddModal()">New Tag</a>
<button class="button is-small is-primary" onclick="expandEntries()">Expand Tags</button>
<div class="column">
<span id="page-num">-</span> / <span id="page-count">-</span>
</div>
<div class="column is-narrow">
<button class="button is-small is-primary" id="next">&gt;</button>
</div>
</div>
<ul id="unassigned-area">
{% for tag, inst in document.work.music_tags %}
<li>
<span class="is-clickable" onclick="assignInstrument('{{tag}}', this)")>{{ inst }}</span>
&nbsp;
<span class="is-clickable" onclick="addNumberedInstrument('{{tag}}', this)">...</span>
</li>
{% endfor %}
</ul>
<a onclick="document.getElementById('add-modal').classList.add('is-active')">Add Tag</a>
</div>
<div class="column is-narrow">
<div class="has-text-centered">
<div class="box" style="display: inline-block; padding: 0px;">
<div class="box" style="display: inline-block;">
<canvas id="inline-viewer" style="width: 500px;"></canvas>
</div>
</div>
@@ -159,6 +102,7 @@
<div class="grid-column" id="tag-area">
</div>
</div>
<button class="button is-primary" style="width: 100%" onclick="expandEntries()">Expand</button>
</div>
</div>
@@ -167,46 +111,24 @@
<div class="modal-background" onclick="closeAddModal()"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Add Tag</p>
<p class="modal-card-title">Add Instrument</p>
<button class="delete" aria-label="close" onclick="closeAddModal()"></button>
</header>
<section class="modal-card-body">
<div class="field has-addons is-justify-content-center">
<div class="field has-addons">
<span class="control">
<span class="select">
<select onChange="changeTagType()" id="tag-type-selection">
<option value="inst">Instrument</option>
<option value="mvmt">Movement</option>
<option value="sect">Section</option>
<option value="no">Number</option>
<option value="page">Page</option>
</select>
</span>
</span>
<span class="control add-tag-option enable-page">
<span class="select">
<select id="add-tag-page">
<option>Left</option>
<option>Right</option>
</select>
</span>
</span>
<span class="control add-tag-option enable-sect">
<input type="text" class="input" id="add-tag-name"/>
</span>
<span class="control add-tag-option enable-inst">
<input type="text" class="input" list="instrument-list" id="add-tag-inst"/>
<input type="text" class="input" list="instrument-list" id="add-instrument-name"/>
<datalist id="instrument-list">
{% for inst in json_data.instruments.values %}
<option value="{{inst}}"/>
{% endfor %}
</datalist>
</span>
<span class="control add-tag-option enable-inst enable-mvmt enable-no">
<input type="number" class="input" min="1" size="3" id="add-tag-ordinal"/>
<span class="control">
<input type="number" class="input" max="4" min="1" size="3" id="add-instrument-variant"/>
</span>
<span class="control">
<button class="button is-primary" onclick="createNewTag();">Add</button>
<button class="button is-primary" onclick="addInstrument();">Add</button>
</span>
</div>
</section>
@@ -215,15 +137,6 @@
<p>{{ document.upload.name }}</p>
<template id="blank-tag">
<div class="grid-tag">
<span class="tag-name"></span>
<div class="tag-handle">
<div class="tag-handle-resize"></div>
</div>
</div>
</template>
{% endblock %}
{% block scripts %}
@@ -253,11 +166,7 @@
pageNumPending = null,
scale = 1,
canvas = document.getElementById('inline-viewer'),
ctx = canvas.getContext('2d'),
draggedTag = null,
dragMode = null,
dragOrigStart = null,
dragOrigEnd = null;
ctx = canvas.getContext('2d');
/**
* Get page info from document, resize canvas accordingly, and render page.
@@ -342,7 +251,6 @@
pdfDoc = pdfDoc_;
const pageList = document.getElementById('page-list');
const tagGrid = document.querySelector('.tag-grid');
pageList.innerHTML = '';
for (var i=0; i<pdfDoc.numPages; i++) {
let page = i+1;
@@ -356,47 +264,10 @@
pageList.appendChild(el);
}
tagGrid.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (draggedTag) {
const rect = pageList.getBoundingClientRect();
const y = e.clientY - rect.top;
const page = Math.min(Math.max(Math.floor(y / 25) + 1, 1), pdfDoc.numPages);
if (dragMode === 'resize') {
if (page >= parseInt(draggedTag.dataset.start) && page !== parseInt(draggedTag.dataset.end)) {
draggedTag.dataset.end = page;
updateTag(draggedTag);
resolveOverlaps();
}
} else {
const length = dragOrigEnd - dragOrigStart;
if (page !== parseInt(draggedTag.dataset.start)) {
draggedTag.dataset.start = page;
draggedTag.dataset.end = Math.min(page + length, pdfDoc.numPages);
updateTag(draggedTag);
resolveOverlaps();
}
}
}
});
tagGrid.addEventListener('drop', (e) => {
e.preventDefault();
if (draggedTag) {
draggedTag = null;
dragMode = null;
}
resolveOverlaps();
});
document.getElementById('page-count').textContent = pdfDoc.numPages;
// Initial/first page rendering
renderPage(pageNum);
setTimeout(() => {
const previewBox = document.querySelector('.box');
tagGrid.style.maxHeight = previewBox.offsetHeight + 'px';
}, 100);
tagArea.innerHTML = '';
for (let pageTag of data.pageTags) {
@@ -405,13 +276,10 @@
dirty = false;
});
function showAddModal() {
document.getElementById("add-modal").classList.add("is-active");
changeTagType();
}
function closeAddModal() {
document.getElementById('add-modal').classList.remove('is-active');
document.getElementById('add-instrument-name').value = "";
document.getElementById('add-instrument-variant').value = "";
}
function addNumberedInstrument(tag, e) {
@@ -424,202 +292,97 @@
modal.classList.add('is-active');
}
function changeTagType() {
const tag_select = document.querySelector("#tag-type-selection");
const selected = tag_select.value;
document.querySelectorAll(".add-tag-option").forEach((x) => {x.classList.add("is-hidden")});
document.querySelectorAll(".add-tag-option input").forEach((x) => {x.value=""});
document.querySelectorAll(".enable-" + selected).forEach((x) => {x.classList.remove("is-hidden")});
tag_select.focus();
}
function addInstrument() {
let name = document.getElementById('add-instrument-name');
let variant = document.getElementById('add-instrument-variant');
function generateNewTag() {
const selected = document.querySelector("#tag-type-selection").value;
const n = document.querySelector("#add-tag-ordinal").value;
switch (selected) {
case "mvmt":
case "no":
return selected + "-" + n;
case "sect":
return "sect-" + document.querySelector("#add-tag-name").value;
case "inst":
const inst_name = document.querySelector("#add-tag-inst").value;
var tag = null;
for (let key in data.instruments) {
if (data.instruments[key] == inst_name) {
tag = key;
break;
}
}
if (!tag) {
tag = inst_name;
}
if (n) {
tag += "-" + n;
}
return tag;
case "page":
return "page-" + document.querySelector("#add-tag-page").value;
let inst_name = name.value;
var tag = null;
for (let key in data.instruments) {
if (data.instruments[key] == inst_name) {
tag = key;
break;
}
}
}
if (!tag) {
alert("Unknown tag: " + name);
return;
}
function createNewTag() {
if (variant.value) {
tag += "-" + variant.value;
}
const tag = generateNewTag()
variant.value = '';
name.value = '';
let li = document.createElement('li');
li.classList.add("is-clickable");
li.addEventListener('click', () => assignInstrument(tag, li));
li.innerHTML = get_instrument(tag);
document.getElementById('unassigned-area').appendChild(li);
addTag(tag, pageNum, pageNum);
closeAddModal();
}
function assignTag(tag, el) {
function assignInstrument(tag, el) {
addTag(tag, pageNum, pageNum);
//el.remove();
}
function addTag(tag, start, end) {
//console.log("addTag", tag, start, end);
const fragment = document.importNode(document.querySelector("#blank-tag").content, true);
const el = fragment.querySelector("div");
const tagName = el.querySelector(".tag-name");
tagName.innerHTML=get_tag_name(tag);
//el.className = 'grid-tag';
console.log("addTag", tag, start, end);
const el = document.createElement('div');
el.className = 'grid-tag';
el.dataset.tag = tag;
el.dataset.start = start;
el.dataset.end = end;
el.dataset.tag = tag;
const parts = tag.split("-")
el.classList.add("tag-type-" + parts[0]);
const handle = el.querySelector('.tag-handle');
handle.draggable = true;
handle.addEventListener('dragstart', (e) => {
draggedTag = el;
dragMode = 'move';
dragOrigStart = parseInt(el.dataset.start);
dragOrigEnd = parseInt(el.dataset.end);
el.style.zIndex = 1000;
e.dataTransfer.effectAllowed = 'move';
const blankImg = new Image();
blankImg.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
e.dataTransfer.setDragImage(blankImg, 0, 0);
});
handle.addEventListener('dragend', () => {
if (draggedTag === el && dragMode === 'move') {
el.dataset.start = dragOrigStart;
el.dataset.end = dragOrigEnd;
updateTag(el);
el.remove();
dirty = true;
} else if (draggedTag === el) {
el.dataset.start = dragOrigStart;
el.dataset.end = dragOrigEnd;
updateTag(el);
el.style.zIndex = '';
}
resolveOverlaps();
draggedTag = null;
dragMode = null;
});
const resize = el.querySelector('.tag-handle-resize');
resize.draggable = true;
resize.addEventListener('dragstart', (e) => {
draggedTag = el;
dragMode = 'resize';
dragOrigEnd = parseInt(el.dataset.end);
el.style.zIndex = 1000;
e.dataTransfer.effectAllowed = 'move';
const blankImg2 = new Image();
blankImg2.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
e.dataTransfer.setDragImage(blankImg2, 0, 0);
e.stopPropagation();
});
resize.addEventListener('dragend', () => {
if (draggedTag === el) {
el.dataset.end = dragOrigEnd;
updateTag(el);
el.style.zIndex = '';
}
resolveOverlaps();
draggedTag = null;
dragMode = null;
});
let setStart = document.createElement('span');
setStart.className = "icon is-action";
setStart.innerHTML = '<span class="material-symbols-outlined" title="Set start page">vertical_align_top</span>';
setStart.addEventListener('click', () => setTagStart(el));
el.appendChild(setStart);
let label = document.createElement('span');
let name = document.createElement('b');
name.innerHTML = get_instrument(tag);
label.appendChild(name);
el.appendChild(label);
let del = document.createElement('span');
del.className = "icon is-action";
del.innerHTML = '<span class="material-symbols-outlined" title="Remove this tag">delete</span>';
del.addEventListener('click', () => {
el.remove();
dirty=true;
});
el.appendChild(del)
let setEnd = document.createElement('span');
setEnd.className = "icon is-action";
setEnd.innerHTML = '<span class="material-symbols-outlined" title="Set end page">vertical_align_bottom</span>';
setEnd.addEventListener('click', () => setTagEnd(el));
el.appendChild(setEnd);
updateTag(el);
tagArea.appendChild(el);
resolveOverlaps();
}
function updateTag(tag) {
let start = tag.dataset.start;
let end = tag.dataset.end;
let span = end-start+1;
let height = span * 25 + 1;
let top = (start-1) * 25;
let height = span * 25 + (span-1) * 5;
let top = (start-1) * 30;
tag.style.height = height + 'px';
tag.style.marginTop = top + 'px';
dirty = true;
const name = tag.querySelector('.tag-name');
const handle = tag.querySelector('.tag-handle');
if (name) {
if (span >= 3) {
name.style.position = 'absolute';
name.style.top = '4px';
name.style.right = '20px';
name.style.writingMode = 'vertical-rl';
tag.style.justifyContent = 'flex-end';
if (handle) handle.style.marginLeft = 'auto';
} else {
name.style.position = '';
name.style.top = '';
name.style.right = '';
name.style.writingMode = '';
tag.style.justifyContent = '';
if (handle) handle.style.marginLeft = '';
}
}
}
function resolveOverlaps() {
const tags = Array.from(tagArea.children);
tags.sort((a, b) => parseInt(a.dataset.start) - parseInt(b.dataset.start));
const groups = [];
let current = [tags[0]];
for (let i = 1; i < tags.length; i++) {
const prevEnd = parseInt(tags[i-1].dataset.end);
const currStart = parseInt(tags[i].dataset.start);
if (currStart <= prevEnd) {
current.push(tags[i]);
} else {
groups.push(current);
current = [tags[i]];
}
}
if (current.length) groups.push(current);
for (let group of groups) {
group.sort((a, b) => {
const spanA = parseInt(a.dataset.end) - parseInt(a.dataset.start);
const spanB = parseInt(b.dataset.end) - parseInt(b.dataset.start);
return spanB - spanA;
});
const n = group.length;
group.forEach((tag, i) => {
tag.style.minWidth = (120 + (n - 1 - i) * 70) + 'px';
tag.style.zIndex = i;
});
}
}
function setTagStart(el) {
@@ -641,10 +404,6 @@
}
function expandEntries() {
const answer = confirm("This will expand all tags to fill up available pages");
if (!answer) return;
const entries = Array.from(tagArea.children);
entries.sort((a, b) => a.dataset.start-b.dataset.start);
const c = entries.length;
@@ -657,33 +416,6 @@
updateTag(entries[c-1]);
}
const body = document.querySelector("body");
function keymap(e) {
if (e.target != body) return;
switch(e.key) {
case "ArrowRight":
case "l":
case "ArrowDown":
case "j":
onNextPage();
break;
case "ArrowLeft":
case "h":
case "ArrowUp":
case "k":
onPrevPage();
break;
case "n":
showAddModal();
break;
default:
return;
}
e.preventDefault();
}
body.addEventListener("keydown", keymap)
function saveTags() {
const pageTags = [];
for (let pageTag of tagArea.children ) {
@@ -712,9 +444,9 @@
dirty = false;
}
function get_tag_name(s) {
function get_instrument(s) {
let parts = s.split('-');
let instrument = data.instruments[parts[0]] || parts[0];
let instrument = data.instruments[parts[0]];
if (parts.length == 2) {
return instrument + " " + parts[1];
}
@@ -2,10 +2,20 @@
{% load polyphonic %}
{% block admin %}
{% if meta.folderid %}
<button class="button is-link" data-api="{% url 'work_sync' work=object.pk %}" data-success="Synced" onclick="background_api(this)">
{% icon "sync" %}
<span>Sync with Drive</span>
</button>
{% endif %}
<a href="{% url 'work_detail' collection=collection.pk pk=object.pk %}" class="button is-link is-light">
{% icon "backspace" %}
<span>Back to work</span>
</a>
{% endblock %}
{% block page %}
@@ -20,8 +30,8 @@
<div class="m-3">
<p>
{% if meta.folderid %}
This work is currently linked to <b>{{ meta.folderid }}</b>.<br/>
Pasting a new folder link will overwrite this.
This work is currently linked to folder <span class="tag is-success">{{ meta.folderid }}</span><br/>
<em>Pasting a new folder link will overwrite this.</em>
{% else %}
There is currently no shared drive folder linked to this work - paste one here to enable syncing.
{% endif %}
@@ -42,4 +52,6 @@ There is currently no shared drive folder linked to this work - paste one here t
{% csrf_token %}
</form>
</div>
{% endblock %}
@@ -3,12 +3,21 @@
{% load polyphonic %}
{% block admin %}
{% if collection %}
<button class="button is-link" data-api="{% url 'collection_sync' collection=collection.pk %}" data-success="Synced" onclick="background_api(this)">
{% icon "sync" %}
<span>Sync Collection</span>
</button>
<a href="{% url 'work_add' collection.pk %}" class="button is-link">
{% icon "add_notes" %}
<span>Add a work</span>
</a>
{% endif %}
{% endblock %}
{% block page %}
@@ -30,7 +39,7 @@
<div class="has-text-centered">
<a href="?">_</a>
{% for letter in letters %}
<a href="?start={{ letter }}&sort={{ sort }}">{{ letter }}</a>&nbsp;
<a href="?start={{ letter }}">{{ letter }}</a>&nbsp;
{% endfor %}
</div>
{% endif %}
@@ -38,10 +47,10 @@
<table class="table is-striped is-fullwidth">
<thead>
<tr>
<th>Work <span class="icon-text"><a href="?start={{ start }}&sort=name">{{ "list_arrow"|icon }}</a></span></th>
<th>Composer <span class="icon-text"><a href="?start={{ start }}&sort=composer">{{ "list_arrow"|icon }}</a></span></th>
<th class="is-hidden-mobile">Edition</th>
{% if not collection %}<th class="is-hidden-touch">Collection</th>{% endif %}
<th>Work</th>
<th>Composer</th>
<th class="is-hidden-mobile">Edition</th>
{% if not collection %}<th class="is-hidden-touch">Collection</th>{% endif %}
</tr>
</thead>
<tbody>
+10
View File
@@ -126,4 +126,14 @@ urlpatterns = [
api.CollectionImportView.as_view(),
name="collection_import",
),
path(
"api/collections/<int:collection>/sync",
api.CollectionSyncView.as_view(),
name="collection_sync",
),
path(
"api/works/<int:work>/sync",
api.WorkSyncView.as_view(),
name="work_sync",
),
]
+10 -28
View File
@@ -35,6 +35,7 @@ class ProjectItemListView(ProjectMixin, ListView):
model = models.ProjectItem
def post(self, request: HttpRequest, **kwargs):
project_works = self.project.works.all()
print(request.POST)
@@ -199,8 +200,13 @@ class WorkListView(CollectionMixin, TemplateView):
data["meta"] = qs.meta
# data["page_range"] = data["page_obj"]["paginator"]
else:
qs, ctx = self.get_filtered_queryset()
data.update(ctx)
qs = self.get_queryset()
start = self.request.GET.get("start")
if start:
start = start.upper()
qs = qs.filter(name__gte=start, name__lt=start + "~")
data["start"] = start
data["letters"] = string.ascii_uppercase
@@ -217,27 +223,9 @@ class WorkListView(CollectionMixin, TemplateView):
def get_collections(self):
raise NotImplementedError
def get_filtered_queryset(self):
def get_queryset(self):
works = self.get_works()
ctx = {}
sort = self.request.GET.get("sort", "name").lower()
order = ["name", "composer", "edition"]
if sort not in order:
raise KeyError(f"Cannot sort by {sort}")
order.remove(sort)
order = [sort] + order + ["pk"]
ctx["sort"] = sort
start = self.request.GET.get("start")
if start:
start = start.upper()
filters = {f"{sort}__gte": start, f"{sort}__lt": start + "~"}
works = works.filter(**filters)
ctx["start"] = start
return works.order_by(*order).distinct(), ctx
return works.order_by("name", "composer", "edition", "pk").distinct()
def get_results(self, query, page):
try:
@@ -609,12 +597,6 @@ class DocumentAnnotateView(DocumentMixin, DetailView):
collection=data["collection"].pk,
pk=data["document"].pk,
)
data["tag_colours"] = [
("page", "danger"),
("no", "link"),
("mvmt", "link"),
("sect", "success"),
]
data["json_data"] = {"pageTags": pages, "instruments": dict(MUSIC_TAGS)}
return data
+25
View File
@@ -3,9 +3,12 @@ from polyphonic.interface.views import AuthorizedResourceMixin
from rest_framework import serializers
from rest_framework.exceptions import APIException
from rest_framework import generics
from rest_framework.views import APIView
from rest_framework.response import Response
from polyphonic.library.models import Collection, Work, Document, Section, WorkMeta
from polyphonic.library.gdrive import sync_collection, sync_work
import requests
import urllib
@@ -208,3 +211,25 @@ class CollectionImportView(AuthorizedResourceMixin, generics.CreateAPIView):
def perform_create(self, serializer):
serializer.save(collection_id=self.kwargs["pk"])
class WorkSyncView(AuthorizedResourceMixin, APIView):
admin_required = True
def get(self, request, work, format=None):
obj = Work.objects.get(pk=work)
result = sync_work(obj)
return Response(result)
class CollectionSyncView(AuthorizedResourceMixin, APIView):
admin_required = True
def get(self, request, collection, format=None):
obj = Collection.objects.get(pk=collection)
result = sync_collection(obj)
return Response(result)