Add split function

- Add ServerWebsocketPacketQueueSplit
- Add Split method to Queue
- Correct automatic separator insertion for newly ingested documents
- Rework UI so that entries have their own set of buttons
- Refactor occurrences of "document" to "queueEntry"
This commit is contained in:
David Vogel 2025-05-19 12:03:40 +02:00
parent 1bd8be6abf
commit b5ecf95a7b
9 changed files with 120 additions and 19 deletions

View File

@ -58,7 +58,12 @@ func main() {
continue
}
server.Documents.Lock()
var entries []QueueEntry
if len(server.Documents.Documents) > 0 {
entries = append(entries, QueueEntry{ID: NewQueueEntryID(), QueueEntryData: QueueEntryDataSeparator{}})
}
for _, page := range docPages {
entries = append(entries, QueueEntry{
ID: NewQueueEntryID(),
@ -66,9 +71,7 @@ func main() {
QueueEntryData: QueueEntryDataPage{Page: &page},
})
}
entries = append(entries, QueueEntry{ID: NewQueueEntryID(), QueueEntryData: QueueEntryDataSeparator{}})
server.Documents.Lock()
server.Documents.Append(entries...)
server.Documents.Unlock()
}

View File

@ -171,15 +171,38 @@ func (d *Queue) Shift(offset int, ids ...QueueEntryID) {
}
}
// Split will add a separator behind every queue entry with the given IDs.
//
// The Queue must be locked when calling this.
func (d *Queue) Split(ids ...QueueEntryID) {
for _, id := range ids {
if i := slices.IndexFunc(d.Documents, func(e QueueEntry) bool { return e.ID == id }); i >= 0 && i < len(d.Documents)-1 {
doc := &d.Documents[i]
switch d.Documents[i+1].QueueEntryData.(type) {
case QueueEntryDataPage:
default:
// If the next entry is not a page, we will not put a separator here.
continue
}
switch doc.QueueEntryData.(type) {
case QueueEntryDataPage:
d.InsertAt(i+1, QueueEntry{
ID: NewQueueEntryID(),
QueueEntryData: QueueEntryDataSeparator{},
})
}
}
}
}
// QueueEntryByID returns the QueueEntry with the given ID.
//
// The Queue must be locked when calling this.
func (d *Queue) QueueEntryByID(id QueueEntryID) *QueueEntry {
for i := range d.Documents {
document := &d.Documents[i]
if document.ID == id {
return document
}
if i := slices.IndexFunc(d.Documents, func(e QueueEntry) bool { return e.ID == id }); i >= 0 {
return &d.Documents[i]
}
return nil

View File

@ -51,7 +51,7 @@ func init() { ServerWebsocketPacketRegister(&ServerWebsocketPacketQueueShiftAt{}
// ServerWebsocketPacketQueueShift represents a shift operation on a document queue list.
type ServerWebsocketPacketQueueShift struct {
IDs []QueueEntryID `json:"ids"` // IDs of the documents.
IDs []QueueEntryID `json:"ids"` // IDs of the entries.
Offset int `json:"offset"` // Shift offset.
}
@ -59,8 +59,8 @@ func (s *ServerWebsocketPacketQueueShift) Type() string { return "QueueShift" }
func init() { ServerWebsocketPacketRegister(&ServerWebsocketPacketQueueShift{}) }
// ServerWebsocketPacketQueueUpdate represents an update operation of documents in a queue list.
// The receiver should update any of the received documents in their local queue list.
// ServerWebsocketPacketQueueUpdate represents an update operation of entries in a queue list.
// The receiver should update any of the received entries in their local queue list.
type ServerWebsocketPacketQueueUpdate struct {
Documents []QueueEntry `json:"documents"`
}
@ -68,3 +68,13 @@ type ServerWebsocketPacketQueueUpdate struct {
func (s *ServerWebsocketPacketQueueUpdate) Type() string { return "QueueUpdate" }
func init() { ServerWebsocketPacketRegister(&ServerWebsocketPacketQueueUpdate{}) }
// ServerWebsocketPacketQueueSplit represents a "add separator" operation in a queue list.
// The split is added behind the given entry IDs.
type ServerWebsocketPacketQueueSplit struct {
IDs []QueueEntryID `json:"ids"` // IDs of the entries.
}
func (s *ServerWebsocketPacketQueueSplit) Type() string { return "QueueSplit" }
func init() { ServerWebsocketPacketRegister(&ServerWebsocketPacketQueueSplit{}) }

View File

@ -92,6 +92,10 @@ func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
s.Documents.Lock()
s.Documents.Shift(packet.Offset, packet.IDs...)
s.Documents.Unlock()
case *ServerWebsocketPacketQueueSplit:
s.Documents.Lock()
s.Documents.Split(packet.IDs...)
s.Documents.Unlock()
default:
log.Printf("Websocket client %q sent unsupported packet type %T.", conn.LocalAddr(), prototype)
return

View File

@ -96,6 +96,20 @@ export class API extends EventTarget {
this.#socket.send(JSON.stringify(message));
}
/**
* Sends a document queue split request to the server.
* @param {...number} ids Document ids.
*/
queueSplit(...ids) {
if (this.#socket.readyState !== WebSocket.OPEN) {
return
}
/** @type {{type: string, payload: import("./model").APIPacketQueueSplit}} */
const message = { type: "QueueSplit", payload: { ids: ids } };
this.#socket.send(JSON.stringify(message));
}
/**
*
* @param {"GET"|"POST"|"DELETE"|"UPDATE"} method

View File

@ -14,20 +14,46 @@ export class DocumentQueueEntryPage extends LitElement {
/** @type {API} */
this.api;
/** @type {import('model').APIQueueEntry} */
this.document;
this.queueEntry;
}
static styles = css`
:host {
width: 100%;
display: flex;
justify-content: space-between;
gap: 8px;
}
img {
width: 128px;
}
#buttons {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 8px;
}
button {
padding: 8px;
}
`;
// @ts-ignore
render() {
return html`
<img id="image" src=${`/api/queue-entry-page/${this.document.id}/preview`}></img>
<img id="image" src=${`/api/queue-entry-page/${this.queueEntry.id}/preview`}></img>
<div style="flex-grow: 1;">
<span>This is a document</span>
</div>
<div id="buttons">
<button @click=${e => this.api.queueShift(-1, this.queueEntry.id)}></button>
<button @click=${e => this.api.queueDelete(this.queueEntry.id)}>Delete</button>
<button @click=${e => this.api.queueSplit(this.queueEntry.id)}>Split</button>
<button @click=${e => this.api.queueShift(1, this.queueEntry.id)}></button>
</div>
`;
}
}

View File

@ -14,20 +14,38 @@ export class DocumentQueueEntrySeparator extends LitElement {
/** @type {API} */
this.api;
/** @type {import('model').APIQueueEntry} */
this.document;
this.queueEntry;
}
static styles = css`
:host {
width: 100%;
background: black;
display: flex;
justify-content: space-between;
gap: 8px;
}
#buttons {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 8px;
}
button {
padding: 8px;
}
`;
// @ts-ignore
render() {
return html`
<div style="flex-grow: 1; align-self: center;">
<hr>
</div>
<div id="buttons">
<button @click=${e => this.api.queueDelete(this.queueEntry.id)}>Delete</button>
</div>
`;
}
}

View File

@ -61,9 +61,9 @@ export class DocumentQueueEntry extends LitElement {
let embeddedElement;
switch (this.queueEntry.type) {
case "Page":
embeddedElement = html`<document-queue-entry-page .document=${this.queueEntry} .api=${this.api}></document-queue-entry-page>`; break;
embeddedElement = html`<document-queue-entry-page .queueEntry=${this.queueEntry} .api=${this.api}></document-queue-entry-page>`; break;
case "Separator":
embeddedElement = html`<document-queue-entry-separator .document=${this.queueEntry} .api=${this.api}></document-queue-entry-separator>`; break;
embeddedElement = html`<document-queue-entry-separator .queueEntry=${this.queueEntry} .api=${this.api}></document-queue-entry-separator>`; break;
default:
embeddedElement = html`<span>Unsupported entry type!</span>`
}
@ -71,7 +71,6 @@ export class DocumentQueueEntry extends LitElement {
return html`
<div id="left-bar">
<input id="checkbox-selected" type="checkbox" .checked=${this.selected} @change=${this.onCheckboxChange}></input>
<button id="button-swap" @click=${e => this.api.queueShift(1, this.queueEntry.id)}></button>
</div>
${embeddedElement}

View File

@ -33,6 +33,10 @@ export type APIPacketQueueShift = {
offset: number;
}
export type APIPacketQueueSplit = {
ids: number[];
}
export type APIEvents = {
queuedeleteat: CustomEvent<APIPacketQueueDeleteAt>;
queueinsertat: CustomEvent<APIPacketQueueInsertAt>;