Compare commits
1 Commits
main
...
test/ipub-
| Author | SHA1 | Date | |
|---|---|---|---|
|
e1734a5310
|
@@ -132,7 +132,9 @@ class IPUBBody extends IPUBElement {
|
|||||||
IPUBImage,
|
IPUBImage,
|
||||||
IPUBInteraction,
|
IPUBInteraction,
|
||||||
IPUBSoundtrack,
|
IPUBSoundtrack,
|
||||||
IPUBTrigger,
|
IPUBTrack,
|
||||||
|
IPUBTrackItem,
|
||||||
|
IPUBTrackItemPosition,
|
||||||
]) {
|
]) {
|
||||||
console.info(`IPUBBody: Defining custom element <${e.elementName}>`);
|
console.info(`IPUBBody: Defining custom element <${e.elementName}>`);
|
||||||
globalThis.customElements.define(e.elementName, e);
|
globalThis.customElements.define(e.elementName, e);
|
||||||
@@ -210,6 +212,18 @@ class IPUBCover extends IPUBElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class IPUBTrack extends IPUBElement {
|
||||||
|
static elementName = "ipub-track";
|
||||||
|
}
|
||||||
|
|
||||||
|
class IPUBTrackItem extends IPUBElement {
|
||||||
|
static elementName = `ipub-track-item`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class IPUBTrackItemPosition extends IPUBElement {
|
||||||
|
static elementName = `ipub-track-item-position`;
|
||||||
|
}
|
||||||
|
|
||||||
class IPUBAudio extends IPUBElement {
|
class IPUBAudio extends IPUBElement {
|
||||||
static elementName = "ipub-audio";
|
static elementName = "ipub-audio";
|
||||||
|
|
||||||
@@ -283,7 +297,6 @@ class IPUBAudio extends IPUBElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.#isFading) {
|
if (this.#isFading) {
|
||||||
// TODO: Be able to force fading to be canceled
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,7 +568,7 @@ class IPUBSoundtrack extends IPUBElement {
|
|||||||
`IPUBSoundtrack: error while trying to play audio, error: ${e}`,
|
`IPUBSoundtrack: error while trying to play audio, error: ${e}`,
|
||||||
{
|
{
|
||||||
error: e,
|
error: e,
|
||||||
audio: last,
|
audio: audio,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -565,32 +578,23 @@ class IPUBSoundtrack extends IPUBElement {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
static #observer = (() => {
|
static #observer = (() => {
|
||||||
return new IntersectionObserver(
|
return new IntersectionObserver((entries) => {
|
||||||
(entries) => {
|
for (const { intersectionRatio, target, time } of entries) {
|
||||||
for (const { intersectionRatio, target, time } of entries) {
|
/** @type {IPUBSoundtrack} */
|
||||||
/** @type {IPUBSoundtrack} */
|
const soundtrack = target;
|
||||||
const soundtrack =
|
|
||||||
target.tagName === IPUBTrigger.elementName
|
|
||||||
? getAncestor(target, IPUBSoundtrack.elementName)
|
|
||||||
: target;
|
|
||||||
|
|
||||||
if (intersectionRatio === 1) {
|
if (intersectionRatio > 0) {
|
||||||
console.debug(
|
console.debug(`${soundtrack.id} is on screen at ${time}`, soundtrack);
|
||||||
`${soundtrack.id} is on screen at ${time}`,
|
this.#onScreenStack.add(soundtrack);
|
||||||
soundtrack,
|
} else {
|
||||||
);
|
console.debug(
|
||||||
this.#onScreenStack.add(soundtrack);
|
`${soundtrack.id} is not on screen ${time}`,
|
||||||
} else {
|
soundtrack,
|
||||||
console.debug(
|
);
|
||||||
`${soundtrack.id} is not on screen ${time}`,
|
this.#onScreenStack.delete(soundtrack);
|
||||||
soundtrack,
|
|
||||||
);
|
|
||||||
this.#onScreenStack.delete(soundtrack);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
{ threshold: 1 },
|
});
|
||||||
);
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -648,142 +652,10 @@ class IPUBSoundtrack extends IPUBElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const trigger = this.querySelector(IPUBTrigger.elementName);
|
IPUBSoundtrack.#observer.observe(this);
|
||||||
if (trigger) {
|
|
||||||
IPUBSoundtrack.#observer.observe(trigger);
|
|
||||||
} else {
|
|
||||||
IPUBSoundtrack.#observer.observe(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(guz013): Handle if element is moved, it's group should be updated
|
// TODO(guz013): Handle if element is moved, it's group should be updated
|
||||||
// TODO(guz013): Handle if element is deleted/disconnected, it should be removed from observer
|
|
||||||
}
|
|
||||||
|
|
||||||
class IPUBTrigger extends IPUBElement {
|
|
||||||
static elementName = "ipub-trigger";
|
|
||||||
static observedAttributes = ["height", "width"].concat(
|
|
||||||
IPUBElement.observedAttributes,
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: Make this observer global
|
|
||||||
/** @private */
|
|
||||||
static #resizeObserver = new ResizeObserver((bodies) => {
|
|
||||||
for (const { target: body, contentRect } of bodies) {
|
|
||||||
const height = Math.max(body.scrollHeight, contentRect.height);
|
|
||||||
const width = Math.max(body.scrollWidth, contentRect.width);
|
|
||||||
|
|
||||||
for (const trigger of IPUBTrigger.#resizableTriggers.get(body)) {
|
|
||||||
const percH = trigger.getAttribute("height");
|
|
||||||
if (percH) {
|
|
||||||
trigger.style.setProperty(
|
|
||||||
"--ipub-height",
|
|
||||||
`${Math.round((height / 100) * Number.parseFloat(percH))}px`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const percW = trigger.getAttribute("width");
|
|
||||||
if (percW) {
|
|
||||||
trigger.style.setProperty(
|
|
||||||
"--ipub-width",
|
|
||||||
`${Math.round((width / 100) * Number.parseFloat(percW))}px`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* @type {Map<IPUBBody, Set<IPUBTrigger>>}
|
|
||||||
*/
|
|
||||||
static #resizableTriggers = new Map();
|
|
||||||
|
|
||||||
// FIXME: trigger can be the same size as viewport, cap it to 80% of viewport
|
|
||||||
// height and 100% of viewport width
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
|
|
||||||
const body = getAncestor(this, "ipub-body");
|
|
||||||
if (!body) {
|
|
||||||
console.error("IPUBTrigger: element must be a descendant of ipub-body");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug(
|
|
||||||
`IPUBTrigger#${this.id}: adding ${IPUBBody.elementName}#${body.id} from resize observer`,
|
|
||||||
);
|
|
||||||
IPUBTrigger.#resizeObserver.observe(body);
|
|
||||||
if (this.getAttribute("height") || this.getAttribute("width")) {
|
|
||||||
IPUBTrigger.#resizableTriggers.set(
|
|
||||||
body,
|
|
||||||
(IPUBTrigger.#resizableTriggers.get(body) || new Set()).add(this),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
attributeChangedCallback(name, oldValue, newValue) {
|
|
||||||
super.attributeChangedCallback(name, oldValue, newValue);
|
|
||||||
|
|
||||||
const body = getAncestor(this, "ipub-body");
|
|
||||||
if (!body) {
|
|
||||||
console.error("IPUBTrigger: element must be a descendant of ipub-body");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const set = IPUBTrigger.#resizableTriggers.get(body) || new Set();
|
|
||||||
if (this.getAttribute("height") || this.getAttribute("width")) {
|
|
||||||
set.add(this);
|
|
||||||
} else {
|
|
||||||
set.delete(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "width" || name === "height") {
|
|
||||||
const height = Math.max(
|
|
||||||
body.scrollHeight,
|
|
||||||
body.getBoundingClientRect().height,
|
|
||||||
);
|
|
||||||
const width = Math.max(
|
|
||||||
body.scrollWidth,
|
|
||||||
body.getBoundingClientRect().width,
|
|
||||||
);
|
|
||||||
|
|
||||||
const percH = this.getAttribute("height");
|
|
||||||
if (percH) {
|
|
||||||
this.style.setProperty(
|
|
||||||
"--ipub-height",
|
|
||||||
`${Math.round((height / 100) * Number.parseFloat(percH))}px`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const percW = this.getAttribute("width");
|
|
||||||
if (percW) {
|
|
||||||
this.style.setProperty(
|
|
||||||
"--ipub-width",
|
|
||||||
`${Math.round((width / 100) * Number.parseFloat(percW))}px`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
const set = IPUBTrigger.#resizableTriggers.get(body) || new Set();
|
|
||||||
set.delete(this);
|
|
||||||
|
|
||||||
if (set.size === 0) {
|
|
||||||
const body = getAncestor(this, "ipub-body");
|
|
||||||
if (!body) {
|
|
||||||
console.error("IPUBTrigger: element must be a descendant of ipub-body");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug(
|
|
||||||
`IPUBTrigger#${this.id}: removing ${IPUBBody.elementName}#${body.id} from resize observer`,
|
|
||||||
);
|
|
||||||
IPUBTrigger.#resizableTriggers.delete(body);
|
|
||||||
IPUBTrigger.#resizeObserver.unobserve(body);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -24,95 +24,126 @@
|
|||||||
</dialog>
|
</dialog>
|
||||||
</ipub-cover>
|
</ipub-cover>
|
||||||
<main id="content">
|
<main id="content">
|
||||||
<ipub-background id="background0001">
|
<ipub-track>
|
||||||
<img src="../images/background0001.jpg" width="100" height="100" />
|
<ipub-track-item>
|
||||||
</ipub-background>
|
<ipub-track-item-position style="--ipub-track-item-position: 0%;" />
|
||||||
<ipub-soundtrack style="--ipub-color:cyan">
|
<ipub-soundtrack style="--ipub-color: cyan">
|
||||||
<ipub-trigger height="10" />
|
<figure>
|
||||||
<!-- TODO: Search on how to make this more accessible, more semantic as using <details> -->
|
<label>
|
||||||
<figure>
|
<input type="checkbox" />
|
||||||
<label>
|
<figcaption>Soundtrack 1</figcaption>
|
||||||
<input type="checkbox" />
|
</label>
|
||||||
<figcaption>Soundtrack 1</figcaption>
|
<ipub-audio>
|
||||||
</label>
|
<audio controls="true" volume="0" controlslist="nofullscreen"
|
||||||
<ipub-audio>
|
disableremoteplayback="true">
|
||||||
<audio controls="true" volume="0" controlslist="nofullscreen"
|
<source src="../audios/track1.webm" />
|
||||||
disableremoteplayback="">
|
</audio>
|
||||||
<source src="../audios/track1.webm" />
|
</ipub-audio>
|
||||||
</audio>
|
</figure>
|
||||||
</ipub-audio>
|
</ipub-soundtrack>
|
||||||
</figure>
|
</ipub-track-item>
|
||||||
</ipub-soundtrack>
|
<ipub-track-item>
|
||||||
<ipub-image>
|
<ipub-track-item-position style="--ipub-track-item-position: 50%;" />
|
||||||
<img src="../images/image0001.png" />
|
<ipub-soundtrack style="--ipub-color: green">
|
||||||
<ipub-interaction style="--ipub-y:88.5%;--ipub-x:6%" circle="">
|
<figure>
|
||||||
<a href="https://krita.org" referrerpolicy="same-origin"
|
<label>
|
||||||
rel="external nofollow noopener noreferrer" target="_blank" />
|
<input type="checkbox" />
|
||||||
</ipub-interaction>
|
<figcaption>Soundtrack 2</figcaption>
|
||||||
<ipub-interaction style="--ipub-y:93.5%;--ipub-x:81.5%;--ipub-size:13%;">
|
</label>
|
||||||
<a href="https://guz.one" referrerpolicy="same-origin"
|
<ipub-audio>
|
||||||
rel="external nofollow noopener noreferrer" target="_blank" />
|
<audio controls="true" volume="0" controlslist="nofullscreen"
|
||||||
</ipub-interaction>
|
disableremoteplayback="true">
|
||||||
</ipub-image>
|
<source src="../audios/track2.webm" />
|
||||||
<ipub-image>
|
</audio>
|
||||||
<img src="../images/image0002.png" />
|
</ipub-audio>
|
||||||
</ipub-image>
|
</figure>
|
||||||
<ipub-soundtrack style="--ipub-color:green;">
|
</ipub-soundtrack>
|
||||||
<ipub-trigger height="10" />
|
</ipub-track-item>
|
||||||
<figure>
|
</ipub-track>
|
||||||
<label>
|
<ipub-background id="background0001">
|
||||||
<input type="checkbox" />
|
<img src="../images/background0001.jpg" width="100" height="100" />
|
||||||
<figcaption>Soundtrack 2</figcaption>
|
</ipub-background>
|
||||||
</label>
|
<!-- <ipub-soundtrack style="==ipub-color:cyan"> -->
|
||||||
<ipub-audio>
|
<!-- <!== TODO: Search on how to make this more accessible, more semantic as using <details> -->-->
|
||||||
<audio controls="true" volume="0" controlslist="nofullscreen"
|
|
||||||
disableremoteplayback="">
|
<!-- <figure> -->
|
||||||
<source src="../audios/track2.webm" />
|
<!-- <label> -->
|
||||||
</audio>
|
<!-- <input type="checkbox" /> -->
|
||||||
</ipub-audio>
|
<!-- <figcaption>Soundtrack 1</figcaption> -->
|
||||||
</figure>
|
<!-- </label> -->
|
||||||
</ipub-soundtrack>
|
<!-- <ipub-audio> -->
|
||||||
<ipub-background id="background0002">
|
<!-- <audio controls="true" volume="0" controlslist="nofullscreen" -->
|
||||||
<picture>
|
<!-- disableremoteplayback=""> -->
|
||||||
<img src="../images/background0002.jpg" />
|
<!-- <source src="../audios/track1.webm" /> -->
|
||||||
</picture>
|
<!-- </audio> -->
|
||||||
</ipub-background>
|
<!-- </ipub-audio> -->
|
||||||
<ipub-image>
|
<!-- </figure> -->
|
||||||
<img src="../images/image0003.png" />
|
<!-- </ipub-soundtrack> -->
|
||||||
</ipub-image>
|
<ipub-image>
|
||||||
<ipub-image>
|
<img src="../images/image0001.png" />
|
||||||
<img src="../images/image0004.png" />
|
<ipub-interaction style="--ipub-y:88.5%;--ipub-x:6%" circle="">
|
||||||
</ipub-image>
|
<a href="https://krita.org" referrerpolicy="same-origin"
|
||||||
<ipub-background id="background0003">
|
rel="external nofollow noopener noreferrer" target="_blank" />
|
||||||
<picture>
|
</ipub-interaction>
|
||||||
<img src="../images/background0003.jpg" />
|
<ipub-interaction style="--ipub-y:93.5%;--ipub-x:81.5%;--ipub-size:13%;">
|
||||||
</picture>
|
<a href="https://guz.one" referrerpolicy="same-origin"
|
||||||
</ipub-background>
|
rel="external nofollow noopener noreferrer" target="_blank" />
|
||||||
<ipub-image>
|
</ipub-interaction>
|
||||||
<img src="../images/image0002.png" />
|
</ipub-image>
|
||||||
</ipub-image>
|
<ipub-image>
|
||||||
<ipub-soundtrack style="--ipub-color:yellow;">
|
<img src="../images/image0002.png" />
|
||||||
<ipub-trigger height="10" />
|
</ipub-image>
|
||||||
<figure>
|
<!-- <ipub-soundtrack style="==ipub-color:green;"> -->
|
||||||
<label>
|
<!-- <figure> -->
|
||||||
<input type="checkbox" />
|
<!-- <label> -->
|
||||||
<figcaption>Soundtrack 3</figcaption>
|
<!-- <input type="checkbox" /> -->
|
||||||
</label>
|
<!-- <figcaption>Soundtrack 2</figcaption> -->
|
||||||
<ipub-audio>
|
<!-- </label> -->
|
||||||
<audio controls="true" volume="0" controlslist="nofullscreen"
|
<!-- <ipub-audio> -->
|
||||||
disableremoteplayback="">
|
<!-- <audio controls="true" volume="0" controlslist="nofullscreen" disableremoteplayback=""> -->
|
||||||
<source src="../audios/track3.webm" />
|
<!-- <source src="../audios/track2.webm" /> -->
|
||||||
</audio>
|
<!-- </audio> -->
|
||||||
</ipub-audio>
|
<!-- </ipub-audio> -->
|
||||||
</figure>
|
<!-- </figure> -->
|
||||||
</ipub-soundtrack>
|
<!-- </ipub-soundtrack> -->
|
||||||
<ipub-image>
|
<ipub-background id="background0002">
|
||||||
<img src="../images/image0003.png" />
|
<picture>
|
||||||
</ipub-image>
|
<img src="../images/background0002.jpg" />
|
||||||
<ipub-image>
|
</picture>
|
||||||
<img src="../images/image0004.png" />
|
</ipub-background>
|
||||||
</ipub-image>
|
<ipub-image>
|
||||||
</main>
|
<img src="../images/image0003.png" />
|
||||||
|
</ipub-image>
|
||||||
|
<ipub-image>
|
||||||
|
<img src="../images/image0004.png" />
|
||||||
|
</ipub-image>
|
||||||
|
<ipub-background id="background0003">
|
||||||
|
<picture>
|
||||||
|
<img src="../images/background0003.jpg" />
|
||||||
|
</picture>
|
||||||
|
</ipub-background>
|
||||||
|
<ipub-image>
|
||||||
|
<img src="../images/image0002.png" />
|
||||||
|
</ipub-image>
|
||||||
|
<!-- <ipub-soundtrack style="==ipub-color:yellow;"> -->
|
||||||
|
<!-- <figure> -->
|
||||||
|
<!-- <label> -->
|
||||||
|
<!-- <input type="checkbox" /> -->
|
||||||
|
<!-- <figcaption>Soundtrack 3</figcaption> -->
|
||||||
|
<!-- </label> -->
|
||||||
|
<!-- <ipub-audio> -->
|
||||||
|
<!-- <audio controls="true" volume="0" controlslist="nofullscreen" disableremoteplayback=""> -->
|
||||||
|
<!-- <source src="../audios/track3.webm" /> -->
|
||||||
|
<!-- </audio> -->
|
||||||
|
<!-- </ipub-audio> -->
|
||||||
|
<!-- </figure> -->
|
||||||
|
<!-- </ipub-soundtrack> -->
|
||||||
|
<ipub-image>
|
||||||
|
<img src="../images/image0003.png" />
|
||||||
|
</ipub-image>
|
||||||
|
<ipub-image>
|
||||||
|
<img src="../images/image0004.png" />
|
||||||
|
</ipub-image></main>
|
||||||
</ipub-body>
|
</ipub-body>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -28,6 +28,38 @@
|
|||||||
outline-style: solid;
|
outline-style: solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ipub-track {
|
||||||
|
z-index: 1000;
|
||||||
|
position: absolute;
|
||||||
|
display: inline-block;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
ipub-track-item {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
*:not(ipub-offset) {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ipub-track-item-position {
|
||||||
|
width: 5rem;
|
||||||
|
display: inline-block;
|
||||||
|
height: var(--ipub-track-item-position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ipub-cover > dialog[open] {
|
ipub-cover > dialog[open] {
|
||||||
--ipub-accent-color: #fff;
|
--ipub-accent-color: #fff;
|
||||||
z-index: var(--z-cover);
|
z-index: var(--z-cover);
|
||||||
@@ -199,18 +231,6 @@ ipub-soundtrack {
|
|||||||
&[playing] figure figcaption::before {
|
&[playing] figure figcaption::before {
|
||||||
content: "P "; /* TODO: change to an icon and better positioning */
|
content: "P "; /* TODO: change to an icon and better positioning */
|
||||||
}
|
}
|
||||||
|
|
||||||
& > ipub-trigger {
|
|
||||||
position: absolute;
|
|
||||||
display: inline-block;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
transform: translateY(--ipub-offset, 0%);
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
width: var(--ipub-width, 100%);
|
|
||||||
height: var(--ipub-height, 0%);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ipub-background {
|
ipub-background {
|
||||||
|
|||||||
17
.forgejo/workflows/todos.yaml
Normal file
17
.forgejo/workflows/todos.yaml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# name: TODO tracker
|
||||||
|
# on: [push, pull_request]
|
||||||
|
# jobs:
|
||||||
|
# build:
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# steps:
|
||||||
|
# - uses: https://forge.capytal.company/actions/checkout@v3
|
||||||
|
# - uses: https://forge.capytal.company/actions/tdg-forgejo-action@master
|
||||||
|
# with:
|
||||||
|
# TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||||
|
# REPO: ${{ github.repository }}
|
||||||
|
# SHA: ${{ github.sha }}
|
||||||
|
# REF: ${{ github.ref }}
|
||||||
|
# LABEL: status/todo
|
||||||
|
# DRY_RUN: true
|
||||||
|
# COMMENT_ON_ISSUES: 128
|
||||||
|
# ASSIGN_FROM_BLAME: 128
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,4 +3,5 @@ out.css
|
|||||||
.tmp
|
.tmp
|
||||||
.env
|
.env
|
||||||
*.db
|
*.db
|
||||||
|
*.epub
|
||||||
tmp
|
tmp
|
||||||
|
|||||||
@@ -151,9 +151,9 @@ func (app *app) setup() error {
|
|||||||
return fmt.Errorf("app: failed to start token repository: %w", err)
|
return fmt.Errorf("app: failed to start token repository: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
publicationRepository, err := repository.NewPublication(app.ctx, app.db, app.logger.WithGroup("repository.publication"), app.assert)
|
projectRepository, err := repository.NewProject(app.ctx, app.db, app.logger.WithGroup("repository.project"), app.assert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("app: failed to start publication repository: %w", err)
|
return fmt.Errorf("app: failed to start project repository: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
permissionRepository, err := repository.NewPermissions(app.ctx, app.db, app.logger.WithGroup("repository.permission"), app.assert)
|
permissionRepository, err := repository.NewPermissions(app.ctx, app.db, app.logger.WithGroup("repository.permission"), app.assert)
|
||||||
@@ -169,12 +169,12 @@ func (app *app) setup() error {
|
|||||||
Logger: app.logger.WithGroup("service.token"),
|
Logger: app.logger.WithGroup("service.token"),
|
||||||
Assertions: app.assert,
|
Assertions: app.assert,
|
||||||
})
|
})
|
||||||
publicationService := service.NewPublication(publicationRepository, permissionRepository, app.logger.WithGroup("service.publication"), app.assert)
|
projectService := service.NewProject(projectRepository, permissionRepository, app.logger.WithGroup("service.project"), app.assert)
|
||||||
|
|
||||||
app.handler, err = router.New(router.Config{
|
app.handler, err = router.New(router.Config{
|
||||||
UserService: userService,
|
UserService: userService,
|
||||||
TokenService: tokenService,
|
TokenService: tokenService,
|
||||||
PublicationService: publicationService,
|
ProjectService: projectService,
|
||||||
|
|
||||||
Templates: app.templates,
|
Templates: app.templates,
|
||||||
DisableCache: app.developmentMode,
|
DisableCache: app.developmentMode,
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
package assets
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"io/fs"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed css/style.css js/*.js ipub/*.js ipub/*.css
|
|
||||||
var files embed.FS
|
|
||||||
|
|
||||||
func New() fs.FS {
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
/*! tailwindcss v4.1.17 | MIT License | https://tailwindcss.com */
|
|
||||||
@layer theme, base, components, utilities;
|
|
||||||
@layer theme {
|
|
||||||
:root, :host {
|
|
||||||
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
|
||||||
'Noto Color Emoji';
|
|
||||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
|
||||||
monospace;
|
|
||||||
--color-gray-50: oklch(98.5% 0.002 247.839);
|
|
||||||
--color-gray-600: oklch(44.6% 0.03 256.802);
|
|
||||||
--color-gray-700: oklch(37.3% 0.034 259.733);
|
|
||||||
--color-gray-900: oklch(21% 0.034 264.665);
|
|
||||||
--spacing: 0.25rem;
|
|
||||||
--radius-md: 0.375rem;
|
|
||||||
--default-font-family: var(--font-sans);
|
|
||||||
--default-mono-font-family: var(--font-mono);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@layer base {
|
|
||||||
*, ::after, ::before, ::backdrop, ::file-selector-button {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0 solid;
|
|
||||||
}
|
|
||||||
html, :host {
|
|
||||||
line-height: 1.5;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
tab-size: 4;
|
|
||||||
font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji');
|
|
||||||
font-feature-settings: var(--default-font-feature-settings, normal);
|
|
||||||
font-variation-settings: var(--default-font-variation-settings, normal);
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
}
|
|
||||||
hr {
|
|
||||||
height: 0;
|
|
||||||
color: inherit;
|
|
||||||
border-top-width: 1px;
|
|
||||||
}
|
|
||||||
abbr:where([title]) {
|
|
||||||
-webkit-text-decoration: underline dotted;
|
|
||||||
text-decoration: underline dotted;
|
|
||||||
}
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
|
||||||
font-size: inherit;
|
|
||||||
font-weight: inherit;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
-webkit-text-decoration: inherit;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
b, strong {
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
code, kbd, samp, pre {
|
|
||||||
font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace);
|
|
||||||
font-feature-settings: var(--default-mono-font-feature-settings, normal);
|
|
||||||
font-variation-settings: var(--default-mono-font-variation-settings, normal);
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
small {
|
|
||||||
font-size: 80%;
|
|
||||||
}
|
|
||||||
sub, sup {
|
|
||||||
font-size: 75%;
|
|
||||||
line-height: 0;
|
|
||||||
position: relative;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
sub {
|
|
||||||
bottom: -0.25em;
|
|
||||||
}
|
|
||||||
sup {
|
|
||||||
top: -0.5em;
|
|
||||||
}
|
|
||||||
table {
|
|
||||||
text-indent: 0;
|
|
||||||
border-color: inherit;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
:-moz-focusring {
|
|
||||||
outline: auto;
|
|
||||||
}
|
|
||||||
progress {
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
summary {
|
|
||||||
display: list-item;
|
|
||||||
}
|
|
||||||
ol, ul, menu {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
img, svg, video, canvas, audio, iframe, embed, object {
|
|
||||||
display: block;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
img, video {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
button, input, select, optgroup, textarea, ::file-selector-button {
|
|
||||||
font: inherit;
|
|
||||||
font-feature-settings: inherit;
|
|
||||||
font-variation-settings: inherit;
|
|
||||||
letter-spacing: inherit;
|
|
||||||
color: inherit;
|
|
||||||
border-radius: 0;
|
|
||||||
background-color: transparent;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
:where(select:is([multiple], [size])) optgroup {
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
:where(select:is([multiple], [size])) optgroup option {
|
|
||||||
padding-inline-start: 20px;
|
|
||||||
}
|
|
||||||
::file-selector-button {
|
|
||||||
margin-inline-end: 4px;
|
|
||||||
}
|
|
||||||
::placeholder {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
|
|
||||||
::placeholder {
|
|
||||||
color: currentcolor;
|
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
|
||||||
color: color-mix(in oklab, currentcolor 50%, transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
textarea {
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
::-webkit-search-decoration {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
}
|
|
||||||
::-webkit-date-and-time-value {
|
|
||||||
min-height: 1lh;
|
|
||||||
text-align: inherit;
|
|
||||||
}
|
|
||||||
::-webkit-datetime-edit {
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
::-webkit-datetime-edit-fields-wrapper {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
|
|
||||||
padding-block: 0;
|
|
||||||
}
|
|
||||||
::-webkit-calendar-picker-indicator {
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
:-moz-ui-invalid {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button {
|
|
||||||
appearance: button;
|
|
||||||
}
|
|
||||||
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
[hidden]:where(:not([hidden='until-found'])) {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@layer utilities {
|
|
||||||
.absolute {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
.relative {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.static {
|
|
||||||
position: static;
|
|
||||||
}
|
|
||||||
.contents {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
.flex {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.resize {
|
|
||||||
resize: both;
|
|
||||||
}
|
|
||||||
.flex-col {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.bg-gray-900 {
|
|
||||||
background-color: var(--color-gray-900);
|
|
||||||
}
|
|
||||||
.text-gray-50 {
|
|
||||||
color: var(--color-gray-50);
|
|
||||||
}
|
|
||||||
.has-\[\#first-publication\]\:h-full {
|
|
||||||
&:has(*:is(#first-publication)) {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.has-\[\#first-publication\]\:h-svw {
|
|
||||||
&:has(*:is(#first-publication)) {
|
|
||||||
height: 100svw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@layer base {
|
|
||||||
form {
|
|
||||||
input {
|
|
||||||
background-color: var(--color-gray-700);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding-inline-start: calc(var(--spacing) * 2);
|
|
||||||
&:has(+ button) {
|
|
||||||
border-radius: var(--radius-md) 0 0 var(--radius-md);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: var(--color-gray-600);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 0 calc(var(--spacing) * 2);
|
|
||||||
input + & {
|
|
||||||
border-radius: 0 var(--radius-md) var(--radius-md) 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
form {
|
|
||||||
input {
|
|
||||||
background-color: var(--color-gray-700);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding-inline-start: --spacing(2);
|
|
||||||
&:has(+ button) {
|
|
||||||
border-radius: var(--radius-md) 0 0 var(--radius-md);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: var(--color-gray-600);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 0 --spacing(2);
|
|
||||||
input + & {
|
|
||||||
border-radius: 0 var(--radius-md) var(--radius-md) 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"code.capytal.cc/capytal/comicverse/editor"
|
|
||||||
"code.capytal.cc/capytal/comicverse/editor/assets"
|
|
||||||
"code.capytal.cc/capytal/comicverse/editor/router"
|
|
||||||
"code.capytal.cc/capytal/comicverse/editor/storage"
|
|
||||||
"code.capytal.cc/capytal/comicverse/editor/template"
|
|
||||||
"code.capytal.cc/loreddev/x/tinyssert"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
hostname = flag.String("hostname", "localhost", "Host to listen to")
|
|
||||||
port = flag.Uint("port", 8080, "Port to be used for the server.")
|
|
||||||
verbose = flag.Bool("verbose", false, "Print debug information on logs")
|
|
||||||
dev = flag.Bool("dev", false, "Run the server in debug mode.")
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
storageDir = getEnv("EDITOR_PUBLICATIONS_DIR", ".publications") // TODO: Use XDG_STATE_HOME as default
|
|
||||||
assetsDir = getEnv("EDITOR_ASSETS_DIR", "assets") // TODO: Use XDG_CONFIG_HOME as default
|
|
||||||
templatesDir = getEnv("EDITOR_TEMPLATES_DIR", "template") // TODO: Use XDG_CONFIG_HOME as default
|
|
||||||
)
|
|
||||||
|
|
||||||
func getEnv(key string, d string) string {
|
|
||||||
v := os.Getenv(key)
|
|
||||||
if v == "" {
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
flag.Parse()
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
ctx := context.Background()
|
|
||||||
log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{}))
|
|
||||||
assert := tinyssert.New(tinyssert.WithLogger(log))
|
|
||||||
|
|
||||||
assets := assets.New()
|
|
||||||
templater, err := template.New()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to initiate templater due to error", slog.String("error", err.Error()))
|
|
||||||
os.Exit(1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if *dev {
|
|
||||||
assets = os.DirFS(assetsDir)
|
|
||||||
log = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
||||||
assert = tinyssert.New(tinyssert.WithPanic(), tinyssert.WithLogger(log.WithGroup("assertions")))
|
|
||||||
templater, err = template.Dev(os.DirFS(templatesDir))
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to initiate dev templater due to error", slog.String("error", err.Error()))
|
|
||||||
os.Exit(1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.MkdirAll(storageDir, os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to create storage directory due to error", slog.String("error", err.Error()))
|
|
||||||
os.Exit(1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
root, err := os.OpenRoot(storageDir)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to open storage directory due to error", slog.String("error", err.Error()))
|
|
||||||
os.Exit(1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
storage := storage.Newlocal(root, log)
|
|
||||||
|
|
||||||
editor := editor.New(storage, log.WithGroup("editor"), assert)
|
|
||||||
|
|
||||||
router := router.New(router.Config{
|
|
||||||
Assets: assets,
|
|
||||||
Editor: editor,
|
|
||||||
Templater: templater,
|
|
||||||
Logger: log.WithGroup("router"),
|
|
||||||
})
|
|
||||||
|
|
||||||
srv := &http.Server{
|
|
||||||
Addr: fmt.Sprintf("%s:%d", *hostname, *port),
|
|
||||||
Handler: router,
|
|
||||||
}
|
|
||||||
|
|
||||||
c, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
defer stop()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
log.Info("Starting application",
|
|
||||||
slog.String("host", *hostname),
|
|
||||||
slog.Uint64("port", uint64(*port)),
|
|
||||||
slog.Bool("verbose", *verbose),
|
|
||||||
slog.Bool("development", *dev))
|
|
||||||
|
|
||||||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
||||||
log.Error("Failed to start application server", slog.String("error", err.Error()))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
<-c.Done()
|
|
||||||
|
|
||||||
log.Info("Stopping application gracefully")
|
|
||||||
if err := srv.Shutdown(c); err != nil {
|
|
||||||
log.Error("Failed to stop application server gracefully", slog.String("error", err.Error()))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("FINAL")
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package editor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
_ "embed"
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"path"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.capytal.cc/capytal/comicverse/editor/epub"
|
|
||||||
"code.capytal.cc/capytal/comicverse/editor/internals/shortid"
|
|
||||||
"code.capytal.cc/capytal/comicverse/editor/storage"
|
|
||||||
"code.capytal.cc/loreddev/x/tinyssert"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"golang.org/x/net/html"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Container struct {
|
|
||||||
id uuid.UUID
|
|
||||||
|
|
||||||
pkg epub.Package
|
|
||||||
storage storage.Storage
|
|
||||||
|
|
||||||
log *slog.Logger
|
|
||||||
assert tinyssert.Assertions
|
|
||||||
|
|
||||||
flushed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func (p *Container) Flush() error {
|
|
||||||
p.assert.NotZero(p.pkg, "invalid ePUB: package must be set")
|
|
||||||
p.assert.NotZero(p.pkg.Metadata, "invalid ePUB: package must have metadata")
|
|
||||||
p.assert.NotZero(p.pkg.Metadata.ID, "invalid ePUB: ID must always be specified")
|
|
||||||
p.assert.NotZero(p.pkg.Metadata.Language, "invalid ePUB: Language must always be specified")
|
|
||||||
p.assert.NotZero(p.pkg.Metadata.Title, "invalid ePUB: Title must always be specified")
|
|
||||||
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
p.log.Debug("Flushing state of publication")
|
|
||||||
|
|
||||||
if p.flushed {
|
|
||||||
p.log.Debug("Publication doesn't have unsaved changes, skipping flush")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
defer p.log.Debug("Publication's state flushed")
|
|
||||||
|
|
||||||
b, err := xml.MarshalIndent(p.pkg, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("editor.Publication: failed to marshal package: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = p.storage.Write("content.opf", b); err != nil {
|
|
||||||
return fmt.Errorf("editor.Publication: failed to write content.opf: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
114
editor/editor.go
114
editor/editor.go
@@ -1,114 +0,0 @@
|
|||||||
package editor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log/slog"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.capytal.cc/capytal/comicverse/editor/epub"
|
|
||||||
"code.capytal.cc/capytal/comicverse/editor/storage"
|
|
||||||
"code.capytal.cc/loreddev/x/tinyssert"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"golang.org/x/text/language"
|
|
||||||
)
|
|
||||||
|
|
||||||
func New(
|
|
||||||
storage storage.Storage,
|
|
||||||
logger *slog.Logger,
|
|
||||||
assert tinyssert.Assertions,
|
|
||||||
) *Editor {
|
|
||||||
assert.NotZero(storage)
|
|
||||||
assert.NotZero(logger)
|
|
||||||
|
|
||||||
return &Editor{
|
|
||||||
storage: storage,
|
|
||||||
|
|
||||||
log: logger,
|
|
||||||
assert: assert,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Editor struct {
|
|
||||||
storage storage.Storage
|
|
||||||
|
|
||||||
ctx context.Context
|
|
||||||
log *slog.Logger
|
|
||||||
assert tinyssert.Assertions
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Editor) New(id uuid.UUID, title string, lang language.Tag) (*Container, error) {
|
|
||||||
f := fmt.Sprintf("%s/content.opf", id)
|
|
||||||
if e.storage.Exists(f) {
|
|
||||||
return nil, ErrAlreadyExists
|
|
||||||
}
|
|
||||||
|
|
||||||
pub := &Container{
|
|
||||||
id: id,
|
|
||||||
|
|
||||||
pkg: epub.Package{
|
|
||||||
Metadata: epub.Metadata{
|
|
||||||
ID: fmt.Sprintf("comicverse:%s", id),
|
|
||||||
Title: title,
|
|
||||||
Language: lang,
|
|
||||||
Date: time.Now(),
|
|
||||||
Modified: time.Now(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
log: e.log.WithGroup(fmt.Sprintf("publication:%s", id)),
|
|
||||||
assert: e.assert,
|
|
||||||
|
|
||||||
storage: storage.WithRoot(id.String(), e.storage),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := pub.Flush()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("editor: unable to flush changes of publication: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pub, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Editor) Open(id uuid.UUID) (*Container, error) {
|
|
||||||
content, err := e.storage.Open(fmt.Sprintf("%s/content.opf", id))
|
|
||||||
if errors.Is(err, storage.ErrNotExists) {
|
|
||||||
return nil, ErrNotExists
|
|
||||||
} else if err != nil {
|
|
||||||
return nil, fmt.Errorf("editor: unable to open package: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := io.ReadAll(content)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("editor: unable to read contents of package: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var pkg epub.Package
|
|
||||||
|
|
||||||
err = xml.Unmarshal(b, &pkg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("editor: unable to decode xml of package: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := &Container{
|
|
||||||
id: id,
|
|
||||||
|
|
||||||
pkg: pkg,
|
|
||||||
|
|
||||||
log: e.log.WithGroup(fmt.Sprintf("publication:%s", id)),
|
|
||||||
assert: e.assert,
|
|
||||||
|
|
||||||
storage: storage.WithRoot(id.String(), e.storage),
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrAlreadyExists = errors.New("editor: file already exists")
|
|
||||||
ErrNotExists = errors.New("editor: file doesn't exist")
|
|
||||||
)
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package epub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Meta struct {
|
|
||||||
Attributes map[string]string `xml:"-"`
|
|
||||||
Value string `xml:",chardata"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ xml.Marshaler = Meta{}
|
|
||||||
_ xml.Unmarshaler = (*Meta)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m Meta) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
|
||||||
for n, v := range m.Attributes {
|
|
||||||
start.Attr = append(start.Attr, xml.Attr{
|
|
||||||
Name: xml.Name{Local: n},
|
|
||||||
Value: v,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return e.EncodeElement(m.Value, start)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Meta) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
||||||
if m == nil {
|
|
||||||
m = &Meta{}
|
|
||||||
}
|
|
||||||
if m.Attributes == nil {
|
|
||||||
m.Attributes = map[string]string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, attr := range start.Attr {
|
|
||||||
m.Attributes[attr.Name.Local] = attr.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := d.DecodeElement(&m.Value, &start); err != nil {
|
|
||||||
return fmt.Errorf("epub.Meta: failed to decode chardata: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,450 +0,0 @@
|
|||||||
package epub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.capytal.cc/capytal/comicverse/editor/internals/shortid"
|
|
||||||
"golang.org/x/text/language"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Package struct {
|
|
||||||
Metadata Metadata `xml:"metadata"`
|
|
||||||
Manifest Manisfest `xml:"manifest"`
|
|
||||||
Spine Spine `xml:"spine"`
|
|
||||||
|
|
||||||
// TODO: Collections https://www.w3.org/TR/epub-33/#sec-pkg-collections
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ xml.Marshaler = Package{}
|
|
||||||
|
|
||||||
func (p Package) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
|
||||||
start.Name = xml.Name{
|
|
||||||
Local: "package",
|
|
||||||
Space: "http://www.idpf.org/2007/opf",
|
|
||||||
}
|
|
||||||
|
|
||||||
start.Attr = append(start.Attr, []xml.Attr{
|
|
||||||
{Name: xml.Name{Local: "xmlns:dc"}, Value: "http://purl.org/dc/elements/1.1/"},
|
|
||||||
{Name: xml.Name{Local: "xmlns:dcterms"}, Value: "http://purl.org/dc/terms/"},
|
|
||||||
{Name: xml.Name{Local: "xmlns:opf"}, Value: "http://www.idpf.org/2007/opf"},
|
|
||||||
{Name: xml.Name{Local: "unique-identifier"}, Value: uniqueIdentifierID},
|
|
||||||
{Name: xml.Name{Local: "version"}, Value: "3.0"},
|
|
||||||
}...)
|
|
||||||
|
|
||||||
if err := e.EncodeToken(start); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err := e.EncodeElement(p.Metadata, xml.StartElement{Name: xml.Name{Local: "metadata"}})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = e.EncodeElement(p.Manifest, xml.StartElement{Name: xml.Name{Local: "manifest"}})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = e.EncodeElement(p.Spine, xml.StartElement{Name: xml.Name{Local: "spine"}})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
|
||||||
}
|
|
||||||
|
|
||||||
type Metadata struct {
|
|
||||||
ID string `xml:"dc:identifier"`
|
|
||||||
Title string `xml:"dc:title"`
|
|
||||||
Language language.Tag `xml:"dc:language"`
|
|
||||||
Creators []Person `xml:"dc:creator"`
|
|
||||||
Contributors []Person `xml:"dc:contributor"`
|
|
||||||
Date time.Time `xml:"dc:date"`
|
|
||||||
Modified time.Time `xml:"-"`
|
|
||||||
|
|
||||||
// TODO: Support for dc:subject, dc:type and meta elements
|
|
||||||
// https://www.w3.org/TR/epub-33/#sec-opf-dcsubject
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ xml.Marshaler = Metadata{}
|
|
||||||
_ xml.Unmarshaler = (*Metadata)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m Metadata) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
|
||||||
if err := e.EncodeToken(start); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
helper := encoderHelper(e, "epub.Metadata")
|
|
||||||
|
|
||||||
err := helper("dc:identifier", m.ID, xml.Attr{
|
|
||||||
Name: xml.Name{Local: "id"}, Value: uniqueIdentifierID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = helper("dc:title", m.Title); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = helper("dc:language", m.Language.String()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, creator := range m.Creators {
|
|
||||||
err := creator.marshalIntoRootXML(xml.Name{Local: "dc:creator"}, e)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, contributor := range m.Contributors {
|
|
||||||
err := contributor.marshalIntoRootXML(xml.Name{Local: "dc:contributor"}, e)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !m.Date.IsZero() {
|
|
||||||
if err = helper("dc:date", m.Date.Format(time.RFC3339)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !m.Modified.IsZero() {
|
|
||||||
if err = helper("meta", m.Modified.Format(time.RFC3339), xml.Attr{
|
|
||||||
Name: xml.Name{Local: "property"}, Value: "dcterms:modified",
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.EncodeToken(start.End())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Metadata) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
||||||
if m == nil {
|
|
||||||
m = &Metadata{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var v struct {
|
|
||||||
ID string `xml:"http://purl.org/dc/elements/1.1/ identifier"`
|
|
||||||
Title string `xml:"http://purl.org/dc/elements/1.1/ title"`
|
|
||||||
Language language.Tag `xml:"http://purl.org/dc/elements/1.1/ language"`
|
|
||||||
Creators []Person `xml:"http://purl.org/dc/elements/1.1/ creator"`
|
|
||||||
Contributors []Person `xml:"http://purl.org/dc/elements/1.1/ contributor"`
|
|
||||||
Date string `xml:"http://purl.org/dc/elements/1.1/ date"`
|
|
||||||
|
|
||||||
Meta []Meta `xml:"meta"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := d.DecodeElement(&v, &start); err != nil {
|
|
||||||
return fmt.Errorf("epub.Metadata: unable to unmarshal: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.ID = v.ID
|
|
||||||
m.Title = v.Title
|
|
||||||
m.Language = v.Language
|
|
||||||
|
|
||||||
if v.Date != "" {
|
|
||||||
t, err := time.Parse(time.RFC3339, v.Date)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("epub.Metadata: date is not valid: %w", err)
|
|
||||||
}
|
|
||||||
m.Date = t
|
|
||||||
}
|
|
||||||
|
|
||||||
m.Creators = v.Creators
|
|
||||||
|
|
||||||
for i, c := range m.Creators {
|
|
||||||
c, err := c.unmarshalFromMetas(v.Meta)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("epub.Metadata: invalid creator metadata %q: %w", c.Name, err)
|
|
||||||
}
|
|
||||||
m.Creators[i] = c
|
|
||||||
}
|
|
||||||
|
|
||||||
m.Contributors = v.Contributors
|
|
||||||
|
|
||||||
for i, c := range m.Contributors {
|
|
||||||
c, err := c.unmarshalFromMetas(v.Meta)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("epub.Metadata: invalid creator metadata %q: %w", c.Name, err)
|
|
||||||
}
|
|
||||||
m.Contributors[i] = c
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, meta := range v.Meta {
|
|
||||||
if property, ok := meta.Attributes["property"]; ok {
|
|
||||||
switch property {
|
|
||||||
case "dcterms:modified":
|
|
||||||
t, err := time.Parse(time.RFC3339, meta.Value)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("epub.Metadata: modified date is not valid: %w", err)
|
|
||||||
}
|
|
||||||
m.Modified = t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var uniqueIdentifierID = "pub-id"
|
|
||||||
|
|
||||||
type Person struct {
|
|
||||||
ID string `xml:"id,attr"`
|
|
||||||
Name string `xml:",chardata"`
|
|
||||||
Role string `xml:"-"`
|
|
||||||
FileAs string `xml:"-"`
|
|
||||||
|
|
||||||
AlternateScripts map[language.Tag]string `xml:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Person) marshalIntoRootXML(name xml.Name, e *xml.Encoder) error {
|
|
||||||
if p.ID == "" {
|
|
||||||
p.ID = shortid.New().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
err := e.EncodeElement(p.Name, xml.StartElement{
|
|
||||||
Name: name,
|
|
||||||
Attr: []xml.Attr{{Name: xml.Name{Local: "id"}, Value: p.ID}},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for lang, name := range p.AlternateScripts {
|
|
||||||
err = e.EncodeElement(name, xml.StartElement{
|
|
||||||
Name: xml.Name{Local: "meta"},
|
|
||||||
Attr: []xml.Attr{
|
|
||||||
{Name: xml.Name{Local: "refines"}, Value: fmt.Sprintf("#%s", p.ID)},
|
|
||||||
{Name: xml.Name{Local: "property"}, Value: "alternate-script"},
|
|
||||||
{Name: xml.Name{Local: "xml:lang"}, Value: lang.String()},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.FileAs != "" {
|
|
||||||
err = e.EncodeElement(p.FileAs, xml.StartElement{
|
|
||||||
Name: xml.Name{Local: "meta"},
|
|
||||||
Attr: []xml.Attr{
|
|
||||||
{Name: xml.Name{Local: "refines"}, Value: fmt.Sprintf("#%s", p.ID)},
|
|
||||||
{Name: xml.Name{Local: "property"}, Value: "file-as"},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.Role != "" {
|
|
||||||
err = e.EncodeElement(p.Role, xml.StartElement{
|
|
||||||
Name: xml.Name{Local: "meta"},
|
|
||||||
Attr: []xml.Attr{
|
|
||||||
{Name: xml.Name{Local: "refines"}, Value: fmt.Sprintf("#%s", p.ID)},
|
|
||||||
{Name: xml.Name{Local: "property"}, Value: "role"},
|
|
||||||
{Name: xml.Name{Local: "scheme"}, Value: "marc:relators"},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Person) unmarshalFromMetas(metaList []Meta) (Person, error) {
|
|
||||||
if p.ID == "" {
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
if p.AlternateScripts == nil {
|
|
||||||
p.AlternateScripts = map[language.Tag]string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, meta := range metaList {
|
|
||||||
refines, ok := meta.Attributes["refines"]
|
|
||||||
if !ok || refines != fmt.Sprintf("#%s", p.ID) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
property, ok := meta.Attributes["property"]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch property {
|
|
||||||
case "alternate-script":
|
|
||||||
l, ok := meta.Attributes["lang"]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
lang, err := language.Parse(l)
|
|
||||||
if err != nil {
|
|
||||||
return p, fmt.Errorf("epub.Person: language %q is not valid: %w", l, err)
|
|
||||||
}
|
|
||||||
p.AlternateScripts[lang] = meta.Value
|
|
||||||
case "file-as":
|
|
||||||
p.FileAs = meta.Value
|
|
||||||
case "role":
|
|
||||||
p.Role = meta.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Manisfest struct {
|
|
||||||
Items []Item `xml:"item"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Item struct {
|
|
||||||
ID string `xml:"id,attr"`
|
|
||||||
HRef string `xml:"href,attr"`
|
|
||||||
MediaType string `xml:"media-type,attr"`
|
|
||||||
MediaOverlay string `xml:"media-overlay,attr,omitempty"`
|
|
||||||
Properties ItemProperties `xml:"properties,attr,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Spine struct {
|
|
||||||
ID string `xml:"id,attr,omitempty"`
|
|
||||||
Toc string `xml:"toc,attr,omitempty"`
|
|
||||||
|
|
||||||
PageProgressionDir PageProgressionDir `xml:"page-progression-direction,attr,omitempty"`
|
|
||||||
|
|
||||||
ItemRefs []ItemRef `xml:"itemref"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PageProgressionDir string
|
|
||||||
|
|
||||||
const (
|
|
||||||
PageProgressionDirDefault PageProgressionDir = "default"
|
|
||||||
PageProgressionDirLTR PageProgressionDir = "ltr"
|
|
||||||
PageProgressionDirRTL PageProgressionDir = "rtl"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ItemRef struct {
|
|
||||||
IDRef string `xml:"idref,attr"`
|
|
||||||
ID string `xml:"id,attr"`
|
|
||||||
NotLinear bool `xml:"linear,attr"`
|
|
||||||
Properties ItemProperties `xml:"properties,attr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ xml.Marshaler = ItemRef{}
|
|
||||||
_ xml.Unmarshaler = (*ItemRef)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func (ref ItemRef) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
|
||||||
linear := xml.Attr{Name: xml.Name{Local: "linear"}}
|
|
||||||
if !ref.NotLinear {
|
|
||||||
linear.Value = "no"
|
|
||||||
} else {
|
|
||||||
linear.Value = "yes"
|
|
||||||
}
|
|
||||||
|
|
||||||
props, _ := ref.Properties.MarshalXMLAttr(xml.Name{Local: "properties"})
|
|
||||||
|
|
||||||
start.Attr = append(start.Attr, []xml.Attr{
|
|
||||||
{Name: xml.Name{Local: "idref"}, Value: ref.IDRef},
|
|
||||||
{Name: xml.Name{Local: "id"}, Value: ref.ID},
|
|
||||||
linear,
|
|
||||||
props,
|
|
||||||
}...)
|
|
||||||
|
|
||||||
if err := e.EncodeToken(start); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return e.EncodeToken(start.End())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ref *ItemRef) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
||||||
if ref == nil {
|
|
||||||
ref = &ItemRef{}
|
|
||||||
}
|
|
||||||
for _, attr := range start.Attr {
|
|
||||||
switch attr.Name.Local {
|
|
||||||
case "idref":
|
|
||||||
ref.IDRef = attr.Value
|
|
||||||
case "id":
|
|
||||||
ref.ID = attr.Value
|
|
||||||
case "linear":
|
|
||||||
if attr.Value == "no" {
|
|
||||||
ref.NotLinear = true
|
|
||||||
} else {
|
|
||||||
ref.NotLinear = false
|
|
||||||
}
|
|
||||||
case "properties":
|
|
||||||
ref.Properties.UnmarshalXMLAttr(attr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var t string
|
|
||||||
return d.DecodeElement(&t, &start)
|
|
||||||
}
|
|
||||||
|
|
||||||
type (
|
|
||||||
ItemProperty string
|
|
||||||
ItemProperties []ItemProperty
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ItemPropertyCoverImage ItemProperty = "cover-image"
|
|
||||||
ItemPropertyNav ItemProperty = "nav"
|
|
||||||
ItemPropertyMathML ItemProperty = "mathml"
|
|
||||||
ItemPropertyRemoteResources ItemProperty = "remote-resources"
|
|
||||||
ItemPropertyScripted ItemProperty = "scripted"
|
|
||||||
ItemPropertySVG ItemProperty = "svg"
|
|
||||||
ItemPropertySwitch ItemProperty = "switch"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ xml.MarshalerAttr = (ItemProperties)(nil)
|
|
||||||
_ xml.UnmarshalerAttr = (*ItemProperties)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func (is ItemProperties) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
|
|
||||||
strs := make([]string, len(is))
|
|
||||||
for i := range is {
|
|
||||||
strs[i] = string(is[i])
|
|
||||||
}
|
|
||||||
return xml.Attr{Name: name, Value: strings.Join(strs, " ")}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (is *ItemProperties) UnmarshalXMLAttr(attr xml.Attr) error {
|
|
||||||
if is == nil {
|
|
||||||
is = &ItemProperties{}
|
|
||||||
}
|
|
||||||
for s := range strings.SplitSeq(attr.Value, " ") {
|
|
||||||
*is = append(*is, ItemProperty(s))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func encoderHelper(e *xml.Encoder, errPrefix ...string) func(
|
|
||||||
key string, value string, attrs ...xml.Attr,
|
|
||||||
) error {
|
|
||||||
if len(errPrefix) == 0 {
|
|
||||||
errPrefix[0] = ""
|
|
||||||
} else {
|
|
||||||
errPrefix[0] = fmt.Sprintf("%s: ", errPrefix[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(key string, value string, attrs ...xml.Attr) error {
|
|
||||||
err := e.EncodeElement(value, xml.StartElement{
|
|
||||||
Name: xml.Name{Local: key},
|
|
||||||
Attr: attrs,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%sfailed to encode %q: %w", errPrefix[0], key, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
module code.capytal.cc/capytal/comicverse/editor
|
|
||||||
|
|
||||||
go 1.25.2
|
|
||||||
|
|
||||||
require (
|
|
||||||
code.capytal.cc/loreddev/smalltrip v0.0.0-20251113171745-e3813daa807e
|
|
||||||
code.capytal.cc/loreddev/x v0.0.0-20251113171626-2ce5d71249c1
|
|
||||||
github.com/google/uuid v1.6.0
|
|
||||||
golang.org/x/net v0.47.0
|
|
||||||
golang.org/x/text v0.31.0
|
|
||||||
)
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
code.capytal.cc/loreddev/smalltrip v0.0.0-20251113171745-e3813daa807e h1:LdkirHDzhkcnhOBnDN0po84DjHAAkGztjHu/4mfWpSI=
|
|
||||||
code.capytal.cc/loreddev/smalltrip v0.0.0-20251113171745-e3813daa807e/go.mod h1:jMvSPUj295pTk/ixyxZfwZJE/RQ7DZzvQ3cVoAklkPA=
|
|
||||||
code.capytal.cc/loreddev/x v0.0.0-20251113171626-2ce5d71249c1 h1:BE0QdvwVVTG/t7nwNO5rrLf1vdAc5axv/1mWd/oAWhw=
|
|
||||||
code.capytal.cc/loreddev/x v0.0.0-20251113171626-2ce5d71249c1/go.mod h1:p5ZPHzutdbUDfpvNBCjv5ls6rM4YNl2k4ipD5b0aRho=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
|
||||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
|
||||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
Adjectives and names list files were copied from Dustin Kirkland's <dustin.kirkland@gmail.com>
|
|
||||||
petname project at Github, specifically from these files:
|
|
||||||
- https://github.com/dustinkirkland/petname/blob/5a79a2954e94ddbe6dc47b35c8b08ed931fc0e7f/usr/share/petname/small/adjectives.txt
|
|
||||||
- https://github.com/dustinkirkland/petname/blob/5a79a2954e94ddbe6dc47b35c8b08ed931fc0e7f/usr/share/petname/small/names.txt
|
|
||||||
|
|
||||||
The original files are provided and released under the Apache License version 2,
|
|
||||||
which a copy is available at
|
|
||||||
https://github.com/dustinkirkland/petname/blob/5a79a2954e94ddbe6dc47b35c8b08ed931fc0e7f/LICENSE
|
|
||||||
@@ -1,449 +0,0 @@
|
|||||||
able
|
|
||||||
above
|
|
||||||
absolute
|
|
||||||
accepted
|
|
||||||
accurate
|
|
||||||
ace
|
|
||||||
active
|
|
||||||
actual
|
|
||||||
adapted
|
|
||||||
adapting
|
|
||||||
adequate
|
|
||||||
adjusted
|
|
||||||
advanced
|
|
||||||
alert
|
|
||||||
alive
|
|
||||||
allowed
|
|
||||||
allowing
|
|
||||||
amazed
|
|
||||||
amazing
|
|
||||||
ample
|
|
||||||
amused
|
|
||||||
amusing
|
|
||||||
apparent
|
|
||||||
apt
|
|
||||||
arriving
|
|
||||||
artistic
|
|
||||||
assured
|
|
||||||
assuring
|
|
||||||
awaited
|
|
||||||
awake
|
|
||||||
aware
|
|
||||||
balanced
|
|
||||||
becoming
|
|
||||||
beloved
|
|
||||||
better
|
|
||||||
big
|
|
||||||
blessed
|
|
||||||
bold
|
|
||||||
boss
|
|
||||||
brave
|
|
||||||
brief
|
|
||||||
bright
|
|
||||||
bursting
|
|
||||||
busy
|
|
||||||
calm
|
|
||||||
capable
|
|
||||||
capital
|
|
||||||
careful
|
|
||||||
caring
|
|
||||||
casual
|
|
||||||
causal
|
|
||||||
central
|
|
||||||
certain
|
|
||||||
champion
|
|
||||||
charmed
|
|
||||||
charming
|
|
||||||
cheerful
|
|
||||||
chief
|
|
||||||
choice
|
|
||||||
civil
|
|
||||||
classic
|
|
||||||
clean
|
|
||||||
clear
|
|
||||||
clever
|
|
||||||
climbing
|
|
||||||
close
|
|
||||||
closing
|
|
||||||
coherent
|
|
||||||
comic
|
|
||||||
communal
|
|
||||||
complete
|
|
||||||
composed
|
|
||||||
concise
|
|
||||||
concrete
|
|
||||||
content
|
|
||||||
cool
|
|
||||||
correct
|
|
||||||
cosmic
|
|
||||||
crack
|
|
||||||
creative
|
|
||||||
credible
|
|
||||||
crisp
|
|
||||||
crucial
|
|
||||||
cuddly
|
|
||||||
cunning
|
|
||||||
curious
|
|
||||||
current
|
|
||||||
cute
|
|
||||||
daring
|
|
||||||
darling
|
|
||||||
dashing
|
|
||||||
dear
|
|
||||||
decent
|
|
||||||
deciding
|
|
||||||
deep
|
|
||||||
definite
|
|
||||||
delicate
|
|
||||||
desired
|
|
||||||
destined
|
|
||||||
devoted
|
|
||||||
direct
|
|
||||||
discrete
|
|
||||||
distinct
|
|
||||||
diverse
|
|
||||||
divine
|
|
||||||
dominant
|
|
||||||
driven
|
|
||||||
driving
|
|
||||||
dynamic
|
|
||||||
eager
|
|
||||||
easy
|
|
||||||
electric
|
|
||||||
elegant
|
|
||||||
emerging
|
|
||||||
eminent
|
|
||||||
enabled
|
|
||||||
enabling
|
|
||||||
endless
|
|
||||||
engaged
|
|
||||||
engaging
|
|
||||||
enhanced
|
|
||||||
enjoyed
|
|
||||||
enormous
|
|
||||||
enough
|
|
||||||
epic
|
|
||||||
equal
|
|
||||||
equipped
|
|
||||||
eternal
|
|
||||||
ethical
|
|
||||||
evident
|
|
||||||
evolved
|
|
||||||
evolving
|
|
||||||
exact
|
|
||||||
excited
|
|
||||||
exciting
|
|
||||||
exotic
|
|
||||||
expert
|
|
||||||
factual
|
|
||||||
fair
|
|
||||||
faithful
|
|
||||||
famous
|
|
||||||
fancy
|
|
||||||
fast
|
|
||||||
feasible
|
|
||||||
fine
|
|
||||||
finer
|
|
||||||
firm
|
|
||||||
first
|
|
||||||
fit
|
|
||||||
fitting
|
|
||||||
fleet
|
|
||||||
flexible
|
|
||||||
flowing
|
|
||||||
fluent
|
|
||||||
flying
|
|
||||||
fond
|
|
||||||
frank
|
|
||||||
free
|
|
||||||
fresh
|
|
||||||
full
|
|
||||||
fun
|
|
||||||
funky
|
|
||||||
funny
|
|
||||||
game
|
|
||||||
generous
|
|
||||||
gentle
|
|
||||||
genuine
|
|
||||||
giving
|
|
||||||
glad
|
|
||||||
glorious
|
|
||||||
glowing
|
|
||||||
golden
|
|
||||||
good
|
|
||||||
gorgeous
|
|
||||||
grand
|
|
||||||
grateful
|
|
||||||
great
|
|
||||||
growing
|
|
||||||
grown
|
|
||||||
guided
|
|
||||||
guiding
|
|
||||||
handy
|
|
||||||
happy
|
|
||||||
hardy
|
|
||||||
harmless
|
|
||||||
healthy
|
|
||||||
helped
|
|
||||||
helpful
|
|
||||||
helping
|
|
||||||
heroic
|
|
||||||
hip
|
|
||||||
holy
|
|
||||||
honest
|
|
||||||
hopeful
|
|
||||||
hot
|
|
||||||
huge
|
|
||||||
humane
|
|
||||||
humble
|
|
||||||
humorous
|
|
||||||
ideal
|
|
||||||
immense
|
|
||||||
immortal
|
|
||||||
immune
|
|
||||||
improved
|
|
||||||
in
|
|
||||||
included
|
|
||||||
infinite
|
|
||||||
informed
|
|
||||||
innocent
|
|
||||||
inspired
|
|
||||||
integral
|
|
||||||
intense
|
|
||||||
intent
|
|
||||||
internal
|
|
||||||
intimate
|
|
||||||
inviting
|
|
||||||
joint
|
|
||||||
just
|
|
||||||
keen
|
|
||||||
key
|
|
||||||
kind
|
|
||||||
knowing
|
|
||||||
known
|
|
||||||
large
|
|
||||||
lasting
|
|
||||||
leading
|
|
||||||
learning
|
|
||||||
legal
|
|
||||||
legible
|
|
||||||
lenient
|
|
||||||
liberal
|
|
||||||
light
|
|
||||||
liked
|
|
||||||
literate
|
|
||||||
live
|
|
||||||
living
|
|
||||||
logical
|
|
||||||
loved
|
|
||||||
loving
|
|
||||||
loyal
|
|
||||||
lucky
|
|
||||||
magical
|
|
||||||
magnetic
|
|
||||||
main
|
|
||||||
major
|
|
||||||
many
|
|
||||||
massive
|
|
||||||
master
|
|
||||||
mature
|
|
||||||
maximum
|
|
||||||
measured
|
|
||||||
meet
|
|
||||||
merry
|
|
||||||
mighty
|
|
||||||
mint
|
|
||||||
model
|
|
||||||
modern
|
|
||||||
modest
|
|
||||||
moral
|
|
||||||
more
|
|
||||||
moved
|
|
||||||
moving
|
|
||||||
musical
|
|
||||||
mutual
|
|
||||||
national
|
|
||||||
native
|
|
||||||
natural
|
|
||||||
nearby
|
|
||||||
neat
|
|
||||||
needed
|
|
||||||
neutral
|
|
||||||
new
|
|
||||||
next
|
|
||||||
nice
|
|
||||||
noble
|
|
||||||
normal
|
|
||||||
notable
|
|
||||||
noted
|
|
||||||
novel
|
|
||||||
obliging
|
|
||||||
on
|
|
||||||
one
|
|
||||||
open
|
|
||||||
optimal
|
|
||||||
optimum
|
|
||||||
organic
|
|
||||||
oriented
|
|
||||||
outgoing
|
|
||||||
patient
|
|
||||||
peaceful
|
|
||||||
perfect
|
|
||||||
pet
|
|
||||||
picked
|
|
||||||
pleasant
|
|
||||||
pleased
|
|
||||||
pleasing
|
|
||||||
poetic
|
|
||||||
polished
|
|
||||||
polite
|
|
||||||
popular
|
|
||||||
positive
|
|
||||||
possible
|
|
||||||
powerful
|
|
||||||
precious
|
|
||||||
precise
|
|
||||||
premium
|
|
||||||
prepared
|
|
||||||
present
|
|
||||||
pretty
|
|
||||||
primary
|
|
||||||
prime
|
|
||||||
pro
|
|
||||||
probable
|
|
||||||
profound
|
|
||||||
promoted
|
|
||||||
prompt
|
|
||||||
proper
|
|
||||||
proud
|
|
||||||
proven
|
|
||||||
pumped
|
|
||||||
pure
|
|
||||||
quality
|
|
||||||
quick
|
|
||||||
quiet
|
|
||||||
rapid
|
|
||||||
rare
|
|
||||||
rational
|
|
||||||
ready
|
|
||||||
real
|
|
||||||
refined
|
|
||||||
regular
|
|
||||||
related
|
|
||||||
relative
|
|
||||||
relaxed
|
|
||||||
relaxing
|
|
||||||
relevant
|
|
||||||
relieved
|
|
||||||
renewed
|
|
||||||
renewing
|
|
||||||
resolved
|
|
||||||
rested
|
|
||||||
rich
|
|
||||||
right
|
|
||||||
robust
|
|
||||||
romantic
|
|
||||||
ruling
|
|
||||||
sacred
|
|
||||||
safe
|
|
||||||
saved
|
|
||||||
saving
|
|
||||||
secure
|
|
||||||
select
|
|
||||||
selected
|
|
||||||
sensible
|
|
||||||
set
|
|
||||||
settled
|
|
||||||
settling
|
|
||||||
sharing
|
|
||||||
sharp
|
|
||||||
shining
|
|
||||||
simple
|
|
||||||
sincere
|
|
||||||
singular
|
|
||||||
skilled
|
|
||||||
smart
|
|
||||||
smashing
|
|
||||||
smiling
|
|
||||||
smooth
|
|
||||||
social
|
|
||||||
solid
|
|
||||||
sought
|
|
||||||
sound
|
|
||||||
special
|
|
||||||
splendid
|
|
||||||
square
|
|
||||||
stable
|
|
||||||
star
|
|
||||||
steady
|
|
||||||
sterling
|
|
||||||
still
|
|
||||||
stirred
|
|
||||||
stirring
|
|
||||||
striking
|
|
||||||
strong
|
|
||||||
stunning
|
|
||||||
subtle
|
|
||||||
suitable
|
|
||||||
suited
|
|
||||||
summary
|
|
||||||
sunny
|
|
||||||
super
|
|
||||||
superb
|
|
||||||
supreme
|
|
||||||
sure
|
|
||||||
sweeping
|
|
||||||
sweet
|
|
||||||
talented
|
|
||||||
teaching
|
|
||||||
tender
|
|
||||||
thankful
|
|
||||||
thorough
|
|
||||||
tidy
|
|
||||||
tight
|
|
||||||
together
|
|
||||||
tolerant
|
|
||||||
top
|
|
||||||
topical
|
|
||||||
tops
|
|
||||||
touched
|
|
||||||
touching
|
|
||||||
tough
|
|
||||||
true
|
|
||||||
trusted
|
|
||||||
trusting
|
|
||||||
trusty
|
|
||||||
ultimate
|
|
||||||
unbiased
|
|
||||||
uncommon
|
|
||||||
unified
|
|
||||||
unique
|
|
||||||
united
|
|
||||||
up
|
|
||||||
upright
|
|
||||||
upward
|
|
||||||
usable
|
|
||||||
useful
|
|
||||||
valid
|
|
||||||
valued
|
|
||||||
vast
|
|
||||||
verified
|
|
||||||
viable
|
|
||||||
vital
|
|
||||||
vocal
|
|
||||||
wanted
|
|
||||||
warm
|
|
||||||
wealthy
|
|
||||||
welcome
|
|
||||||
welcomed
|
|
||||||
well
|
|
||||||
whole
|
|
||||||
willing
|
|
||||||
winning
|
|
||||||
wired
|
|
||||||
wise
|
|
||||||
witty
|
|
||||||
wondrous
|
|
||||||
workable
|
|
||||||
working
|
|
||||||
worthy
|
|
||||||
@@ -1,452 +0,0 @@
|
|||||||
ox
|
|
||||||
ant
|
|
||||||
ape
|
|
||||||
asp
|
|
||||||
bat
|
|
||||||
bee
|
|
||||||
boa
|
|
||||||
bug
|
|
||||||
cat
|
|
||||||
cod
|
|
||||||
cow
|
|
||||||
cub
|
|
||||||
doe
|
|
||||||
dog
|
|
||||||
eel
|
|
||||||
eft
|
|
||||||
elf
|
|
||||||
elk
|
|
||||||
emu
|
|
||||||
ewe
|
|
||||||
fly
|
|
||||||
fox
|
|
||||||
gar
|
|
||||||
gnu
|
|
||||||
hen
|
|
||||||
hog
|
|
||||||
imp
|
|
||||||
jay
|
|
||||||
kid
|
|
||||||
kit
|
|
||||||
koi
|
|
||||||
lab
|
|
||||||
man
|
|
||||||
owl
|
|
||||||
pig
|
|
||||||
pug
|
|
||||||
pup
|
|
||||||
ram
|
|
||||||
rat
|
|
||||||
ray
|
|
||||||
yak
|
|
||||||
bass
|
|
||||||
bear
|
|
||||||
bird
|
|
||||||
boar
|
|
||||||
buck
|
|
||||||
bull
|
|
||||||
calf
|
|
||||||
chow
|
|
||||||
clam
|
|
||||||
colt
|
|
||||||
crab
|
|
||||||
crow
|
|
||||||
dane
|
|
||||||
deer
|
|
||||||
dodo
|
|
||||||
dory
|
|
||||||
dove
|
|
||||||
drum
|
|
||||||
duck
|
|
||||||
fawn
|
|
||||||
fish
|
|
||||||
flea
|
|
||||||
foal
|
|
||||||
fowl
|
|
||||||
frog
|
|
||||||
gnat
|
|
||||||
goat
|
|
||||||
grub
|
|
||||||
gull
|
|
||||||
hare
|
|
||||||
hawk
|
|
||||||
ibex
|
|
||||||
joey
|
|
||||||
kite
|
|
||||||
kiwi
|
|
||||||
lamb
|
|
||||||
lark
|
|
||||||
lion
|
|
||||||
loon
|
|
||||||
lynx
|
|
||||||
mako
|
|
||||||
mink
|
|
||||||
mite
|
|
||||||
mole
|
|
||||||
moth
|
|
||||||
mule
|
|
||||||
mutt
|
|
||||||
newt
|
|
||||||
orca
|
|
||||||
oryx
|
|
||||||
pika
|
|
||||||
pony
|
|
||||||
puma
|
|
||||||
seal
|
|
||||||
shad
|
|
||||||
slug
|
|
||||||
sole
|
|
||||||
stag
|
|
||||||
stud
|
|
||||||
swan
|
|
||||||
tahr
|
|
||||||
teal
|
|
||||||
tick
|
|
||||||
toad
|
|
||||||
tuna
|
|
||||||
wasp
|
|
||||||
wolf
|
|
||||||
worm
|
|
||||||
wren
|
|
||||||
yeti
|
|
||||||
adder
|
|
||||||
akita
|
|
||||||
alien
|
|
||||||
aphid
|
|
||||||
bison
|
|
||||||
boxer
|
|
||||||
bream
|
|
||||||
bunny
|
|
||||||
burro
|
|
||||||
camel
|
|
||||||
chimp
|
|
||||||
civet
|
|
||||||
cobra
|
|
||||||
coral
|
|
||||||
corgi
|
|
||||||
crane
|
|
||||||
dingo
|
|
||||||
drake
|
|
||||||
eagle
|
|
||||||
egret
|
|
||||||
filly
|
|
||||||
finch
|
|
||||||
gator
|
|
||||||
gecko
|
|
||||||
ghost
|
|
||||||
ghoul
|
|
||||||
goose
|
|
||||||
guppy
|
|
||||||
heron
|
|
||||||
hippo
|
|
||||||
horse
|
|
||||||
hound
|
|
||||||
husky
|
|
||||||
hyena
|
|
||||||
koala
|
|
||||||
krill
|
|
||||||
leech
|
|
||||||
lemur
|
|
||||||
liger
|
|
||||||
llama
|
|
||||||
louse
|
|
||||||
macaw
|
|
||||||
midge
|
|
||||||
molly
|
|
||||||
moose
|
|
||||||
moray
|
|
||||||
mouse
|
|
||||||
panda
|
|
||||||
perch
|
|
||||||
prawn
|
|
||||||
quail
|
|
||||||
racer
|
|
||||||
raven
|
|
||||||
rhino
|
|
||||||
robin
|
|
||||||
satyr
|
|
||||||
shark
|
|
||||||
sheep
|
|
||||||
shrew
|
|
||||||
skink
|
|
||||||
skunk
|
|
||||||
sloth
|
|
||||||
snail
|
|
||||||
snake
|
|
||||||
snipe
|
|
||||||
squid
|
|
||||||
stork
|
|
||||||
swift
|
|
||||||
tapir
|
|
||||||
tetra
|
|
||||||
tiger
|
|
||||||
troll
|
|
||||||
trout
|
|
||||||
viper
|
|
||||||
wahoo
|
|
||||||
whale
|
|
||||||
zebra
|
|
||||||
alpaca
|
|
||||||
amoeba
|
|
||||||
baboon
|
|
||||||
badger
|
|
||||||
beagle
|
|
||||||
bedbug
|
|
||||||
beetle
|
|
||||||
bengal
|
|
||||||
bobcat
|
|
||||||
caiman
|
|
||||||
cattle
|
|
||||||
cicada
|
|
||||||
collie
|
|
||||||
condor
|
|
||||||
cougar
|
|
||||||
coyote
|
|
||||||
dassie
|
|
||||||
dragon
|
|
||||||
earwig
|
|
||||||
falcon
|
|
||||||
feline
|
|
||||||
ferret
|
|
||||||
gannet
|
|
||||||
gibbon
|
|
||||||
glider
|
|
||||||
goblin
|
|
||||||
gopher
|
|
||||||
grouse
|
|
||||||
guinea
|
|
||||||
hermit
|
|
||||||
hornet
|
|
||||||
iguana
|
|
||||||
impala
|
|
||||||
insect
|
|
||||||
jackal
|
|
||||||
jaguar
|
|
||||||
jennet
|
|
||||||
kitten
|
|
||||||
kodiak
|
|
||||||
lizard
|
|
||||||
locust
|
|
||||||
maggot
|
|
||||||
magpie
|
|
||||||
mammal
|
|
||||||
mantis
|
|
||||||
marlin
|
|
||||||
marmot
|
|
||||||
marten
|
|
||||||
martin
|
|
||||||
mayfly
|
|
||||||
minnow
|
|
||||||
monkey
|
|
||||||
mullet
|
|
||||||
muskox
|
|
||||||
ocelot
|
|
||||||
oriole
|
|
||||||
osprey
|
|
||||||
oyster
|
|
||||||
parrot
|
|
||||||
pigeon
|
|
||||||
piglet
|
|
||||||
poodle
|
|
||||||
possum
|
|
||||||
python
|
|
||||||
quagga
|
|
||||||
rabbit
|
|
||||||
raptor
|
|
||||||
rodent
|
|
||||||
roughy
|
|
||||||
salmon
|
|
||||||
sawfly
|
|
||||||
serval
|
|
||||||
shiner
|
|
||||||
shrimp
|
|
||||||
spider
|
|
||||||
sponge
|
|
||||||
tarpon
|
|
||||||
thrush
|
|
||||||
tomcat
|
|
||||||
toucan
|
|
||||||
turkey
|
|
||||||
turtle
|
|
||||||
urchin
|
|
||||||
vervet
|
|
||||||
walrus
|
|
||||||
weasel
|
|
||||||
weevil
|
|
||||||
wombat
|
|
||||||
anchovy
|
|
||||||
anemone
|
|
||||||
bluejay
|
|
||||||
buffalo
|
|
||||||
bulldog
|
|
||||||
buzzard
|
|
||||||
caribou
|
|
||||||
catfish
|
|
||||||
chamois
|
|
||||||
cheetah
|
|
||||||
chicken
|
|
||||||
chigger
|
|
||||||
cowbird
|
|
||||||
crappie
|
|
||||||
crawdad
|
|
||||||
cricket
|
|
||||||
dogfish
|
|
||||||
dolphin
|
|
||||||
firefly
|
|
||||||
garfish
|
|
||||||
gazelle
|
|
||||||
gelding
|
|
||||||
giraffe
|
|
||||||
gobbler
|
|
||||||
gorilla
|
|
||||||
goshawk
|
|
||||||
grackle
|
|
||||||
griffon
|
|
||||||
grizzly
|
|
||||||
grouper
|
|
||||||
haddock
|
|
||||||
hagfish
|
|
||||||
halibut
|
|
||||||
hamster
|
|
||||||
herring
|
|
||||||
javelin
|
|
||||||
jawfish
|
|
||||||
jaybird
|
|
||||||
katydid
|
|
||||||
ladybug
|
|
||||||
lamprey
|
|
||||||
lemming
|
|
||||||
leopard
|
|
||||||
lioness
|
|
||||||
lobster
|
|
||||||
macaque
|
|
||||||
mallard
|
|
||||||
mammoth
|
|
||||||
manatee
|
|
||||||
mastiff
|
|
||||||
meerkat
|
|
||||||
mollusk
|
|
||||||
monarch
|
|
||||||
mongrel
|
|
||||||
monitor
|
|
||||||
monster
|
|
||||||
mudfish
|
|
||||||
muskrat
|
|
||||||
mustang
|
|
||||||
narwhal
|
|
||||||
oarfish
|
|
||||||
octopus
|
|
||||||
opossum
|
|
||||||
ostrich
|
|
||||||
panther
|
|
||||||
peacock
|
|
||||||
pegasus
|
|
||||||
pelican
|
|
||||||
penguin
|
|
||||||
phoenix
|
|
||||||
piranha
|
|
||||||
polecat
|
|
||||||
primate
|
|
||||||
quetzal
|
|
||||||
raccoon
|
|
||||||
rattler
|
|
||||||
redbird
|
|
||||||
redfish
|
|
||||||
reptile
|
|
||||||
rooster
|
|
||||||
sawfish
|
|
||||||
sculpin
|
|
||||||
seagull
|
|
||||||
skylark
|
|
||||||
snapper
|
|
||||||
spaniel
|
|
||||||
sparrow
|
|
||||||
sunbeam
|
|
||||||
sunbird
|
|
||||||
sunfish
|
|
||||||
tadpole
|
|
||||||
terrier
|
|
||||||
unicorn
|
|
||||||
vulture
|
|
||||||
wallaby
|
|
||||||
walleye
|
|
||||||
warthog
|
|
||||||
whippet
|
|
||||||
wildcat
|
|
||||||
aardvark
|
|
||||||
airedale
|
|
||||||
albacore
|
|
||||||
anteater
|
|
||||||
antelope
|
|
||||||
arachnid
|
|
||||||
barnacle
|
|
||||||
basilisk
|
|
||||||
blowfish
|
|
||||||
bluebird
|
|
||||||
bluegill
|
|
||||||
bonefish
|
|
||||||
bullfrog
|
|
||||||
cardinal
|
|
||||||
chipmunk
|
|
||||||
cockatoo
|
|
||||||
crayfish
|
|
||||||
dinosaur
|
|
||||||
doberman
|
|
||||||
duckling
|
|
||||||
elephant
|
|
||||||
escargot
|
|
||||||
flamingo
|
|
||||||
flounder
|
|
||||||
foxhound
|
|
||||||
glowworm
|
|
||||||
goldfish
|
|
||||||
grubworm
|
|
||||||
hedgehog
|
|
||||||
honeybee
|
|
||||||
hookworm
|
|
||||||
humpback
|
|
||||||
kangaroo
|
|
||||||
killdeer
|
|
||||||
kingfish
|
|
||||||
labrador
|
|
||||||
lacewing
|
|
||||||
ladybird
|
|
||||||
lionfish
|
|
||||||
longhorn
|
|
||||||
mackerel
|
|
||||||
malamute
|
|
||||||
marmoset
|
|
||||||
mastodon
|
|
||||||
moccasin
|
|
||||||
mongoose
|
|
||||||
monkfish
|
|
||||||
mosquito
|
|
||||||
pangolin
|
|
||||||
parakeet
|
|
||||||
pheasant
|
|
||||||
pipefish
|
|
||||||
platypus
|
|
||||||
polliwog
|
|
||||||
porpoise
|
|
||||||
reindeer
|
|
||||||
ringtail
|
|
||||||
sailfish
|
|
||||||
scorpion
|
|
||||||
seahorse
|
|
||||||
seasnail
|
|
||||||
sheepdog
|
|
||||||
shepherd
|
|
||||||
silkworm
|
|
||||||
squirrel
|
|
||||||
stallion
|
|
||||||
starfish
|
|
||||||
starling
|
|
||||||
stingray
|
|
||||||
stinkbug
|
|
||||||
sturgeon
|
|
||||||
terrapin
|
|
||||||
titmouse
|
|
||||||
tortoise
|
|
||||||
treefrog
|
|
||||||
werewolf
|
|
||||||
woodcock
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package randname
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: Make generator be based on fantasy, sci-fi and other literature
|
|
||||||
// and artistic names.
|
|
||||||
|
|
||||||
//go:embed adjectives.txt
|
|
||||||
var adjectives string
|
|
||||||
|
|
||||||
//go:embed names.txt
|
|
||||||
var names string
|
|
||||||
|
|
||||||
var (
|
|
||||||
adjectivesList = strings.Split(adjectives, "\n")
|
|
||||||
namesList = strings.Split(names, "\n")
|
|
||||||
)
|
|
||||||
|
|
||||||
func New(sep ...string) string {
|
|
||||||
if len(sep) == 0 {
|
|
||||||
sep = append(sep, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
a := adjectivesList[rand.Intn(len(adjectivesList))]
|
|
||||||
n := namesList[rand.Intn(len(namesList))]
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s%s%s", a, sep[0], n)
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"code.capytal.cc/capytal/comicverse/editor"
|
|
||||||
"code.capytal.cc/capytal/comicverse/editor/internals/randname"
|
|
||||||
"code.capytal.cc/loreddev/smalltrip/problem"
|
|
||||||
"code.capytal.cc/loreddev/x/xtemplate"
|
|
||||||
)
|
|
||||||
|
|
||||||
type dashboardController struct {
|
|
||||||
editor *editor.Editor
|
|
||||||
templater xtemplate.Templater
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctrl *dashboardController) dashboard(w http.ResponseWriter, r *http.Request) {
|
|
||||||
randtitle := randname.New()
|
|
||||||
|
|
||||||
err := ctrl.templater.ExecuteTemplate(w, "editor-dashboard", map[string]any{
|
|
||||||
"RandTitle": randtitle,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
problem.NewInternalServerError(err).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"code.capytal.cc/capytal/comicverse/editor"
|
|
||||||
"code.capytal.cc/capytal/comicverse/editor/internals/randname"
|
|
||||||
"code.capytal.cc/loreddev/smalltrip/problem"
|
|
||||||
"code.capytal.cc/loreddev/x/xtemplate"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"golang.org/x/text/language"
|
|
||||||
)
|
|
||||||
|
|
||||||
type publicationController struct {
|
|
||||||
editor *editor.Editor
|
|
||||||
templater xtemplate.Templater
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctrl *publicationController) createPublication(w http.ResponseWriter, r *http.Request) {
|
|
||||||
title := r.FormValue("title")
|
|
||||||
if title == "" {
|
|
||||||
title = randname.New()
|
|
||||||
}
|
|
||||||
|
|
||||||
lang := language.English
|
|
||||||
|
|
||||||
id, err := uuid.NewV7()
|
|
||||||
if err != nil {
|
|
||||||
problem.NewInternalServerError(err).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = ctrl.editor.New(id, title, lang)
|
|
||||||
if err != nil {
|
|
||||||
problem.NewInternalServerError(err).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Redirect(w, r, fmt.Sprintf("./%s", id), http.StatusTemporaryRedirect)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctrl *publicationController) getPublication(w http.ResponseWriter, r *http.Request) {
|
|
||||||
idstr := r.PathValue("publicationID")
|
|
||||||
if idstr == "" {
|
|
||||||
problem.NewBadRequest("Missing publication ID in path").ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := uuid.Parse(idstr)
|
|
||||||
if err != nil {
|
|
||||||
problem.NewBadRequest("Invalid UUID in path", problem.WithError(err)).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pkg, err := ctrl.editor.Open(id)
|
|
||||||
if errors.Is(err, editor.ErrNotExists) {
|
|
||||||
problem.NewNotFound().ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
} else if err != nil {
|
|
||||||
problem.NewInternalServerError(err).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write(fmt.Appendf([]byte{}, "%+v", pkg))
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/fs"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"code.capytal.cc/capytal/comicverse/editor"
|
|
||||||
"code.capytal.cc/loreddev/smalltrip"
|
|
||||||
"code.capytal.cc/loreddev/smalltrip/middleware"
|
|
||||||
"code.capytal.cc/loreddev/smalltrip/multiplexer"
|
|
||||||
"code.capytal.cc/loreddev/x/xtemplate"
|
|
||||||
)
|
|
||||||
|
|
||||||
func New(cfg Config) http.Handler {
|
|
||||||
log := cfg.Logger
|
|
||||||
|
|
||||||
mux := multiplexer.New()
|
|
||||||
mux = multiplexer.WithFormMethod(mux, "x-method")
|
|
||||||
mux = multiplexer.WithPatternRules(mux,
|
|
||||||
multiplexer.EnsureMethod(),
|
|
||||||
multiplexer.EnsureTrailingSlash(),
|
|
||||||
multiplexer.EnsureStrictEnd(),
|
|
||||||
)
|
|
||||||
|
|
||||||
r := smalltrip.NewRouter(
|
|
||||||
smalltrip.WithMultiplexer(mux),
|
|
||||||
smalltrip.WithLogger(log.WithGroup("router")),
|
|
||||||
)
|
|
||||||
|
|
||||||
r.Use(middleware.Logger(log.WithGroup("requests")))
|
|
||||||
// r.Use(problem.Middleware(problem.DefaultHandler))
|
|
||||||
|
|
||||||
r.Handle("GET /assets/{asset...}", http.StripPrefix("/assets/", http.FileServerFS(cfg.Assets)))
|
|
||||||
|
|
||||||
dashboardCtrl := &dashboardController{editor: cfg.Editor, templater: cfg.Templater}
|
|
||||||
publicationCtrl := &publicationController{editor: cfg.Editor, templater: cfg.Templater}
|
|
||||||
|
|
||||||
r.HandleFunc("GET /{$}", dashboardCtrl.dashboard)
|
|
||||||
|
|
||||||
r.HandleFunc("POST /publication/{$}", publicationCtrl.createPublication)
|
|
||||||
r.HandleFunc("GET /publication/{publicationID}/{$}", publicationCtrl.getPublication)
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Assets fs.FS
|
|
||||||
Editor *editor.Editor
|
|
||||||
Templater xtemplate.Templater
|
|
||||||
Logger *slog.Logger
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/fs"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Newlocal(
|
|
||||||
root *os.Root,
|
|
||||||
logger *slog.Logger,
|
|
||||||
) Storage {
|
|
||||||
return &local{
|
|
||||||
log: logger,
|
|
||||||
root: root,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type local struct {
|
|
||||||
log *slog.Logger
|
|
||||||
root *os.Root
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Storage = (*local)(nil)
|
|
||||||
|
|
||||||
func (files *local) Exists(p string) bool {
|
|
||||||
if _, err := files.root.Stat(p); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (files *local) Open(p string) (fs.File, error) {
|
|
||||||
log := files.log.With(
|
|
||||||
slog.String("path", p),
|
|
||||||
slog.String("root", files.root.Name()))
|
|
||||||
|
|
||||||
log.Debug("Opening file")
|
|
||||||
defer log.Debug("File opened")
|
|
||||||
|
|
||||||
f, err := files.root.Open(p)
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
return nil, ErrNotExists
|
|
||||||
} else if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return f, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (files *local) Write(p string, d []byte) (int, error) {
|
|
||||||
log := files.log.With(
|
|
||||||
slog.String("path", p),
|
|
||||||
slog.String("root", files.root.Name()))
|
|
||||||
|
|
||||||
log.Debug("Writing file")
|
|
||||||
defer log.Debug("File wrote")
|
|
||||||
|
|
||||||
if err := files.root.MkdirAll(path.Dir(p), os.ModePerm); err != nil {
|
|
||||||
return 0, fmt.Errorf("file.local: failed to create parent directories %q: %w", path.Dir(p), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := files.root.WriteFile(p, d, os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("file.local: failed to write file %q: %w", p, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(d), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (files *local) WriteFrom(p string, r io.Reader) (int64, error) {
|
|
||||||
log := files.log.With(
|
|
||||||
slog.String("path", p),
|
|
||||||
slog.String("root", files.root.Name()))
|
|
||||||
|
|
||||||
log.Debug("Writing file")
|
|
||||||
defer log.Debug("File wrote")
|
|
||||||
|
|
||||||
if err := files.root.MkdirAll(path.Dir(p), os.ModePerm); err != nil {
|
|
||||||
return 0, fmt.Errorf("file.local: failed to create parent directories %q: %w", path.Dir(p), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := files.root.Create(p)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("file.local: failed to create file %q: %w", p, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := f.ReadFrom(r)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("file.local: failed to write file %q: %w", p, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Storage interface {
|
|
||||||
Exists(p string) bool
|
|
||||||
Open(p string) (fs.File, error)
|
|
||||||
Write(p string, b []byte) (int, error)
|
|
||||||
WriteFrom(p string, r io.Reader) (int64, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type withRoot struct {
|
|
||||||
root string
|
|
||||||
Storage
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithRoot(rootDir string, s Storage) Storage {
|
|
||||||
return &withRoot{root: rootDir, Storage: s}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *withRoot) Exists(p string) bool {
|
|
||||||
return f.Storage.Exists(path.Join(f.root, p))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *withRoot) Open(p string) (fs.File, error) {
|
|
||||||
return f.Storage.Open(path.Join(f.root, p))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *withRoot) Write(p string, b []byte) (int, error) {
|
|
||||||
return f.Storage.Write(path.Join(f.root, p), b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *withRoot) WriteFrom(p string, r io.Reader) (int64, error) {
|
|
||||||
return f.Storage.WriteFrom(path.Join(f.root, p), r)
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrNotExists = os.ErrNotExist
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{{define "editor-dashboard"}} {{template "layout-base"}}
|
|
||||||
<body class="bg-gray-900 text-gray-50 has-[#first-publication]:h-svw">
|
|
||||||
<main class="has-[#first-publication]:h-full flex flex-col">
|
|
||||||
{{if .Publications}}
|
|
||||||
<p>Publications</p>
|
|
||||||
{{else}}
|
|
||||||
<h1>Create your first publication</h1>
|
|
||||||
<form method="post" action="/publication/" id="first-publication">
|
|
||||||
<input
|
|
||||||
id="title"
|
|
||||||
type="text"
|
|
||||||
name="title"
|
|
||||||
value="{{if .RandTitle}}{{.RandTitle}}{{end}}"
|
|
||||||
/><button type="submit">Create</button>
|
|
||||||
</form>
|
|
||||||
<style>
|
|
||||||
body:has(:is(#first-publication)) {
|
|
||||||
height: 100svh;
|
|
||||||
& > main {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{{end}}
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
{{template "layout-base-end"}} {{end}}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{{define "layout-base"}}
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
{{if .Title}}
|
|
||||||
<title>{{.Title}}</title>
|
|
||||||
{{end}}
|
|
||||||
<link href="/assets/css/style.css" rel="stylesheet" />
|
|
||||||
<script
|
|
||||||
src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.js"
|
|
||||||
integrity="sha384-oeUn82QNXPuVkGCkcrInrS1twIxKhkZiFfr2TdiuObZ3n3yIeMiqcRzkIcguaof1"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
></script>
|
|
||||||
</head>
|
|
||||||
{{end}} {{define "layout-base-end"}}
|
|
||||||
</html>
|
|
||||||
{{end}}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package template
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"html/template"
|
|
||||||
"io/fs"
|
|
||||||
|
|
||||||
"code.capytal.cc/loreddev/x/xtemplate"
|
|
||||||
)
|
|
||||||
|
|
||||||
func New() (xtemplate.Template, error) {
|
|
||||||
return xtemplate.New[template.Template]("template").
|
|
||||||
Funcs(functions).
|
|
||||||
ParseFS(embedded, patterns...)
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:embed *.html layouts/*.html
|
|
||||||
var embedded embed.FS
|
|
||||||
|
|
||||||
func Dev(dir fs.FS) (xtemplate.Template, error) {
|
|
||||||
return xtemplate.NewHot[template.Template]("template").
|
|
||||||
Funcs(functions).
|
|
||||||
ParseFS(dir, patterns...)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
patterns = []string{"*.html", "layouts/*.html"}
|
|
||||||
functions = template.FuncMap{
|
|
||||||
"args": func(pairs ...any) (map[string]any, error) {
|
|
||||||
if len(pairs)%2 != 0 {
|
|
||||||
return nil, errors.New("misaligned map in template arguments")
|
|
||||||
}
|
|
||||||
|
|
||||||
m := make(map[string]any, len(pairs)/2)
|
|
||||||
|
|
||||||
for i := 0; i < len(pairs); i += 2 {
|
|
||||||
key, ok := pairs[i].(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("cannot use type %T as map key", pairs[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
m[key] = pairs[i+1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return m, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1762844143,
|
"lastModified": 1742069588,
|
||||||
"narHash": "sha256-SlybxLZ1/e4T2lb1czEtWVzDCVSTvk9WLwGhmxFmBxI=",
|
"narHash": "sha256-C7jVfohcGzdZRF6DO+ybyG/sqpo1h6bZi9T56sxLy+k=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "9da7f1cf7f8a6e2a7cb3001b048546c92a8258b4",
|
"rev": "c80f6a7e10b39afcc1894e02ef785b1ad0b0d7e5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
# Go tools
|
# Go tools
|
||||||
go_1_25
|
go
|
||||||
golangci-lint
|
golangci-lint
|
||||||
gofumpt
|
gofumpt
|
||||||
gotools
|
gotools
|
||||||
|
|||||||
5
go.mod
5
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module code.capytal.cc/capytal/comicverse
|
module code.capytal.cc/capytal/comicverse
|
||||||
|
|
||||||
go 1.25.2
|
go 1.24.8
|
||||||
|
|
||||||
require (
|
require (
|
||||||
code.capytal.cc/loreddev/smalltrip v0.0.0-20251013182121-3d201d21226c
|
code.capytal.cc/loreddev/smalltrip v0.0.0-20251013182121-3d201d21226c
|
||||||
@@ -10,7 +10,7 @@ require (
|
|||||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/tursodatabase/go-libsql v0.0.0-20241221181756-6121e81fbf92
|
github.com/tursodatabase/go-libsql v0.0.0-20241221181756-6121e81fbf92
|
||||||
golang.org/x/crypto v0.43.0
|
golang.org/x/crypto v0.38.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -26,5 +26,4 @@ require (
|
|||||||
github.com/aws/smithy-go v1.22.2 // indirect
|
github.com/aws/smithy-go v1.22.2 // indirect
|
||||||
github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 // indirect
|
github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
|
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
|
||||||
golang.org/x/sync v0.17.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -38,9 +38,11 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/tursodatabase/go-libsql v0.0.0-20241221181756-6121e81fbf92 h1:IYI1S1xt4WdQHjgVYzMa+Owot82BqlZfQV05BLnTcTA=
|
github.com/tursodatabase/go-libsql v0.0.0-20241221181756-6121e81fbf92 h1:IYI1S1xt4WdQHjgVYzMa+Owot82BqlZfQV05BLnTcTA=
|
||||||
github.com/tursodatabase/go-libsql v0.0.0-20241221181756-6121e81fbf92/go.mod h1:TjsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8=
|
github.com/tursodatabase/go-libsql v0.0.0-20241221181756-6121e81fbf92/go.mod h1:TjsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8=
|
||||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||||
|
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU=
|
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU=
|
||||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
|
|||||||
5
go.work
5
go.work
@@ -1,8 +1,7 @@
|
|||||||
go 1.25.2
|
go 1.24.8
|
||||||
|
|
||||||
use (
|
use (
|
||||||
.
|
./.
|
||||||
./editor
|
|
||||||
./smalltrip
|
./smalltrip
|
||||||
./x
|
./x
|
||||||
)
|
)
|
||||||
|
|||||||
49
go.work.sum
49
go.work.sum
@@ -1,16 +1,47 @@
|
|||||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
||||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.6.2 h1:t/gZFyrijKuSU0elA5kRngP/oU3mc0I+Dvp8HwRE4c0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.6.2/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.1 h1:1M0gSbyP6q06gl3384wpoKPaH9G16NPqZFieEhLboSU=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.1/go.mod h1:4qzsZSzB/KiX2EzDjs9D7A8rI/WGJxZceVJIHqtJjIU=
|
||||||
|
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
|
||||||
|
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 h1:JLvn7D+wXjH9g4Jsjo+VqmzTUpl/LX7vfr6VOfSWTdM=
|
||||||
|
github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06/go.mod h1:FUkZ5OHjlGPjnM2UyGJz9TypXQFgYqw6AFNO1UiROTM=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/tursodatabase/go-libsql v0.0.0-20241221181756-6121e81fbf92 h1:IYI1S1xt4WdQHjgVYzMa+Owot82BqlZfQV05BLnTcTA=
|
||||||
|
github.com/tursodatabase/go-libsql v0.0.0-20241221181756-6121e81fbf92/go.mod h1:TjsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8=
|
||||||
|
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||||
|
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||||
|
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU=
|
||||||
|
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||||
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
|
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
|
||||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
||||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
|
||||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
|
||||||
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
|
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
|
||||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
Adjectives and names list files were copied from Dustin Kirkland's <dustin.kirkland@gmail.com>
|
|
||||||
petname project at Github, specifically from these files:
|
|
||||||
- https://github.com/dustinkirkland/petname/blob/5a79a2954e94ddbe6dc47b35c8b08ed931fc0e7f/usr/share/petname/small/adjectives.txt
|
|
||||||
- https://github.com/dustinkirkland/petname/blob/5a79a2954e94ddbe6dc47b35c8b08ed931fc0e7f/usr/share/petname/small/names.txt
|
|
||||||
|
|
||||||
The original files are provided and released under the Apache License version 2,
|
|
||||||
which a copy is available at
|
|
||||||
https://github.com/dustinkirkland/petname/blob/5a79a2954e94ddbe6dc47b35c8b08ed931fc0e7f/LICENSE
|
|
||||||
@@ -1,452 +0,0 @@
|
|||||||
ox
|
|
||||||
ant
|
|
||||||
ape
|
|
||||||
asp
|
|
||||||
bat
|
|
||||||
bee
|
|
||||||
boa
|
|
||||||
bug
|
|
||||||
cat
|
|
||||||
cod
|
|
||||||
cow
|
|
||||||
cub
|
|
||||||
doe
|
|
||||||
dog
|
|
||||||
eel
|
|
||||||
eft
|
|
||||||
elf
|
|
||||||
elk
|
|
||||||
emu
|
|
||||||
ewe
|
|
||||||
fly
|
|
||||||
fox
|
|
||||||
gar
|
|
||||||
gnu
|
|
||||||
hen
|
|
||||||
hog
|
|
||||||
imp
|
|
||||||
jay
|
|
||||||
kid
|
|
||||||
kit
|
|
||||||
koi
|
|
||||||
lab
|
|
||||||
man
|
|
||||||
owl
|
|
||||||
pig
|
|
||||||
pug
|
|
||||||
pup
|
|
||||||
ram
|
|
||||||
rat
|
|
||||||
ray
|
|
||||||
yak
|
|
||||||
bass
|
|
||||||
bear
|
|
||||||
bird
|
|
||||||
boar
|
|
||||||
buck
|
|
||||||
bull
|
|
||||||
calf
|
|
||||||
chow
|
|
||||||
clam
|
|
||||||
colt
|
|
||||||
crab
|
|
||||||
crow
|
|
||||||
dane
|
|
||||||
deer
|
|
||||||
dodo
|
|
||||||
dory
|
|
||||||
dove
|
|
||||||
drum
|
|
||||||
duck
|
|
||||||
fawn
|
|
||||||
fish
|
|
||||||
flea
|
|
||||||
foal
|
|
||||||
fowl
|
|
||||||
frog
|
|
||||||
gnat
|
|
||||||
goat
|
|
||||||
grub
|
|
||||||
gull
|
|
||||||
hare
|
|
||||||
hawk
|
|
||||||
ibex
|
|
||||||
joey
|
|
||||||
kite
|
|
||||||
kiwi
|
|
||||||
lamb
|
|
||||||
lark
|
|
||||||
lion
|
|
||||||
loon
|
|
||||||
lynx
|
|
||||||
mako
|
|
||||||
mink
|
|
||||||
mite
|
|
||||||
mole
|
|
||||||
moth
|
|
||||||
mule
|
|
||||||
mutt
|
|
||||||
newt
|
|
||||||
orca
|
|
||||||
oryx
|
|
||||||
pika
|
|
||||||
pony
|
|
||||||
puma
|
|
||||||
seal
|
|
||||||
shad
|
|
||||||
slug
|
|
||||||
sole
|
|
||||||
stag
|
|
||||||
stud
|
|
||||||
swan
|
|
||||||
tahr
|
|
||||||
teal
|
|
||||||
tick
|
|
||||||
toad
|
|
||||||
tuna
|
|
||||||
wasp
|
|
||||||
wolf
|
|
||||||
worm
|
|
||||||
wren
|
|
||||||
yeti
|
|
||||||
adder
|
|
||||||
akita
|
|
||||||
alien
|
|
||||||
aphid
|
|
||||||
bison
|
|
||||||
boxer
|
|
||||||
bream
|
|
||||||
bunny
|
|
||||||
burro
|
|
||||||
camel
|
|
||||||
chimp
|
|
||||||
civet
|
|
||||||
cobra
|
|
||||||
coral
|
|
||||||
corgi
|
|
||||||
crane
|
|
||||||
dingo
|
|
||||||
drake
|
|
||||||
eagle
|
|
||||||
egret
|
|
||||||
filly
|
|
||||||
finch
|
|
||||||
gator
|
|
||||||
gecko
|
|
||||||
ghost
|
|
||||||
ghoul
|
|
||||||
goose
|
|
||||||
guppy
|
|
||||||
heron
|
|
||||||
hippo
|
|
||||||
horse
|
|
||||||
hound
|
|
||||||
husky
|
|
||||||
hyena
|
|
||||||
koala
|
|
||||||
krill
|
|
||||||
leech
|
|
||||||
lemur
|
|
||||||
liger
|
|
||||||
llama
|
|
||||||
louse
|
|
||||||
macaw
|
|
||||||
midge
|
|
||||||
molly
|
|
||||||
moose
|
|
||||||
moray
|
|
||||||
mouse
|
|
||||||
panda
|
|
||||||
perch
|
|
||||||
prawn
|
|
||||||
quail
|
|
||||||
racer
|
|
||||||
raven
|
|
||||||
rhino
|
|
||||||
robin
|
|
||||||
satyr
|
|
||||||
shark
|
|
||||||
sheep
|
|
||||||
shrew
|
|
||||||
skink
|
|
||||||
skunk
|
|
||||||
sloth
|
|
||||||
snail
|
|
||||||
snake
|
|
||||||
snipe
|
|
||||||
squid
|
|
||||||
stork
|
|
||||||
swift
|
|
||||||
tapir
|
|
||||||
tetra
|
|
||||||
tiger
|
|
||||||
troll
|
|
||||||
trout
|
|
||||||
viper
|
|
||||||
wahoo
|
|
||||||
whale
|
|
||||||
zebra
|
|
||||||
alpaca
|
|
||||||
amoeba
|
|
||||||
baboon
|
|
||||||
badger
|
|
||||||
beagle
|
|
||||||
bedbug
|
|
||||||
beetle
|
|
||||||
bengal
|
|
||||||
bobcat
|
|
||||||
caiman
|
|
||||||
cattle
|
|
||||||
cicada
|
|
||||||
collie
|
|
||||||
condor
|
|
||||||
cougar
|
|
||||||
coyote
|
|
||||||
dassie
|
|
||||||
dragon
|
|
||||||
earwig
|
|
||||||
falcon
|
|
||||||
feline
|
|
||||||
ferret
|
|
||||||
gannet
|
|
||||||
gibbon
|
|
||||||
glider
|
|
||||||
goblin
|
|
||||||
gopher
|
|
||||||
grouse
|
|
||||||
guinea
|
|
||||||
hermit
|
|
||||||
hornet
|
|
||||||
iguana
|
|
||||||
impala
|
|
||||||
insect
|
|
||||||
jackal
|
|
||||||
jaguar
|
|
||||||
jennet
|
|
||||||
kitten
|
|
||||||
kodiak
|
|
||||||
lizard
|
|
||||||
locust
|
|
||||||
maggot
|
|
||||||
magpie
|
|
||||||
mammal
|
|
||||||
mantis
|
|
||||||
marlin
|
|
||||||
marmot
|
|
||||||
marten
|
|
||||||
martin
|
|
||||||
mayfly
|
|
||||||
minnow
|
|
||||||
monkey
|
|
||||||
mullet
|
|
||||||
muskox
|
|
||||||
ocelot
|
|
||||||
oriole
|
|
||||||
osprey
|
|
||||||
oyster
|
|
||||||
parrot
|
|
||||||
pigeon
|
|
||||||
piglet
|
|
||||||
poodle
|
|
||||||
possum
|
|
||||||
python
|
|
||||||
quagga
|
|
||||||
rabbit
|
|
||||||
raptor
|
|
||||||
rodent
|
|
||||||
roughy
|
|
||||||
salmon
|
|
||||||
sawfly
|
|
||||||
serval
|
|
||||||
shiner
|
|
||||||
shrimp
|
|
||||||
spider
|
|
||||||
sponge
|
|
||||||
tarpon
|
|
||||||
thrush
|
|
||||||
tomcat
|
|
||||||
toucan
|
|
||||||
turkey
|
|
||||||
turtle
|
|
||||||
urchin
|
|
||||||
vervet
|
|
||||||
walrus
|
|
||||||
weasel
|
|
||||||
weevil
|
|
||||||
wombat
|
|
||||||
anchovy
|
|
||||||
anemone
|
|
||||||
bluejay
|
|
||||||
buffalo
|
|
||||||
bulldog
|
|
||||||
buzzard
|
|
||||||
caribou
|
|
||||||
catfish
|
|
||||||
chamois
|
|
||||||
cheetah
|
|
||||||
chicken
|
|
||||||
chigger
|
|
||||||
cowbird
|
|
||||||
crappie
|
|
||||||
crawdad
|
|
||||||
cricket
|
|
||||||
dogfish
|
|
||||||
dolphin
|
|
||||||
firefly
|
|
||||||
garfish
|
|
||||||
gazelle
|
|
||||||
gelding
|
|
||||||
giraffe
|
|
||||||
gobbler
|
|
||||||
gorilla
|
|
||||||
goshawk
|
|
||||||
grackle
|
|
||||||
griffon
|
|
||||||
grizzly
|
|
||||||
grouper
|
|
||||||
haddock
|
|
||||||
hagfish
|
|
||||||
halibut
|
|
||||||
hamster
|
|
||||||
herring
|
|
||||||
javelin
|
|
||||||
jawfish
|
|
||||||
jaybird
|
|
||||||
katydid
|
|
||||||
ladybug
|
|
||||||
lamprey
|
|
||||||
lemming
|
|
||||||
leopard
|
|
||||||
lioness
|
|
||||||
lobster
|
|
||||||
macaque
|
|
||||||
mallard
|
|
||||||
mammoth
|
|
||||||
manatee
|
|
||||||
mastiff
|
|
||||||
meerkat
|
|
||||||
mollusk
|
|
||||||
monarch
|
|
||||||
mongrel
|
|
||||||
monitor
|
|
||||||
monster
|
|
||||||
mudfish
|
|
||||||
muskrat
|
|
||||||
mustang
|
|
||||||
narwhal
|
|
||||||
oarfish
|
|
||||||
octopus
|
|
||||||
opossum
|
|
||||||
ostrich
|
|
||||||
panther
|
|
||||||
peacock
|
|
||||||
pegasus
|
|
||||||
pelican
|
|
||||||
penguin
|
|
||||||
phoenix
|
|
||||||
piranha
|
|
||||||
polecat
|
|
||||||
primate
|
|
||||||
quetzal
|
|
||||||
raccoon
|
|
||||||
rattler
|
|
||||||
redbird
|
|
||||||
redfish
|
|
||||||
reptile
|
|
||||||
rooster
|
|
||||||
sawfish
|
|
||||||
sculpin
|
|
||||||
seagull
|
|
||||||
skylark
|
|
||||||
snapper
|
|
||||||
spaniel
|
|
||||||
sparrow
|
|
||||||
sunbeam
|
|
||||||
sunbird
|
|
||||||
sunfish
|
|
||||||
tadpole
|
|
||||||
terrier
|
|
||||||
unicorn
|
|
||||||
vulture
|
|
||||||
wallaby
|
|
||||||
walleye
|
|
||||||
warthog
|
|
||||||
whippet
|
|
||||||
wildcat
|
|
||||||
aardvark
|
|
||||||
airedale
|
|
||||||
albacore
|
|
||||||
anteater
|
|
||||||
antelope
|
|
||||||
arachnid
|
|
||||||
barnacle
|
|
||||||
basilisk
|
|
||||||
blowfish
|
|
||||||
bluebird
|
|
||||||
bluegill
|
|
||||||
bonefish
|
|
||||||
bullfrog
|
|
||||||
cardinal
|
|
||||||
chipmunk
|
|
||||||
cockatoo
|
|
||||||
crayfish
|
|
||||||
dinosaur
|
|
||||||
doberman
|
|
||||||
duckling
|
|
||||||
elephant
|
|
||||||
escargot
|
|
||||||
flamingo
|
|
||||||
flounder
|
|
||||||
foxhound
|
|
||||||
glowworm
|
|
||||||
goldfish
|
|
||||||
grubworm
|
|
||||||
hedgehog
|
|
||||||
honeybee
|
|
||||||
hookworm
|
|
||||||
humpback
|
|
||||||
kangaroo
|
|
||||||
killdeer
|
|
||||||
kingfish
|
|
||||||
labrador
|
|
||||||
lacewing
|
|
||||||
ladybird
|
|
||||||
lionfish
|
|
||||||
longhorn
|
|
||||||
mackerel
|
|
||||||
malamute
|
|
||||||
marmoset
|
|
||||||
mastodon
|
|
||||||
moccasin
|
|
||||||
mongoose
|
|
||||||
monkfish
|
|
||||||
mosquito
|
|
||||||
pangolin
|
|
||||||
parakeet
|
|
||||||
pheasant
|
|
||||||
pipefish
|
|
||||||
platypus
|
|
||||||
polliwog
|
|
||||||
porpoise
|
|
||||||
reindeer
|
|
||||||
ringtail
|
|
||||||
sailfish
|
|
||||||
scorpion
|
|
||||||
seahorse
|
|
||||||
seasnail
|
|
||||||
sheepdog
|
|
||||||
shepherd
|
|
||||||
silkworm
|
|
||||||
squirrel
|
|
||||||
stallion
|
|
||||||
starfish
|
|
||||||
starling
|
|
||||||
stingray
|
|
||||||
stinkbug
|
|
||||||
sturgeon
|
|
||||||
terrapin
|
|
||||||
titmouse
|
|
||||||
tortoise
|
|
||||||
treefrog
|
|
||||||
werewolf
|
|
||||||
woodcock
|
|
||||||
@@ -1,449 +0,0 @@
|
|||||||
able
|
|
||||||
above
|
|
||||||
absolute
|
|
||||||
accepted
|
|
||||||
accurate
|
|
||||||
ace
|
|
||||||
active
|
|
||||||
actual
|
|
||||||
adapted
|
|
||||||
adapting
|
|
||||||
adequate
|
|
||||||
adjusted
|
|
||||||
advanced
|
|
||||||
alert
|
|
||||||
alive
|
|
||||||
allowed
|
|
||||||
allowing
|
|
||||||
amazed
|
|
||||||
amazing
|
|
||||||
ample
|
|
||||||
amused
|
|
||||||
amusing
|
|
||||||
apparent
|
|
||||||
apt
|
|
||||||
arriving
|
|
||||||
artistic
|
|
||||||
assured
|
|
||||||
assuring
|
|
||||||
awaited
|
|
||||||
awake
|
|
||||||
aware
|
|
||||||
balanced
|
|
||||||
becoming
|
|
||||||
beloved
|
|
||||||
better
|
|
||||||
big
|
|
||||||
blessed
|
|
||||||
bold
|
|
||||||
boss
|
|
||||||
brave
|
|
||||||
brief
|
|
||||||
bright
|
|
||||||
bursting
|
|
||||||
busy
|
|
||||||
calm
|
|
||||||
capable
|
|
||||||
capital
|
|
||||||
careful
|
|
||||||
caring
|
|
||||||
casual
|
|
||||||
causal
|
|
||||||
central
|
|
||||||
certain
|
|
||||||
champion
|
|
||||||
charmed
|
|
||||||
charming
|
|
||||||
cheerful
|
|
||||||
chief
|
|
||||||
choice
|
|
||||||
civil
|
|
||||||
classic
|
|
||||||
clean
|
|
||||||
clear
|
|
||||||
clever
|
|
||||||
climbing
|
|
||||||
close
|
|
||||||
closing
|
|
||||||
coherent
|
|
||||||
comic
|
|
||||||
communal
|
|
||||||
complete
|
|
||||||
composed
|
|
||||||
concise
|
|
||||||
concrete
|
|
||||||
content
|
|
||||||
cool
|
|
||||||
correct
|
|
||||||
cosmic
|
|
||||||
crack
|
|
||||||
creative
|
|
||||||
credible
|
|
||||||
crisp
|
|
||||||
crucial
|
|
||||||
cuddly
|
|
||||||
cunning
|
|
||||||
curious
|
|
||||||
current
|
|
||||||
cute
|
|
||||||
daring
|
|
||||||
darling
|
|
||||||
dashing
|
|
||||||
dear
|
|
||||||
decent
|
|
||||||
deciding
|
|
||||||
deep
|
|
||||||
definite
|
|
||||||
delicate
|
|
||||||
desired
|
|
||||||
destined
|
|
||||||
devoted
|
|
||||||
direct
|
|
||||||
discrete
|
|
||||||
distinct
|
|
||||||
diverse
|
|
||||||
divine
|
|
||||||
dominant
|
|
||||||
driven
|
|
||||||
driving
|
|
||||||
dynamic
|
|
||||||
eager
|
|
||||||
easy
|
|
||||||
electric
|
|
||||||
elegant
|
|
||||||
emerging
|
|
||||||
eminent
|
|
||||||
enabled
|
|
||||||
enabling
|
|
||||||
endless
|
|
||||||
engaged
|
|
||||||
engaging
|
|
||||||
enhanced
|
|
||||||
enjoyed
|
|
||||||
enormous
|
|
||||||
enough
|
|
||||||
epic
|
|
||||||
equal
|
|
||||||
equipped
|
|
||||||
eternal
|
|
||||||
ethical
|
|
||||||
evident
|
|
||||||
evolved
|
|
||||||
evolving
|
|
||||||
exact
|
|
||||||
excited
|
|
||||||
exciting
|
|
||||||
exotic
|
|
||||||
expert
|
|
||||||
factual
|
|
||||||
fair
|
|
||||||
faithful
|
|
||||||
famous
|
|
||||||
fancy
|
|
||||||
fast
|
|
||||||
feasible
|
|
||||||
fine
|
|
||||||
finer
|
|
||||||
firm
|
|
||||||
first
|
|
||||||
fit
|
|
||||||
fitting
|
|
||||||
fleet
|
|
||||||
flexible
|
|
||||||
flowing
|
|
||||||
fluent
|
|
||||||
flying
|
|
||||||
fond
|
|
||||||
frank
|
|
||||||
free
|
|
||||||
fresh
|
|
||||||
full
|
|
||||||
fun
|
|
||||||
funky
|
|
||||||
funny
|
|
||||||
game
|
|
||||||
generous
|
|
||||||
gentle
|
|
||||||
genuine
|
|
||||||
giving
|
|
||||||
glad
|
|
||||||
glorious
|
|
||||||
glowing
|
|
||||||
golden
|
|
||||||
good
|
|
||||||
gorgeous
|
|
||||||
grand
|
|
||||||
grateful
|
|
||||||
great
|
|
||||||
growing
|
|
||||||
grown
|
|
||||||
guided
|
|
||||||
guiding
|
|
||||||
handy
|
|
||||||
happy
|
|
||||||
hardy
|
|
||||||
harmless
|
|
||||||
healthy
|
|
||||||
helped
|
|
||||||
helpful
|
|
||||||
helping
|
|
||||||
heroic
|
|
||||||
hip
|
|
||||||
holy
|
|
||||||
honest
|
|
||||||
hopeful
|
|
||||||
hot
|
|
||||||
huge
|
|
||||||
humane
|
|
||||||
humble
|
|
||||||
humorous
|
|
||||||
ideal
|
|
||||||
immense
|
|
||||||
immortal
|
|
||||||
immune
|
|
||||||
improved
|
|
||||||
in
|
|
||||||
included
|
|
||||||
infinite
|
|
||||||
informed
|
|
||||||
innocent
|
|
||||||
inspired
|
|
||||||
integral
|
|
||||||
intense
|
|
||||||
intent
|
|
||||||
internal
|
|
||||||
intimate
|
|
||||||
inviting
|
|
||||||
joint
|
|
||||||
just
|
|
||||||
keen
|
|
||||||
key
|
|
||||||
kind
|
|
||||||
knowing
|
|
||||||
known
|
|
||||||
large
|
|
||||||
lasting
|
|
||||||
leading
|
|
||||||
learning
|
|
||||||
legal
|
|
||||||
legible
|
|
||||||
lenient
|
|
||||||
liberal
|
|
||||||
light
|
|
||||||
liked
|
|
||||||
literate
|
|
||||||
live
|
|
||||||
living
|
|
||||||
logical
|
|
||||||
loved
|
|
||||||
loving
|
|
||||||
loyal
|
|
||||||
lucky
|
|
||||||
magical
|
|
||||||
magnetic
|
|
||||||
main
|
|
||||||
major
|
|
||||||
many
|
|
||||||
massive
|
|
||||||
master
|
|
||||||
mature
|
|
||||||
maximum
|
|
||||||
measured
|
|
||||||
meet
|
|
||||||
merry
|
|
||||||
mighty
|
|
||||||
mint
|
|
||||||
model
|
|
||||||
modern
|
|
||||||
modest
|
|
||||||
moral
|
|
||||||
more
|
|
||||||
moved
|
|
||||||
moving
|
|
||||||
musical
|
|
||||||
mutual
|
|
||||||
national
|
|
||||||
native
|
|
||||||
natural
|
|
||||||
nearby
|
|
||||||
neat
|
|
||||||
needed
|
|
||||||
neutral
|
|
||||||
new
|
|
||||||
next
|
|
||||||
nice
|
|
||||||
noble
|
|
||||||
normal
|
|
||||||
notable
|
|
||||||
noted
|
|
||||||
novel
|
|
||||||
obliging
|
|
||||||
on
|
|
||||||
one
|
|
||||||
open
|
|
||||||
optimal
|
|
||||||
optimum
|
|
||||||
organic
|
|
||||||
oriented
|
|
||||||
outgoing
|
|
||||||
patient
|
|
||||||
peaceful
|
|
||||||
perfect
|
|
||||||
pet
|
|
||||||
picked
|
|
||||||
pleasant
|
|
||||||
pleased
|
|
||||||
pleasing
|
|
||||||
poetic
|
|
||||||
polished
|
|
||||||
polite
|
|
||||||
popular
|
|
||||||
positive
|
|
||||||
possible
|
|
||||||
powerful
|
|
||||||
precious
|
|
||||||
precise
|
|
||||||
premium
|
|
||||||
prepared
|
|
||||||
present
|
|
||||||
pretty
|
|
||||||
primary
|
|
||||||
prime
|
|
||||||
pro
|
|
||||||
probable
|
|
||||||
profound
|
|
||||||
promoted
|
|
||||||
prompt
|
|
||||||
proper
|
|
||||||
proud
|
|
||||||
proven
|
|
||||||
pumped
|
|
||||||
pure
|
|
||||||
quality
|
|
||||||
quick
|
|
||||||
quiet
|
|
||||||
rapid
|
|
||||||
rare
|
|
||||||
rational
|
|
||||||
ready
|
|
||||||
real
|
|
||||||
refined
|
|
||||||
regular
|
|
||||||
related
|
|
||||||
relative
|
|
||||||
relaxed
|
|
||||||
relaxing
|
|
||||||
relevant
|
|
||||||
relieved
|
|
||||||
renewed
|
|
||||||
renewing
|
|
||||||
resolved
|
|
||||||
rested
|
|
||||||
rich
|
|
||||||
right
|
|
||||||
robust
|
|
||||||
romantic
|
|
||||||
ruling
|
|
||||||
sacred
|
|
||||||
safe
|
|
||||||
saved
|
|
||||||
saving
|
|
||||||
secure
|
|
||||||
select
|
|
||||||
selected
|
|
||||||
sensible
|
|
||||||
set
|
|
||||||
settled
|
|
||||||
settling
|
|
||||||
sharing
|
|
||||||
sharp
|
|
||||||
shining
|
|
||||||
simple
|
|
||||||
sincere
|
|
||||||
singular
|
|
||||||
skilled
|
|
||||||
smart
|
|
||||||
smashing
|
|
||||||
smiling
|
|
||||||
smooth
|
|
||||||
social
|
|
||||||
solid
|
|
||||||
sought
|
|
||||||
sound
|
|
||||||
special
|
|
||||||
splendid
|
|
||||||
square
|
|
||||||
stable
|
|
||||||
star
|
|
||||||
steady
|
|
||||||
sterling
|
|
||||||
still
|
|
||||||
stirred
|
|
||||||
stirring
|
|
||||||
striking
|
|
||||||
strong
|
|
||||||
stunning
|
|
||||||
subtle
|
|
||||||
suitable
|
|
||||||
suited
|
|
||||||
summary
|
|
||||||
sunny
|
|
||||||
super
|
|
||||||
superb
|
|
||||||
supreme
|
|
||||||
sure
|
|
||||||
sweeping
|
|
||||||
sweet
|
|
||||||
talented
|
|
||||||
teaching
|
|
||||||
tender
|
|
||||||
thankful
|
|
||||||
thorough
|
|
||||||
tidy
|
|
||||||
tight
|
|
||||||
together
|
|
||||||
tolerant
|
|
||||||
top
|
|
||||||
topical
|
|
||||||
tops
|
|
||||||
touched
|
|
||||||
touching
|
|
||||||
tough
|
|
||||||
true
|
|
||||||
trusted
|
|
||||||
trusting
|
|
||||||
trusty
|
|
||||||
ultimate
|
|
||||||
unbiased
|
|
||||||
uncommon
|
|
||||||
unified
|
|
||||||
unique
|
|
||||||
united
|
|
||||||
up
|
|
||||||
upright
|
|
||||||
upward
|
|
||||||
usable
|
|
||||||
useful
|
|
||||||
valid
|
|
||||||
valued
|
|
||||||
vast
|
|
||||||
verified
|
|
||||||
viable
|
|
||||||
vital
|
|
||||||
vocal
|
|
||||||
wanted
|
|
||||||
warm
|
|
||||||
wealthy
|
|
||||||
welcome
|
|
||||||
welcomed
|
|
||||||
well
|
|
||||||
whole
|
|
||||||
willing
|
|
||||||
winning
|
|
||||||
wired
|
|
||||||
wise
|
|
||||||
witty
|
|
||||||
wondrous
|
|
||||||
workable
|
|
||||||
working
|
|
||||||
worthy
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package randname
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: Make generator be based on fantasy, sci-fi and other literature
|
|
||||||
// and artistic names.
|
|
||||||
|
|
||||||
//go:embed adjectives.txt
|
|
||||||
var adjectives string
|
|
||||||
|
|
||||||
//go:embed names.txt
|
|
||||||
var names string
|
|
||||||
|
|
||||||
var (
|
|
||||||
adjectivesList = strings.Split(adjectives, "\n")
|
|
||||||
namesList = strings.Split(names, "\n")
|
|
||||||
)
|
|
||||||
|
|
||||||
func New(sep ...string) string {
|
|
||||||
if len(sep) == 0 {
|
|
||||||
sep = append(sep, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
a := adjectivesList[rand.Intn(len(adjectivesList))]
|
|
||||||
n := namesList[rand.Intn(len(namesList))]
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s%s%s", a, sep[0], n)
|
|
||||||
}
|
|
||||||
20
makefile
20
makefile
@@ -30,26 +30,6 @@ dev:
|
|||||||
dev/debug:
|
dev/debug:
|
||||||
$(MAKE) -j2 debug dev/assets
|
$(MAKE) -j2 debug dev/assets
|
||||||
|
|
||||||
editor/dev/server:
|
|
||||||
cd ./editor; go run github.com/joho/godotenv/cmd/godotenv@v1.5.1 \
|
|
||||||
go run github.com/air-verse/air@v1.52.2 \
|
|
||||||
--build.cmd "go build -o tmp/bin/main ./cmd" \
|
|
||||||
--build.bin "tmp/bin/main" \
|
|
||||||
--build.exclude_dir "node_modules" \
|
|
||||||
--build.include_ext "go" \
|
|
||||||
--build.stop_on_error "false" \
|
|
||||||
--misc.clean_on_exit true \
|
|
||||||
-- -dev -port $(PORT) -hostname 0.0.0.0
|
|
||||||
|
|
||||||
editor/dev/assets:
|
|
||||||
cd ./editor; tailwindcss \
|
|
||||||
-i ./assets/css/tailwind.css \
|
|
||||||
-o ./assets/css/style.css \
|
|
||||||
--watch
|
|
||||||
|
|
||||||
editor/dev:
|
|
||||||
$(MAKE) -j2 editor/dev/assets editor/dev/server
|
|
||||||
|
|
||||||
debug:
|
debug:
|
||||||
dlv debug -l 127.0.0.1:38697 \
|
dlv debug -l 127.0.0.1:38697 \
|
||||||
--continue \
|
--continue \
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ const (
|
|||||||
PermissionAuthor Permissions = 0x1111111111111111 // "author"
|
PermissionAuthor Permissions = 0x1111111111111111 // "author"
|
||||||
PermissionAdminDelete Permissions = 0x1000000000000000 // "admin.delete" -----
|
PermissionAdminDelete Permissions = 0x1000000000000000 // "admin.delete" -----
|
||||||
PermissionAdminAll Permissions = 0x0111110000000001 // "admin.all"
|
PermissionAdminAll Permissions = 0x0111110000000001 // "admin.all"
|
||||||
PermissionAdminPublication Permissions = 0x0100000000000000 // "admin.publication"
|
PermissionAdminProject Permissions = 0x0100000000000000 // "admin.project"
|
||||||
PermissionAdminMembers Permissions = 0x0010000000000000 // "admin.members"
|
PermissionAdminMembers Permissions = 0x0010000000000000 // "admin.members"
|
||||||
PermissionEditAll Permissions = 0x0000001111111111 // "edit.all" ---------
|
PermissionEditAll Permissions = 0x0000001111111111 // "edit.all" ---------
|
||||||
PermissionEditPages Permissions = 0x0000000100000000 // "edit.pages"
|
PermissionEditPages Permissions = 0x0000000100000000 // "edit.pages"
|
||||||
@@ -134,7 +134,7 @@ const (
|
|||||||
var PermissionLabels = map[Permissions]string{
|
var PermissionLabels = map[Permissions]string{
|
||||||
PermissionAuthor: "author",
|
PermissionAuthor: "author",
|
||||||
PermissionAdminDelete: "admin.delete",
|
PermissionAdminDelete: "admin.delete",
|
||||||
PermissionAdminPublication: "admin.publication",
|
PermissionAdminProject: "admin.project",
|
||||||
PermissionAdminMembers: "admin.members",
|
PermissionAdminMembers: "admin.members",
|
||||||
PermissionEditPages: "edit.pages",
|
PermissionEditPages: "edit.pages",
|
||||||
PermissionEditInteractions: "edit.interactions",
|
PermissionEditInteractions: "edit.interactions",
|
||||||
|
|||||||
@@ -6,16 +6,16 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Publication struct {
|
type Project struct {
|
||||||
ID uuid.UUID // Must be unique, represented as base64 string in URLs
|
ID uuid.UUID // Must be unique, represented as base64 string in URLs
|
||||||
Title string // Must not be empty
|
Title string // Must not be empty
|
||||||
DateCreated time.Time
|
DateCreated time.Time
|
||||||
DateUpdated time.Time
|
DateUpdated time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Model = (*Publication)(nil)
|
var _ Model = (*Project)(nil)
|
||||||
|
|
||||||
func (p Publication) Validate() error {
|
func (p Project) Validate() error {
|
||||||
errs := []error{}
|
errs := []error{}
|
||||||
if len(p.ID) == 0 {
|
if len(p.ID) == 0 {
|
||||||
errs = append(errs, ErrZeroValue{Name: "UUID"})
|
errs = append(errs, ErrZeroValue{Name: "UUID"})
|
||||||
@@ -31,7 +31,7 @@ func (p Publication) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
return ErrInvalidModel{Name: "Publication", Errors: errs}
|
return ErrInvalidModel{Name: "Project", Errors: errs}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -17,7 +17,7 @@ type Permissions struct {
|
|||||||
baseRepostiory
|
baseRepostiory
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must be initiated after [User] and [Publication]
|
// Must be initiated after [User] and [Project]
|
||||||
func NewPermissions(
|
func NewPermissions(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
db *sql.DB,
|
db *sql.DB,
|
||||||
@@ -32,17 +32,17 @@ func NewPermissions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
q := fmt.Sprintf(`
|
q := fmt.Sprintf(`
|
||||||
CREATE TABLE IF NOT EXISTS publication_permissions (
|
CREATE TABLE IF NOT EXISTS project_permissions (
|
||||||
publication_id TEXT NOT NULL,
|
project_id TEXT NOT NULL,
|
||||||
user_id TEXT NOT NULL,
|
user_id TEXT NOT NULL,
|
||||||
permissions_value INTEGER NOT NULL DEFAULT '0',
|
permissions_value INTEGER NOT NULL DEFAULT '0',
|
||||||
_permissions_text TEXT NOT NULL DEFAULT '', -- For display purposes only, may not always be up-to-date
|
_permissions_text TEXT NOT NULL DEFAULT '', -- For display purposes only, may not always be up-to-date
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT NOT NULL,
|
updated_at TEXT NOT NULL,
|
||||||
|
|
||||||
PRIMARY KEY(publication_id, user_id)
|
PRIMARY KEY(project_id, user_id)
|
||||||
FOREIGN KEY(publication_id)
|
FOREIGN KEY(project_id)
|
||||||
REFERENCES publications (id)
|
REFERENCES projects (id)
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
ON UPDATE RESTRICT,
|
ON UPDATE RESTRICT,
|
||||||
FOREIGN KEY(user_id)
|
FOREIGN KEY(user_id)
|
||||||
@@ -58,13 +58,13 @@ func NewPermissions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
if err := tx.Commit(); err != nil {
|
||||||
return nil, errors.Join(errors.New("unable to create publication tables"), err)
|
return nil, errors.Join(errors.New("unable to create project tables"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Permissions{baseRepostiory: b}, nil
|
return &Permissions{baseRepostiory: b}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo Permissions) Create(publication, user uuid.UUID, permissions model.Permissions) error {
|
func (repo Permissions) Create(project, user uuid.UUID, permissions model.Permissions) error {
|
||||||
repo.assert.NotNil(repo.db)
|
repo.assert.NotNil(repo.db)
|
||||||
repo.assert.NotNil(repo.ctx)
|
repo.assert.NotNil(repo.ctx)
|
||||||
repo.assert.NotNil(repo.ctx)
|
repo.assert.NotNil(repo.ctx)
|
||||||
@@ -75,21 +75,21 @@ func (repo Permissions) Create(publication, user uuid.UUID, permissions model.Pe
|
|||||||
}
|
}
|
||||||
|
|
||||||
q := `
|
q := `
|
||||||
INSERT INTO publication_permissions (publication_id, user_id, permissions_value, _permissions_text, created_at, updated_at)
|
INSERT INTO project_permissions (project_id, user_id, permissions_value, _permissions_text, created_at, updated_at)
|
||||||
VALUES (:publication_id, :user_id, :permissions_value, :permissions_text, :created_at, :updated_at)
|
VALUES (:project_id, :user_id, :permissions_value, :permissions_text, :created_at, :updated_at)
|
||||||
`
|
`
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
log := repo.log.With(slog.String("publication_id", publication.String()),
|
log := repo.log.With(slog.String("project_id", project.String()),
|
||||||
slog.String("user_id", user.String()),
|
slog.String("user_id", user.String()),
|
||||||
slog.String("permissions", fmt.Sprintf("%d", permissions)),
|
slog.String("permissions", fmt.Sprintf("%d", permissions)),
|
||||||
slog.String("permissions_text", permissions.String()),
|
slog.String("permissions_text", permissions.String()),
|
||||||
slog.String("query", q))
|
slog.String("query", q))
|
||||||
log.DebugContext(repo.ctx, "Inserting new publication permissions")
|
log.DebugContext(repo.ctx, "Inserting new project permissions")
|
||||||
|
|
||||||
_, err = tx.ExecContext(repo.ctx, q,
|
_, err = tx.ExecContext(repo.ctx, q,
|
||||||
sql.Named("publication_id", publication),
|
sql.Named("project_id", project),
|
||||||
sql.Named("user_id", user),
|
sql.Named("user_id", user),
|
||||||
sql.Named("permissions_value", permissions),
|
sql.Named("permissions_value", permissions),
|
||||||
sql.Named("permissions_text", permissions.String()),
|
sql.Named("permissions_text", permissions.String()),
|
||||||
@@ -97,7 +97,7 @@ func (repo Permissions) Create(publication, user uuid.UUID, permissions model.Pe
|
|||||||
sql.Named("updated_at", now.Format(dateFormat)),
|
sql.Named("updated_at", now.Format(dateFormat)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorContext(repo.ctx, "Failed to insert publication permissions", slog.String("error", err.Error()))
|
log.ErrorContext(repo.ctx, "Failed to insert project permissions", slog.String("error", err.Error()))
|
||||||
return errors.Join(ErrExecuteQuery, err)
|
return errors.Join(ErrExecuteQuery, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,24 +109,24 @@ func (repo Permissions) Create(publication, user uuid.UUID, permissions model.Pe
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo Permissions) GetByID(publication uuid.UUID, user uuid.UUID) (model.Permissions, error) {
|
func (repo Permissions) GetByID(project uuid.UUID, user uuid.UUID) (model.Permissions, error) {
|
||||||
repo.assert.NotNil(repo.db)
|
repo.assert.NotNil(repo.db)
|
||||||
repo.assert.NotNil(repo.ctx)
|
repo.assert.NotNil(repo.ctx)
|
||||||
repo.assert.NotNil(repo.log)
|
repo.assert.NotNil(repo.log)
|
||||||
|
|
||||||
q := `
|
q := `
|
||||||
SELECT permissions_value FROM publication_permissions
|
SELECT permissions_value FROM project_permissions
|
||||||
WHERE publication_id = :publication_id
|
WHERE project_id = :project_id
|
||||||
AND user_id = :user_id
|
AND user_id = :user_id
|
||||||
`
|
`
|
||||||
|
|
||||||
log := repo.log.With(slog.String("projcet_id", publication.String()),
|
log := repo.log.With(slog.String("projcet_id", project.String()),
|
||||||
slog.String("user_id", user.String()),
|
slog.String("user_id", user.String()),
|
||||||
slog.String("query", q))
|
slog.String("query", q))
|
||||||
log.DebugContext(repo.ctx, "Getting by ID")
|
log.DebugContext(repo.ctx, "Getting by ID")
|
||||||
|
|
||||||
row := repo.db.QueryRowContext(repo.ctx, q,
|
row := repo.db.QueryRowContext(repo.ctx, q,
|
||||||
sql.Named("publication_id", user),
|
sql.Named("project_id", user),
|
||||||
sql.Named("user_id", user))
|
sql.Named("user_id", user))
|
||||||
|
|
||||||
var p model.Permissions
|
var p model.Permissions
|
||||||
@@ -138,7 +138,7 @@ func (repo Permissions) GetByID(publication uuid.UUID, user uuid.UUID) (model.Pe
|
|||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetByUserID returns a publication_id-to-permissions map containing all publications and permissions that said userID
|
// GetByUserID returns a project_id-to-permissions map containing all projects and permissions that said userID
|
||||||
// has relation to.
|
// has relation to.
|
||||||
func (repo Permissions) GetByUserID(user uuid.UUID) (permissions map[uuid.UUID]model.Permissions, err error) {
|
func (repo Permissions) GetByUserID(user uuid.UUID) (permissions map[uuid.UUID]model.Permissions, err error) {
|
||||||
repo.assert.NotNil(repo.db)
|
repo.assert.NotNil(repo.db)
|
||||||
@@ -152,7 +152,7 @@ func (repo Permissions) GetByUserID(user uuid.UUID) (permissions map[uuid.UUID]m
|
|||||||
}
|
}
|
||||||
|
|
||||||
q := `
|
q := `
|
||||||
SELECT publication_id, permissions_value FROM publication_permissions
|
SELECT project_id, permissions_value FROM project_permissions
|
||||||
WHERE user_id = :user_id
|
WHERE user_id = :user_id
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -176,16 +176,16 @@ func (repo Permissions) GetByUserID(user uuid.UUID) (permissions map[uuid.UUID]m
|
|||||||
ps := map[uuid.UUID]model.Permissions{}
|
ps := map[uuid.UUID]model.Permissions{}
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var publication uuid.UUID
|
var project uuid.UUID
|
||||||
var permissions model.Permissions
|
var permissions model.Permissions
|
||||||
|
|
||||||
err := rows.Scan(&publication, &permissions)
|
err := rows.Scan(&project, &permissions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorContext(repo.ctx, "Failed to scan permissions of user id", slog.String("error", err.Error()))
|
log.ErrorContext(repo.ctx, "Failed to scan permissions of user id", slog.String("error", err.Error()))
|
||||||
return nil, errors.Join(ErrInvalidOutput, err)
|
return nil, errors.Join(ErrInvalidOutput, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ps[publication] = permissions
|
ps[project] = permissions
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
if err := tx.Commit(); err != nil {
|
||||||
@@ -196,7 +196,7 @@ func (repo Permissions) GetByUserID(user uuid.UUID) (permissions map[uuid.UUID]m
|
|||||||
return ps, nil
|
return ps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo Permissions) Update(publication, user uuid.UUID, permissions model.Permissions) error {
|
func (repo Permissions) Update(project, user uuid.UUID, permissions model.Permissions) error {
|
||||||
repo.assert.NotNil(repo.db)
|
repo.assert.NotNil(repo.db)
|
||||||
repo.assert.NotNil(repo.ctx)
|
repo.assert.NotNil(repo.ctx)
|
||||||
repo.assert.NotNil(repo.log)
|
repo.assert.NotNil(repo.log)
|
||||||
@@ -207,20 +207,20 @@ func (repo Permissions) Update(publication, user uuid.UUID, permissions model.Pe
|
|||||||
}
|
}
|
||||||
|
|
||||||
q := `
|
q := `
|
||||||
UPDATE publication_permissions
|
UPDATE project_permissions
|
||||||
SET permissions_value = :permissions_value
|
SET permissions_value = :permissions_value
|
||||||
_permissions_text = :permissions_text
|
_permissions_text = :permissions_text
|
||||||
updated_at = :updated_at
|
updated_at = :updated_at
|
||||||
WHERE publication_uuid = :publication_uuid
|
WHERE project_uuid = :project_uuid
|
||||||
AND user_uuid = :user_uuid
|
AND user_uuid = :user_uuid
|
||||||
`
|
`
|
||||||
|
|
||||||
log := repo.log.With(slog.String("publication_id", publication.String()),
|
log := repo.log.With(slog.String("project_id", project.String()),
|
||||||
slog.String("user_id", user.String()),
|
slog.String("user_id", user.String()),
|
||||||
slog.String("permissions", fmt.Sprintf("%d", permissions)),
|
slog.String("permissions", fmt.Sprintf("%d", permissions)),
|
||||||
slog.String("permissions_text", permissions.String()),
|
slog.String("permissions_text", permissions.String()),
|
||||||
slog.String("query", q))
|
slog.String("query", q))
|
||||||
log.DebugContext(repo.ctx, "Updating publication permissions")
|
log.DebugContext(repo.ctx, "Updating project permissions")
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
@@ -228,11 +228,11 @@ func (repo Permissions) Update(publication, user uuid.UUID, permissions model.Pe
|
|||||||
sql.Named("permissions_value", permissions),
|
sql.Named("permissions_value", permissions),
|
||||||
sql.Named("permissions_text", permissions.String()),
|
sql.Named("permissions_text", permissions.String()),
|
||||||
sql.Named("updated_at", now.Format(dateFormat)),
|
sql.Named("updated_at", now.Format(dateFormat)),
|
||||||
sql.Named("publication_id", publication),
|
sql.Named("project_id", project),
|
||||||
sql.Named("user_id", user),
|
sql.Named("user_id", user),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorContext(repo.ctx, "Failed to update publication permissions", slog.String("error", err.Error()))
|
log.ErrorContext(repo.ctx, "Failed to update project permissions", slog.String("error", err.Error()))
|
||||||
return errors.Join(ErrExecuteQuery, err)
|
return errors.Join(ErrExecuteQuery, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +244,7 @@ func (repo Permissions) Update(publication, user uuid.UUID, permissions model.Pe
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo Permissions) Delete(publication, user uuid.UUID) error {
|
func (repo Permissions) Delete(project, user uuid.UUID) error {
|
||||||
repo.assert.NotNil(repo.db)
|
repo.assert.NotNil(repo.db)
|
||||||
repo.assert.NotNil(repo.ctx)
|
repo.assert.NotNil(repo.ctx)
|
||||||
repo.assert.NotNil(repo.ctx)
|
repo.assert.NotNil(repo.ctx)
|
||||||
@@ -255,22 +255,22 @@ func (repo Permissions) Delete(publication, user uuid.UUID) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
q := `
|
q := `
|
||||||
DELETE FROM publication_permissions
|
DELETE FROM project_permissions
|
||||||
WHERE publication_id = :publication_id
|
WHERE project_id = :project_id
|
||||||
AND user_id = :user_id
|
AND user_id = :user_id
|
||||||
`
|
`
|
||||||
|
|
||||||
log := repo.log.With(slog.String("publication_id", publication.String()),
|
log := repo.log.With(slog.String("project_id", project.String()),
|
||||||
slog.String("user_id", user.String()),
|
slog.String("user_id", user.String()),
|
||||||
slog.String("query", q))
|
slog.String("query", q))
|
||||||
log.DebugContext(repo.ctx, "Deleting publication permissions")
|
log.DebugContext(repo.ctx, "Deleting project permissions")
|
||||||
|
|
||||||
_, err = tx.ExecContext(repo.ctx, q,
|
_, err = tx.ExecContext(repo.ctx, q,
|
||||||
sql.Named("publication_id", publication),
|
sql.Named("project_id", project),
|
||||||
sql.Named("user_id", user),
|
sql.Named("user_id", user),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorContext(repo.ctx, "Failed to delete publication permissions", slog.String("error", err.Error()))
|
log.ErrorContext(repo.ctx, "Failed to delete project permissions", slog.String("error", err.Error()))
|
||||||
return errors.Join(ErrExecuteQuery, err)
|
return errors.Join(ErrExecuteQuery, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Publication struct {
|
type Project struct {
|
||||||
baseRepostiory
|
baseRepostiory
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPublication(ctx context.Context, db *sql.DB, log *slog.Logger, assert tinyssert.Assertions) (*Publication, error) {
|
func NewProject(ctx context.Context, db *sql.DB, log *slog.Logger, assert tinyssert.Assertions) (*Project, error) {
|
||||||
b := newBaseRepostiory(ctx, db, log, assert)
|
b := newBaseRepostiory(ctx, db, log, assert)
|
||||||
|
|
||||||
tx, err := db.BeginTx(ctx, nil)
|
tx, err := db.BeginTx(ctx, nil)
|
||||||
@@ -27,7 +27,7 @@ func NewPublication(ctx context.Context, db *sql.DB, log *slog.Logger, assert ti
|
|||||||
}
|
}
|
||||||
|
|
||||||
_, err = tx.ExecContext(ctx, `
|
_, err = tx.ExecContext(ctx, `
|
||||||
CREATE TABLE IF NOT EXISTS publications (
|
CREATE TABLE IF NOT EXISTS projects (
|
||||||
id TEXT NOT NULL PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
@@ -38,13 +38,13 @@ func NewPublication(ctx context.Context, db *sql.DB, log *slog.Logger, assert ti
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
if err := tx.Commit(); err != nil {
|
||||||
return nil, errors.Join(errors.New("unable to create publication tables"), err)
|
return nil, errors.Join(errors.New("unable to create project tables"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Publication{baseRepostiory: b}, nil
|
return &Project{baseRepostiory: b}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo Publication) Create(p model.Publication) error {
|
func (repo Project) Create(p model.Project) error {
|
||||||
repo.assert.NotNil(repo.db)
|
repo.assert.NotNil(repo.db)
|
||||||
repo.assert.NotNil(repo.ctx)
|
repo.assert.NotNil(repo.ctx)
|
||||||
repo.assert.NotNil(repo.ctx)
|
repo.assert.NotNil(repo.ctx)
|
||||||
@@ -59,12 +59,12 @@ func (repo Publication) Create(p model.Publication) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
q := `
|
q := `
|
||||||
INSERT INTO publications (id, title, created_at, updated_at)
|
INSERT INTO projects (id, title, created_at, updated_at)
|
||||||
VALUES (:id, :title, :created_at, :updated_at)
|
VALUES (:id, :title, :created_at, :updated_at)
|
||||||
`
|
`
|
||||||
|
|
||||||
log := repo.log.With(slog.String("id", p.ID.String()), slog.String("query", q))
|
log := repo.log.With(slog.String("id", p.ID.String()), slog.String("query", q))
|
||||||
log.DebugContext(repo.ctx, "Inserting new publication")
|
log.DebugContext(repo.ctx, "Inserting new project")
|
||||||
|
|
||||||
_, err = tx.ExecContext(repo.ctx, q,
|
_, err = tx.ExecContext(repo.ctx, q,
|
||||||
sql.Named("id", p.ID),
|
sql.Named("id", p.ID),
|
||||||
@@ -73,7 +73,7 @@ func (repo Publication) Create(p model.Publication) error {
|
|||||||
sql.Named("updated_at", p.DateUpdated.Format(dateFormat)),
|
sql.Named("updated_at", p.DateUpdated.Format(dateFormat)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorContext(repo.ctx, "Failed to insert publication", slog.String("error", err.Error()))
|
log.ErrorContext(repo.ctx, "Failed to insert project", slog.String("error", err.Error()))
|
||||||
return errors.Join(ErrExecuteQuery, err)
|
return errors.Join(ErrExecuteQuery, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,20 +85,20 @@ func (repo Publication) Create(p model.Publication) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo Publication) GetByID(publicationID uuid.UUID) (publication model.Publication, err error) {
|
func (repo Project) GetByID(projectID uuid.UUID) (project model.Project, err error) {
|
||||||
repo.assert.NotNil(repo.db)
|
repo.assert.NotNil(repo.db)
|
||||||
repo.assert.NotNil(repo.ctx)
|
repo.assert.NotNil(repo.ctx)
|
||||||
repo.assert.NotNil(repo.log)
|
repo.assert.NotNil(repo.log)
|
||||||
|
|
||||||
q := `
|
q := `
|
||||||
SELECT id, title, created_at, updated_at FROM publications
|
SELECT id, title, created_at, updated_at FROM projects
|
||||||
WHERE id = :id
|
WHERE id = :id
|
||||||
`
|
`
|
||||||
|
|
||||||
log := repo.log.With(slog.String("query", q), slog.String("id", publicationID.String()))
|
log := repo.log.With(slog.String("query", q), slog.String("id", projectID.String()))
|
||||||
log.DebugContext(repo.ctx, "Getting publication by ID")
|
log.DebugContext(repo.ctx, "Getting project by ID")
|
||||||
|
|
||||||
row := repo.db.QueryRowContext(repo.ctx, q, sql.Named("id", publicationID))
|
row := repo.db.QueryRowContext(repo.ctx, q, sql.Named("id", projectID))
|
||||||
|
|
||||||
var id uuid.UUID
|
var id uuid.UUID
|
||||||
var title string
|
var title string
|
||||||
@@ -106,23 +106,23 @@ func (repo Publication) GetByID(publicationID uuid.UUID) (publication model.Publ
|
|||||||
|
|
||||||
err = row.Scan(&id, &title, &dateCreatedStr, &dateUpdatedStr)
|
err = row.Scan(&id, &title, &dateCreatedStr, &dateUpdatedStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorContext(repo.ctx, "Failed to scan publications with IDs", slog.String("error", err.Error()))
|
log.ErrorContext(repo.ctx, "Failed to scan projects with IDs", slog.String("error", err.Error()))
|
||||||
return model.Publication{}, errors.Join(ErrInvalidOutput, err)
|
return model.Project{}, errors.Join(ErrInvalidOutput, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dateCreated, err := time.Parse(dateFormat, dateCreatedStr)
|
dateCreated, err := time.Parse(dateFormat, dateCreatedStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorContext(repo.ctx, "Failed to scan publications with IDs", slog.String("error", err.Error()))
|
log.ErrorContext(repo.ctx, "Failed to scan projects with IDs", slog.String("error", err.Error()))
|
||||||
return model.Publication{}, errors.Join(ErrInvalidOutput, err)
|
return model.Project{}, errors.Join(ErrInvalidOutput, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dateUpdated, err := time.Parse(dateFormat, dateUpdatedStr)
|
dateUpdated, err := time.Parse(dateFormat, dateUpdatedStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorContext(repo.ctx, "Failed to scan publications with IDs", slog.String("error", err.Error()))
|
log.ErrorContext(repo.ctx, "Failed to scan projects with IDs", slog.String("error", err.Error()))
|
||||||
return model.Publication{}, errors.Join(ErrInvalidOutput, err)
|
return model.Project{}, errors.Join(ErrInvalidOutput, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return model.Publication{
|
return model.Project{
|
||||||
ID: id,
|
ID: id,
|
||||||
Title: title,
|
Title: title,
|
||||||
DateCreated: dateCreated,
|
DateCreated: dateCreated,
|
||||||
@@ -130,7 +130,7 @@ func (repo Publication) GetByID(publicationID uuid.UUID) (publication model.Publ
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo Publication) GetByIDs(ids []uuid.UUID) (publications []model.Publication, err error) {
|
func (repo Project) GetByIDs(ids []uuid.UUID) (projects []model.Project, err error) {
|
||||||
repo.assert.NotNil(repo.db)
|
repo.assert.NotNil(repo.db)
|
||||||
repo.assert.NotNil(repo.ctx)
|
repo.assert.NotNil(repo.ctx)
|
||||||
repo.assert.NotNil(repo.log)
|
repo.assert.NotNil(repo.log)
|
||||||
@@ -147,16 +147,16 @@ func (repo Publication) GetByIDs(ids []uuid.UUID) (publications []model.Publicat
|
|||||||
}
|
}
|
||||||
|
|
||||||
q := fmt.Sprintf(`
|
q := fmt.Sprintf(`
|
||||||
SELECT id, title, created_at, updated_at FROM publications
|
SELECT id, title, created_at, updated_at FROM projects
|
||||||
WHERE %s
|
WHERE %s
|
||||||
`, strings.Join(c, " OR "))
|
`, strings.Join(c, " OR "))
|
||||||
|
|
||||||
log := repo.log.With(slog.String("query", q))
|
log := repo.log.With(slog.String("query", q))
|
||||||
log.DebugContext(repo.ctx, "Getting publications by IDs")
|
log.DebugContext(repo.ctx, "Getting projects by IDs")
|
||||||
|
|
||||||
rows, err := tx.QueryContext(repo.ctx, q)
|
rows, err := tx.QueryContext(repo.ctx, q)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorContext(repo.ctx, "Failed to get publications by IDs", slog.String("error", err.Error()))
|
log.ErrorContext(repo.ctx, "Failed to get projects by IDs", slog.String("error", err.Error()))
|
||||||
return nil, errors.Join(ErrExecuteQuery, err)
|
return nil, errors.Join(ErrExecuteQuery, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ func (repo Publication) GetByIDs(ids []uuid.UUID) (publications []model.Publicat
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
ps := []model.Publication{}
|
ps := []model.Project{}
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var id uuid.UUID
|
var id uuid.UUID
|
||||||
@@ -176,23 +176,23 @@ func (repo Publication) GetByIDs(ids []uuid.UUID) (publications []model.Publicat
|
|||||||
|
|
||||||
err := rows.Scan(&id, &title, &dateCreatedStr, &dateUpdatedStr)
|
err := rows.Scan(&id, &title, &dateCreatedStr, &dateUpdatedStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorContext(repo.ctx, "Failed to scan publications with IDs", slog.String("error", err.Error()))
|
log.ErrorContext(repo.ctx, "Failed to scan projects with IDs", slog.String("error", err.Error()))
|
||||||
return nil, errors.Join(ErrInvalidOutput, err)
|
return nil, errors.Join(ErrInvalidOutput, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dateCreated, err := time.Parse(dateFormat, dateCreatedStr)
|
dateCreated, err := time.Parse(dateFormat, dateCreatedStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorContext(repo.ctx, "Failed to scan publications with IDs", slog.String("error", err.Error()))
|
log.ErrorContext(repo.ctx, "Failed to scan projects with IDs", slog.String("error", err.Error()))
|
||||||
return nil, errors.Join(ErrInvalidOutput, err)
|
return nil, errors.Join(ErrInvalidOutput, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dateUpdated, err := time.Parse(dateFormat, dateUpdatedStr)
|
dateUpdated, err := time.Parse(dateFormat, dateUpdatedStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorContext(repo.ctx, "Failed to scan publications with IDs", slog.String("error", err.Error()))
|
log.ErrorContext(repo.ctx, "Failed to scan projects with IDs", slog.String("error", err.Error()))
|
||||||
return nil, errors.Join(ErrInvalidOutput, err)
|
return nil, errors.Join(ErrInvalidOutput, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ps = append(ps, model.Publication{
|
ps = append(ps, model.Project{
|
||||||
ID: id,
|
ID: id,
|
||||||
Title: title,
|
Title: title,
|
||||||
DateCreated: dateCreated,
|
DateCreated: dateCreated,
|
||||||
@@ -208,7 +208,7 @@ func (repo Publication) GetByIDs(ids []uuid.UUID) (publications []model.Publicat
|
|||||||
return ps, nil
|
return ps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo Publication) Update(p model.Publication) error {
|
func (repo Project) Update(p model.Project) error {
|
||||||
repo.assert.NotNil(repo.db)
|
repo.assert.NotNil(repo.db)
|
||||||
repo.assert.NotNil(repo.ctx)
|
repo.assert.NotNil(repo.ctx)
|
||||||
repo.assert.NotNil(repo.ctx)
|
repo.assert.NotNil(repo.ctx)
|
||||||
@@ -223,14 +223,14 @@ func (repo Publication) Update(p model.Publication) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
q := `
|
q := `
|
||||||
UPDATE publications
|
UPDATE projects
|
||||||
SET title = :title
|
SET title = :title
|
||||||
updated_at = :updated_at
|
updated_at = :updated_at
|
||||||
WHERE id = :id
|
WHERE id = :id
|
||||||
`
|
`
|
||||||
|
|
||||||
log := repo.log.With(slog.String("id", p.ID.String()), slog.String("query", q))
|
log := repo.log.With(slog.String("id", p.ID.String()), slog.String("query", q))
|
||||||
log.DebugContext(repo.ctx, "Updating publication")
|
log.DebugContext(repo.ctx, "Updating project")
|
||||||
|
|
||||||
_, err = tx.ExecContext(repo.ctx, q,
|
_, err = tx.ExecContext(repo.ctx, q,
|
||||||
sql.Named("title", p.Title),
|
sql.Named("title", p.Title),
|
||||||
@@ -238,7 +238,7 @@ func (repo Publication) Update(p model.Publication) error {
|
|||||||
sql.Named("id", p.ID),
|
sql.Named("id", p.ID),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorContext(repo.ctx, "Failed to insert publication", slog.String("error", err.Error()))
|
log.ErrorContext(repo.ctx, "Failed to insert project", slog.String("error", err.Error()))
|
||||||
return errors.Join(ErrExecuteQuery, err)
|
return errors.Join(ErrExecuteQuery, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +250,7 @@ func (repo Publication) Update(p model.Publication) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo Publication) DeleteByID(id uuid.UUID) error {
|
func (repo Project) DeleteByID(id uuid.UUID) error {
|
||||||
repo.assert.NotNil(repo.db)
|
repo.assert.NotNil(repo.db)
|
||||||
repo.assert.NotNil(repo.ctx)
|
repo.assert.NotNil(repo.ctx)
|
||||||
repo.assert.NotNil(repo.ctx)
|
repo.assert.NotNil(repo.ctx)
|
||||||
@@ -261,15 +261,15 @@ func (repo Publication) DeleteByID(id uuid.UUID) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
q := `
|
q := `
|
||||||
DELETE FROM publications WHERE id = :id
|
DELETE FROM projects WHERE id = :id
|
||||||
`
|
`
|
||||||
|
|
||||||
log := repo.log.With(slog.String("id", id.String()), slog.String("query", q))
|
log := repo.log.With(slog.String("id", id.String()), slog.String("query", q))
|
||||||
log.DebugContext(repo.ctx, "Deleting publication")
|
log.DebugContext(repo.ctx, "Deleting project")
|
||||||
|
|
||||||
_, err = tx.ExecContext(repo.ctx, q, sql.Named("id", id))
|
_, err = tx.ExecContext(repo.ctx, q, sql.Named("id", id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorContext(repo.ctx, "Failed to delete publication", slog.String("error", err.Error()))
|
log.ErrorContext(repo.ctx, "Failed to delete project", slog.String("error", err.Error()))
|
||||||
return errors.Join(ErrExecuteQuery, err)
|
return errors.Join(ErrExecuteQuery, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +35,6 @@ func newBaseRepostiory(ctx context.Context, db *sql.DB, log *slog.Logger, assert
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
// TODO: Change all ErrDatabaseConn to ErrCloseConn
|
// TODO: Change all ErrDatabaseConn to ErrCloseConn
|
||||||
// TODO: Change error to be agnostic to underlying storage type
|
|
||||||
ErrDatabaseConn = errors.New("repository: failed to begin transaction/connection with database")
|
ErrDatabaseConn = errors.New("repository: failed to begin transaction/connection with database")
|
||||||
ErrCloseConn = errors.New("repository: failed to close/commit connection")
|
ErrCloseConn = errors.New("repository: failed to close/commit connection")
|
||||||
ErrExecuteQuery = errors.New("repository: failed to execute query")
|
ErrExecuteQuery = errors.New("repository: failed to execute query")
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"code.capytal.cc/loreddev/x/tinyssert"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RoleRepository struct {
|
|
||||||
db *sql.DB
|
|
||||||
|
|
||||||
ctx context.Context
|
|
||||||
log *slog.Logger
|
|
||||||
assert tinyssert.Assertions
|
|
||||||
}
|
|
||||||
@@ -14,27 +14,27 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type publicationController struct {
|
type projectController struct {
|
||||||
publicationSvc *service.Publication
|
projectSvc *service.Project
|
||||||
|
|
||||||
templates templates.ITemplate
|
templates templates.ITemplate
|
||||||
|
|
||||||
assert tinyssert.Assertions
|
assert tinyssert.Assertions
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPublicationController(
|
func newProjectController(
|
||||||
publicationService *service.Publication,
|
projectService *service.Project,
|
||||||
templates templates.ITemplate,
|
templates templates.ITemplate,
|
||||||
assertions tinyssert.Assertions,
|
assertions tinyssert.Assertions,
|
||||||
) *publicationController {
|
) *projectController {
|
||||||
return &publicationController{
|
return &projectController{
|
||||||
publicationSvc: publicationService,
|
projectSvc: projectService,
|
||||||
templates: templates,
|
templates: templates,
|
||||||
assert: assertions,
|
assert: assertions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctrl publicationController) dashboard(w http.ResponseWriter, r *http.Request) {
|
func (ctrl projectController) dashboard(w http.ResponseWriter, r *http.Request) {
|
||||||
userCtx := NewUserContext(r.Context())
|
userCtx := NewUserContext(r.Context())
|
||||||
|
|
||||||
userID, ok := userCtx.GetUserID()
|
userID, ok := userCtx.GetUserID()
|
||||||
@@ -43,7 +43,7 @@ func (ctrl publicationController) dashboard(w http.ResponseWriter, r *http.Reque
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
publications, err := ctrl.publicationSvc.ListOwnedBy(userID)
|
projects, err := ctrl.projectSvc.GetUserProjects(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
problem.NewInternalServerError(err).ServeHTTP(w, r)
|
problem.NewInternalServerError(err).ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
@@ -52,15 +52,15 @@ func (ctrl publicationController) dashboard(w http.ResponseWriter, r *http.Reque
|
|||||||
ps := make([]struct {
|
ps := make([]struct {
|
||||||
ID string
|
ID string
|
||||||
Title string
|
Title string
|
||||||
}, len(publications))
|
}, len(projects))
|
||||||
|
|
||||||
for i, publication := range publications {
|
for i, project := range projects {
|
||||||
ps[i] = struct {
|
ps[i] = struct {
|
||||||
ID string
|
ID string
|
||||||
Title string
|
Title string
|
||||||
}{
|
}{
|
||||||
ID: base64.URLEncoding.EncodeToString([]byte(publication.ID.String())),
|
ID: base64.URLEncoding.EncodeToString([]byte(project.ID.String())),
|
||||||
Title: publication.Title,
|
Title: project.Title,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,24 +70,24 @@ func (ctrl publicationController) dashboard(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctrl publicationController) getPublication(w http.ResponseWriter, r *http.Request) {
|
func (ctrl projectController) getProject(w http.ResponseWriter, r *http.Request) {
|
||||||
// TODO: Handle private publications
|
// TODO: Handle private projects
|
||||||
|
|
||||||
shortPublicationID := r.PathValue("publicationID")
|
shortProjectID := r.PathValue("projectID")
|
||||||
|
|
||||||
id, err := base64.URLEncoding.DecodeString(shortPublicationID)
|
id, err := base64.URLEncoding.DecodeString(shortProjectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
problem.NewBadRequest(fmt.Sprintf("Incorrectly encoded publication ID: %s", err.Error())).ServeHTTP(w, r)
|
problem.NewBadRequest(fmt.Sprintf("Incorrectly encoded project ID: %s", err.Error())).ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
publicationID, err := uuid.ParseBytes(id)
|
projectID, err := uuid.ParseBytes(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
problem.NewBadRequest("Publication ID is not a valid UUID").ServeHTTP(w, r)
|
problem.NewBadRequest("Project ID is not a valid UUID").ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
publication, err := ctrl.publicationSvc.Get(publicationID)
|
project, err := ctrl.projectSvc.GetProject(projectID)
|
||||||
if errors.Is(err, service.ErrNotFound) {
|
if errors.Is(err, service.ErrNotFound) {
|
||||||
problem.NewNotFound().ServeHTTP(w, r)
|
problem.NewNotFound().ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
@@ -96,8 +96,8 @@ func (ctrl publicationController) getPublication(w http.ResponseWriter, r *http.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Return publication template
|
// TODO: Return project template
|
||||||
b, err := json.Marshal(publication)
|
b, err := json.Marshal(project)
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
w.Header().Add("Content-Type", "application/json")
|
||||||
if _, err := w.Write(b); err != nil {
|
if _, err := w.Write(b); err != nil {
|
||||||
@@ -106,7 +106,7 @@ func (ctrl publicationController) getPublication(w http.ResponseWriter, r *http.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctrl publicationController) createPublication(w http.ResponseWriter, r *http.Request) {
|
func (ctrl projectController) createProject(w http.ResponseWriter, r *http.Request) {
|
||||||
userCtx := NewUserContext(r.Context())
|
userCtx := NewUserContext(r.Context())
|
||||||
|
|
||||||
userID, ok := userCtx.GetUserID()
|
userID, ok := userCtx.GetUserID()
|
||||||
@@ -121,12 +121,12 @@ func (ctrl publicationController) createPublication(w http.ResponseWriter, r *ht
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
publication, err := ctrl.publicationSvc.Create(title, userID)
|
project, err := ctrl.projectSvc.Create(title, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
problem.NewInternalServerError(err).ServeHTTP(w, r)
|
problem.NewInternalServerError(err).ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
path := fmt.Sprintf("/publication/%s/", base64.URLEncoding.EncodeToString([]byte(publication.ID.String())))
|
path := fmt.Sprintf("/p/%s/", base64.URLEncoding.EncodeToString([]byte(project.ID.String())))
|
||||||
http.Redirect(w, r, path, http.StatusSeeOther)
|
http.Redirect(w, r, path, http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"code.capytal.cc/capytal/comicverse/service"
|
"code.capytal.cc/capytal/comicverse/service"
|
||||||
"code.capytal.cc/capytal/comicverse/templates"
|
"code.capytal.cc/capytal/comicverse/templates"
|
||||||
@@ -16,9 +17,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type router struct {
|
type router struct {
|
||||||
userService *service.User
|
userService *service.User
|
||||||
tokenService *service.Token
|
tokenService *service.Token
|
||||||
publicationService *service.Publication
|
projectService *service.Project
|
||||||
|
|
||||||
templates templates.ITemplate
|
templates templates.ITemplate
|
||||||
assets fs.FS
|
assets fs.FS
|
||||||
@@ -35,8 +36,8 @@ func New(cfg Config) (http.Handler, error) {
|
|||||||
if cfg.TokenService == nil {
|
if cfg.TokenService == nil {
|
||||||
return nil, errors.New("token service is nil")
|
return nil, errors.New("token service is nil")
|
||||||
}
|
}
|
||||||
if cfg.PublicationService == nil {
|
if cfg.ProjectService == nil {
|
||||||
return nil, errors.New("publication service is nil")
|
return nil, errors.New("project service is nil")
|
||||||
}
|
}
|
||||||
if cfg.Templates == nil {
|
if cfg.Templates == nil {
|
||||||
return nil, errors.New("templates is nil")
|
return nil, errors.New("templates is nil")
|
||||||
@@ -52,9 +53,9 @@ func New(cfg Config) (http.Handler, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
r := &router{
|
r := &router{
|
||||||
userService: cfg.UserService,
|
userService: cfg.UserService,
|
||||||
tokenService: cfg.TokenService,
|
tokenService: cfg.TokenService,
|
||||||
publicationService: cfg.PublicationService,
|
projectService: cfg.ProjectService,
|
||||||
|
|
||||||
templates: cfg.Templates,
|
templates: cfg.Templates,
|
||||||
assets: cfg.Assets,
|
assets: cfg.Assets,
|
||||||
@@ -68,9 +69,9 @@ func New(cfg Config) (http.Handler, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
UserService *service.User
|
UserService *service.User
|
||||||
TokenService *service.Token
|
TokenService *service.Token
|
||||||
PublicationService *service.Publication
|
ProjectService *service.Project
|
||||||
|
|
||||||
Templates templates.ITemplate
|
Templates templates.ITemplate
|
||||||
Assets fs.FS
|
Assets fs.FS
|
||||||
@@ -88,16 +89,8 @@ func (router *router) setup() http.Handler {
|
|||||||
|
|
||||||
log.Debug("Initializing router")
|
log.Debug("Initializing router")
|
||||||
|
|
||||||
mux := multiplexer.New()
|
|
||||||
mux = multiplexer.WithFormMethod(mux, "x-method")
|
|
||||||
mux = multiplexer.WithPatternRules(mux,
|
|
||||||
multiplexer.EnsureMethod(),
|
|
||||||
multiplexer.EnsureStrictEnd(),
|
|
||||||
multiplexer.EnsureTrailingSlash(),
|
|
||||||
)
|
|
||||||
|
|
||||||
r := smalltrip.NewRouter(
|
r := smalltrip.NewRouter(
|
||||||
smalltrip.WithMultiplexer(mux),
|
smalltrip.WithAssertions(router.assert),
|
||||||
smalltrip.WithLogger(log.WithGroup("smalltrip")),
|
smalltrip.WithLogger(log.WithGroup("smalltrip")),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -121,17 +114,17 @@ func (router *router) setup() http.Handler {
|
|||||||
Templates: router.templates,
|
Templates: router.templates,
|
||||||
Assert: router.assert,
|
Assert: router.assert,
|
||||||
})
|
})
|
||||||
publicationController := newPublicationController(router.publicationService, router.templates, router.assert)
|
projectController := newProjectController(router.projectService, router.templates, router.assert)
|
||||||
|
|
||||||
r.Handle("GET /assets/{_file...}", http.StripPrefix("/assets/", http.FileServerFS(router.assets)))
|
r.Handle("/assets/", http.StripPrefix("/assets/", http.FileServerFS(router.assets)))
|
||||||
|
|
||||||
r.Use(userController.userMiddleware)
|
r.Use(userController.userMiddleware)
|
||||||
|
|
||||||
r.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
|
r.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
// TODO: Add a way to the user to bypass this check and see the landing page.
|
// TODO: Add a way to the user to bypass this check and see the landing page.
|
||||||
// Probably a query parameter to bypass like "?landing=true"
|
// Probably a query parameter to bypass like "?landing=true"
|
||||||
if _, ok := NewUserContext(r.Context()).GetUserID(); ok {
|
if _, ok := NewUserContext(r.Context()).GetUserID(); ok {
|
||||||
publicationController.dashboard(w, r)
|
projectController.dashboard(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,14 +134,23 @@ func (router *router) setup() http.Handler {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
r.HandleFunc("GET /login/{$}", userController.login)
|
r.HandleFunc("/login/{$}", userController.login)
|
||||||
r.HandleFunc("POST /login/{$}", userController.login)
|
r.HandleFunc("/register/{$}", userController.register)
|
||||||
r.HandleFunc("GET /register/{$}", userController.register)
|
|
||||||
r.HandleFunc("POST /register/{$}", userController.register)
|
|
||||||
|
|
||||||
// TODO: Provide/redirect short publication-id paths to long paths with the publication title as URL /publications/title-of-the-publication-<start of uuid>
|
// TODO: Provide/redirect short project-id paths to long paths with the project title as URL /projects/title-of-the-project-<start of uuid>
|
||||||
r.HandleFunc("GET /publication/{publicationID}/{$}", publicationController.getPublication)
|
r.HandleFunc("GET /p/{projectID}/{$}", projectController.getProject)
|
||||||
r.HandleFunc("POST /publication/{$}", publicationController.createPublication)
|
r.HandleFunc("POST /p/{$}", projectController.createProject)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getMethod is a helper function to get the HTTP method of request, tacking precedence
|
||||||
|
// the "x-method" argument sent by requests via form or query values.
|
||||||
|
func getMethod(r *http.Request) string {
|
||||||
|
m := r.FormValue("x-method")
|
||||||
|
if m != "" {
|
||||||
|
return strings.ToUpper(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.ToUpper(r.Method)
|
||||||
|
}
|
||||||
|
|||||||
124
service/project.go
Normal file
124
service/project.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.capytal.cc/capytal/comicverse/model"
|
||||||
|
"code.capytal.cc/capytal/comicverse/repository"
|
||||||
|
"code.capytal.cc/loreddev/x/tinyssert"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Project struct {
|
||||||
|
projectRepo *repository.Project
|
||||||
|
permissionRepo *repository.Permissions
|
||||||
|
|
||||||
|
log *slog.Logger
|
||||||
|
assert tinyssert.Assertions
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProject(
|
||||||
|
project *repository.Project,
|
||||||
|
permissions *repository.Permissions,
|
||||||
|
logger *slog.Logger,
|
||||||
|
assertions tinyssert.Assertions,
|
||||||
|
) *Project {
|
||||||
|
return &Project{
|
||||||
|
projectRepo: project,
|
||||||
|
permissionRepo: permissions,
|
||||||
|
|
||||||
|
log: logger,
|
||||||
|
assert: assertions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc Project) Create(title string, ownerUserID ...uuid.UUID) (model.Project, error) {
|
||||||
|
log := svc.log.With(slog.String("title", title))
|
||||||
|
log.Info("Creating project")
|
||||||
|
defer log.Info("Finished creating project")
|
||||||
|
|
||||||
|
id, err := uuid.NewV7()
|
||||||
|
if err != nil {
|
||||||
|
return model.Project{}, fmt.Errorf("service: failed to generate id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
p := model.Project{
|
||||||
|
ID: id,
|
||||||
|
Title: title,
|
||||||
|
DateCreated: now,
|
||||||
|
DateUpdated: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = svc.projectRepo.Create(p)
|
||||||
|
if err != nil {
|
||||||
|
return model.Project{}, fmt.Errorf("service: failed to create project: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ownerUserID) > 0 {
|
||||||
|
err := svc.SetAuthor(p.ID, ownerUserID[0])
|
||||||
|
if err != nil {
|
||||||
|
return model.Project{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc Project) SetAuthor(projectID uuid.UUID, userID uuid.UUID) error {
|
||||||
|
log := svc.log.With(slog.String("project", projectID.String()), slog.String("user", userID.String()))
|
||||||
|
log.Info("Setting project owner")
|
||||||
|
defer log.Info("Finished setting project owner")
|
||||||
|
|
||||||
|
if _, err := svc.permissionRepo.GetByID(projectID, userID); err == nil {
|
||||||
|
err := svc.permissionRepo.Update(projectID, userID, model.PermissionAuthor)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("service: failed to update project author: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p := model.PermissionAuthor
|
||||||
|
|
||||||
|
err := svc.permissionRepo.Create(projectID, userID, p)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("service: failed to set project owner: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc Project) GetUserProjects(userID uuid.UUID) ([]model.Project, error) {
|
||||||
|
perms, err := svc.permissionRepo.GetByUserID(userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("service: failed to get user permissions: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := []uuid.UUID{}
|
||||||
|
for project, permissions := range perms {
|
||||||
|
if permissions.Has(model.PermissionRead) {
|
||||||
|
ids = append(ids, project)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return []model.Project{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
projects, err := svc.projectRepo.GetByIDs(ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("service: failed to get projects: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return projects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc Project) GetProject(projectID uuid.UUID) (model.Project, error) {
|
||||||
|
p, err := svc.projectRepo.GetByID(projectID)
|
||||||
|
if err != nil {
|
||||||
|
return model.Project{}, fmt.Errorf("service: failed to get project: %w", err)
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.capytal.cc/capytal/comicverse/model"
|
|
||||||
"code.capytal.cc/capytal/comicverse/repository"
|
|
||||||
"code.capytal.cc/loreddev/x/tinyssert"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Publication struct {
|
|
||||||
publicationRepo *repository.Publication
|
|
||||||
permissionRepo *repository.Permissions
|
|
||||||
|
|
||||||
log *slog.Logger
|
|
||||||
assert tinyssert.Assertions
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPublication(
|
|
||||||
publication *repository.Publication,
|
|
||||||
permissions *repository.Permissions,
|
|
||||||
logger *slog.Logger,
|
|
||||||
assertions tinyssert.Assertions,
|
|
||||||
) *Publication {
|
|
||||||
return &Publication{
|
|
||||||
publicationRepo: publication,
|
|
||||||
permissionRepo: permissions,
|
|
||||||
|
|
||||||
log: logger,
|
|
||||||
assert: assertions,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc Publication) Get(publicationID uuid.UUID) (model.Publication, error) {
|
|
||||||
p, err := svc.publicationRepo.GetByID(publicationID)
|
|
||||||
if err != nil {
|
|
||||||
return model.Publication{}, fmt.Errorf("service: failed to get publication: %w", err)
|
|
||||||
}
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc Publication) Create(title string, ownerUserID ...uuid.UUID) (model.Publication, error) {
|
|
||||||
log := svc.log.With(slog.String("title", title))
|
|
||||||
log.Info("Creating publication")
|
|
||||||
defer log.Info("Finished creating publication")
|
|
||||||
|
|
||||||
id, err := uuid.NewV7()
|
|
||||||
if err != nil {
|
|
||||||
return model.Publication{}, fmt.Errorf("service: failed to generate id: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
p := model.Publication{
|
|
||||||
ID: id,
|
|
||||||
Title: title,
|
|
||||||
DateCreated: now,
|
|
||||||
DateUpdated: now,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = svc.publicationRepo.Create(p)
|
|
||||||
if err != nil {
|
|
||||||
return model.Publication{}, fmt.Errorf("service: failed to create publication: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ownerUserID) > 0 {
|
|
||||||
err := svc.SetAuthor(p.ID, ownerUserID[0])
|
|
||||||
if err != nil {
|
|
||||||
return model.Publication{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc Publication) SetAuthor(publicationID uuid.UUID, userID uuid.UUID) error {
|
|
||||||
log := svc.log.With(slog.String("publication", publicationID.String()), slog.String("user", userID.String()))
|
|
||||||
log.Info("Setting publication owner")
|
|
||||||
defer log.Info("Finished setting publication owner")
|
|
||||||
|
|
||||||
if _, err := svc.permissionRepo.GetByID(publicationID, userID); err == nil {
|
|
||||||
err := svc.permissionRepo.Update(publicationID, userID, model.PermissionAuthor)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("service: failed to update publication author: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p := model.PermissionAuthor
|
|
||||||
|
|
||||||
err := svc.permissionRepo.Create(publicationID, userID, p)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("service: failed to set publication owner: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc Publication) ListOwnedBy(userID uuid.UUID) ([]model.Publication, error) {
|
|
||||||
perms, err := svc.permissionRepo.GetByUserID(userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("service: failed to get user permissions: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ids := []uuid.UUID{}
|
|
||||||
for publication, permissions := range perms {
|
|
||||||
if permissions.Has(model.PermissionRead) {
|
|
||||||
ids = append(ids, publication)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ids) == 0 {
|
|
||||||
return []model.Publication{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
publications, err := svc.publicationRepo.GetByIDs(ids)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("service: failed to get publications: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return publications, nil
|
|
||||||
}
|
|
||||||
Submodule smalltrip updated: e3813daa80...3d201d2122
@@ -4,12 +4,12 @@
|
|||||||
{{if and (ne . nil) (ne (len .) 0)}}
|
{{if and (ne . nil) (ne (len .) 0)}}
|
||||||
<section class="flex h-64 flex-col gap-5">
|
<section class="flex h-64 flex-col gap-5">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<h2 class="text-2xl">Publications</h2>
|
<h2 class="text-2xl">Projects</h2>
|
||||||
<form action="/publication/" method="post">
|
<form action="/p/" method="post">
|
||||||
<button
|
<button
|
||||||
class="rounded-full bg-slate-700 p-1 px-3 text-sm text-slate-100"
|
class="rounded-full bg-slate-700 p-1 px-3 text-sm text-slate-100"
|
||||||
>
|
>
|
||||||
New publication
|
New project
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -20,11 +20,11 @@
|
|||||||
<div class="w-38 grid h-full grid-rows-2 bg-slate-500">
|
<div class="w-38 grid h-full grid-rows-2 bg-slate-500">
|
||||||
<div class="bg-blue-500 p-2">Image</div>
|
<div class="bg-blue-500 p-2">Image</div>
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<a href="/publication/{{.ID}}/">
|
<a href="/p/{{.ID}}/">
|
||||||
<h3>{{.Title}}</h3>
|
<h3>{{.Title}}</h3>
|
||||||
<p class="hidden">{{.ID}}</p>
|
<p class="hidden">{{.ID}}</p>
|
||||||
</a>
|
</a>
|
||||||
<form action="/publication/{{.ID}}/" method="post">
|
<form action="/p/{{.ID}}/" method="post">
|
||||||
<input type="hidden" name="x-method" value="delete" />
|
<input type="hidden" name="x-method" value="delete" />
|
||||||
<button
|
<button
|
||||||
class="rounded-full bg-red-700 p-1 px-3 text-sm text-slate-100"
|
class="rounded-full bg-red-700 p-1 px-3 text-sm text-slate-100"
|
||||||
@@ -41,21 +41,16 @@
|
|||||||
<div
|
<div
|
||||||
class="fixed flex h-screen w-full items-center justify-center top-0 left-0"
|
class="fixed flex h-screen w-full items-center justify-center top-0 left-0"
|
||||||
>
|
>
|
||||||
<form
|
<form action="/p/" method="post" class="bg-slate-300 rounded-full">
|
||||||
action="/publication/"
|
|
||||||
method="post"
|
|
||||||
class="bg-slate-300 rounded-full"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="title"
|
name="title"
|
||||||
placeholder="Publication title"
|
placeholder="Project title"
|
||||||
value="{{randomName}}"
|
|
||||||
required
|
required
|
||||||
class="pl-5"
|
class="pl-5"
|
||||||
/>
|
/>
|
||||||
<button class="rounded-full bg-slate-700 p-2 px-5 text-slate-100">
|
<button class="rounded-full bg-slate-700 p-2 px-5 text-slate-100">
|
||||||
New publication
|
New project
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
75
templates/project.html
Normal file
75
templates/project.html
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{{define "project"}}
|
||||||
|
{{template "layout-page-start" (args "Title" .Title)}}
|
||||||
|
<div class="fixed w-full h-full bg-green-500 grid grid-cols-4 grid-rows-1">
|
||||||
|
<nav class="bg-red-500 h-full">
|
||||||
|
<h1>{{.Title}}</h1>
|
||||||
|
<p>{{.ID}}</p>
|
||||||
|
</nav>
|
||||||
|
<main class="overflow-y-scroll flex justify-center col-span-3 py-20">
|
||||||
|
<div class="flex flex-col gap-10 h-fit">
|
||||||
|
{{range $page := .Pages}}
|
||||||
|
<section id="{{$page.ID}}" class="w-fit">
|
||||||
|
<!--
|
||||||
|
INFO: The interaction form could be another page that is shown
|
||||||
|
when "Add Interaction" is clicked. Said page could be also a partial
|
||||||
|
than can replace the current image using htmx, so it is
|
||||||
|
compatible with JavaScript enabled or not.
|
||||||
|
-->
|
||||||
|
<div class="flex flex-row">
|
||||||
|
<form action="/projects/{{$.ID}}/pages/{{$page.ID}}/interactions/" method="post" class="w-100">
|
||||||
|
<div class="flex">
|
||||||
|
{{if (gt (len $page.Interactions) 0)}}
|
||||||
|
<div class="relative flex">
|
||||||
|
<div class="absolute z-2 w-full h-full top-0 left-0">
|
||||||
|
{{range $interactionID, $interaction := $page.Interactions}}
|
||||||
|
<a class="absolute" href="{{$interaction.URL}}"
|
||||||
|
style="top:{{$interaction.Y}}%;left:{{$interaction.X}}%;">
|
||||||
|
<span
|
||||||
|
class="bg-red-200 opacity-10 block w-10 h-10 transform -translate-x-[50%] -translate-y-[50%]"></span>
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<img src="/projects/{{$.ID}}/pages/{{$page.ID}}/" class="z-1 relative">
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<img src="/projects/{{$.ID}}/pages/{{$page.ID}}/" class="z-1 relative">
|
||||||
|
{{end}}
|
||||||
|
<input type="range" min="0" max="100" name="y" style="writing-mode: vertical-lr;">
|
||||||
|
</div>
|
||||||
|
<input type="range" min="0" max="100" name="x" class="w-full">
|
||||||
|
<input type="url" required name="link" class="bg-slate-300" placeholder="url of interaction">
|
||||||
|
<button class="rounded-full bg-blue-700 p-1 px-3 text-sm text-slate-100">
|
||||||
|
Add interaction
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{{if (gt (len $page.Interactions) 0)}}
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{{range $interactionID, $interaction := $page.Interactions}}
|
||||||
|
<form action="/projects/{{$.ID}}/pages/{{$page.ID}}/interactions/{{$interactionID}}/"
|
||||||
|
method="post">
|
||||||
|
<input type="hidden" name="x-method" value="delete">
|
||||||
|
<button class="rounded-full bg-red-700 p-1 px-3 text-sm text-slate-100">
|
||||||
|
🗑️{{$interaction.URL}}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<form action="/projects/{{$.ID}}/pages/{{$page.ID}}/" method="post">
|
||||||
|
<input type="hidden" name="x-method" value="delete">
|
||||||
|
<button class="rounded-full bg-red-700 p-1 px-3 text-sm text-slate-100">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{{end}}
|
||||||
|
<form action="/projects/{{.ID}}/pages/" method="post" enctype="multipart/form-data">
|
||||||
|
<input type="file" name="image" required>
|
||||||
|
<button>Add new page</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
{{template "layout-page-end"}}
|
||||||
|
{{end}}
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
{{define "publication"}} {{template "layout-page-start" (args "Title" .Title)}}
|
|
||||||
<div class="fixed w-full h-full bg-green-500 grid grid-cols-4 grid-rows-1">
|
|
||||||
<nav class="bg-red-500 h-full">
|
|
||||||
<h1>{{.Title}}</h1>
|
|
||||||
<p>{{.ID}}</p>
|
|
||||||
</nav>
|
|
||||||
<main class="overflow-y-scroll flex justify-center col-span-3 py-20">
|
|
||||||
<div class="flex flex-col gap-10 h-fit">
|
|
||||||
{{range $page := .Pages}}
|
|
||||||
<section id="{{$page.ID}}" class="w-fit">
|
|
||||||
<!--
|
|
||||||
INFO: The interaction form could be another page that is shown
|
|
||||||
when "Add Interaction" is clicked. Said page could be also a partial
|
|
||||||
than can replace the current image using htmx, so it is
|
|
||||||
compatible with JavaScript enabled or not.
|
|
||||||
-->
|
|
||||||
<div class="flex flex-row">
|
|
||||||
<form
|
|
||||||
action="/publications/{{$.ID}}/pages/{{$page.ID}}/interactions/"
|
|
||||||
method="post"
|
|
||||||
class="w-100"
|
|
||||||
>
|
|
||||||
<div class="flex">
|
|
||||||
{{if (gt (len $page.Interactions) 0)}}
|
|
||||||
<div class="relative flex">
|
|
||||||
<div class="absolute z-2 w-full h-full top-0 left-0">
|
|
||||||
{{range $interactionID, $interaction := $page.Interactions}}
|
|
||||||
<a
|
|
||||||
class="absolute"
|
|
||||||
href="{{$interaction.URL}}"
|
|
||||||
style="top:{{$interaction.Y}}%;left:{{$interaction.X}}%;"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="bg-red-200 opacity-10 block w-10 h-10 transform -translate-x-[50%] -translate-y-[50%]"
|
|
||||||
></span>
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
<img
|
|
||||||
src="/publications/{{$.ID}}/pages/{{$page.ID}}/"
|
|
||||||
class="z-1 relative"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<img
|
|
||||||
src="/publications/{{$.ID}}/pages/{{$page.ID}}/"
|
|
||||||
class="z-1 relative"
|
|
||||||
/>
|
|
||||||
{{end}}
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
name="y"
|
|
||||||
style="writing-mode: vertical-lr"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<input type="range" min="0" max="100" name="x" class="w-full" />
|
|
||||||
<input
|
|
||||||
type="url"
|
|
||||||
required
|
|
||||||
name="link"
|
|
||||||
class="bg-slate-300"
|
|
||||||
placeholder="url of interaction"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="rounded-full bg-blue-700 p-1 px-3 text-sm text-slate-100"
|
|
||||||
>
|
|
||||||
Add interaction
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{{if (gt (len $page.Interactions) 0)}}
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
{{range $interactionID, $interaction := $page.Interactions}}
|
|
||||||
<form
|
|
||||||
action="/publications/{{$.ID}}/pages/{{$page.ID}}/interactions/{{$interactionID}}/"
|
|
||||||
method="post"
|
|
||||||
>
|
|
||||||
<input type="hidden" name="x-method" value="delete" />
|
|
||||||
<button
|
|
||||||
class="rounded-full bg-red-700 p-1 px-3 text-sm text-slate-100"
|
|
||||||
>
|
|
||||||
🗑️{{$interaction.URL}}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
<form action="/publications/{{$.ID}}/pages/{{$page.ID}}/" method="post">
|
|
||||||
<input type="hidden" name="x-method" value="delete" />
|
|
||||||
<button
|
|
||||||
class="rounded-full bg-red-700 p-1 px-3 text-sm text-slate-100"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
{{end}}
|
|
||||||
<form
|
|
||||||
action="/publications/{{.ID}}/pages/"
|
|
||||||
method="post"
|
|
||||||
enctype="multipart/form-data"
|
|
||||||
>
|
|
||||||
<input type="file" name="image" required />
|
|
||||||
<button>Add new page</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
{{template "layout-page-end"}} {{end}}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{{define "publications"}} {{end}}
|
|
||||||
@@ -9,8 +9,6 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
|
||||||
"code.capytal.cc/capytal/comicverse/internals/randname"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -34,9 +32,6 @@ var (
|
|||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
},
|
},
|
||||||
"randomName": func() string {
|
|
||||||
return randname.New()
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
2
x
2
x
Submodule x updated: 2ce5d71249...6ea200aa64
Reference in New Issue
Block a user