From 22bb134198fb05461d64cec9c69826ed663811d8 Mon Sep 17 00:00:00 2001
From: Moritz Vifian <moritz.vifian@gmx.ch>
Date: Sun, 19 Jan 2025 19:06:16 +0100
Subject: [PATCH 1/5] title not at side

wip

annoying to scroll
---
 app/client/chordsheet.less | 14 +++++++++-----
 1 file changed, 9 insertions(+), 5 deletions(-)

diff --git a/app/client/chordsheet.less b/app/client/chordsheet.less
index 878fbef..3828bf8 100644
--- a/app/client/chordsheet.less
+++ b/app/client/chordsheet.less
@@ -54,8 +54,9 @@
     }
   }
 
-  .inlineRefs .ref,
-  h3 {
+  .hideRefs h3,
+  .inlineRefs h3,
+  .inlineRefs .ref {
     font-size: 1em;
     margin: 0;
 
@@ -224,9 +225,12 @@
     .select(none);
     cursor: pointer;
 
-    .ref,
     .inlineReference {
-      display: none;
+      filter: grayscale(0.3);
+      background-color:var(--bg-list);
+      h3 {
+        display: none;
+      }
     }
 
     // interim UI for to know which datapoint to delete
@@ -422,7 +426,7 @@
     }
   }
   .song-video-preview {
-    position: absolute;
+    position: fixed;
     top: 0;
     right: 50%;
   }

From e9384bbd6696cd50dad1f09af56d453534022485 Mon Sep 17 00:00:00 2001
From: Moritz Vifian <moritz.vifian@gmx.ch>
Date: Mon, 20 Jan 2025 02:12:00 +0100
Subject: [PATCH 2/5] showing data points

---
 app/.vscode/launch.json        | 17 +++++++++++++++++
 app/client/chordsheet.less     | 10 +++++-----
 app/imports/api/collections.ts | 15 ++++++++++++---
 app/imports/api/extractData.ts |  9 +++++++++
 app/imports/ui/Preview.tsx     | 35 ++++++++++++++++++++++------------
 app/imports/ui/YtInter.tsx     | 10 +---------
 6 files changed, 67 insertions(+), 29 deletions(-)
 create mode 100644 app/.vscode/launch.json
 create mode 100644 app/imports/api/extractData.ts

diff --git a/app/.vscode/launch.json b/app/.vscode/launch.json
new file mode 100644
index 0000000..d6f936d
--- /dev/null
+++ b/app/.vscode/launch.json
@@ -0,0 +1,17 @@
+{
+    // Use IntelliSense to learn about possible attributes.
+    // Hover to view descriptions of existing attributes.
+    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "name": "Attach",
+            "port": 9229,
+            "request": "attach",
+            "skipFiles": [
+                "<node_internals>/**"
+            ],
+            "type": "node"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/app/client/chordsheet.less b/app/client/chordsheet.less
index 3828bf8..43b4d72 100644
--- a/app/client/chordsheet.less
+++ b/app/client/chordsheet.less
@@ -227,14 +227,14 @@
 
     .inlineReference {
       filter: grayscale(0.3);
-      background-color:var(--bg-list);
+      background-color: var(--bg-list);
       h3 {
         display: none;
       }
     }
 
     // interim UI for to know which datapoint to delete
-    .line::before {
+    .line .rowidx {
       content: attr(data-line-cnt);
       opacity: 0.5;
       position: absolute;
@@ -242,10 +242,10 @@
       text-align: right;
       width: 2em;
       line-height: 1.4em;
-    }
 
-    .line:hover::before {
-      opacity: 1;
+      &:hover {
+        opacity: 1;
+      }
     }
   }
 
diff --git a/app/imports/api/collections.ts b/app/imports/api/collections.ts
index 00f393a..ef3bd4b 100644
--- a/app/imports/api/collections.ts
+++ b/app/imports/api/collections.ts
@@ -4,6 +4,7 @@ import { parse, HTMLElement } from "node-html-parser";
 import slug from "slug";
 import { Meteor } from "meteor/meteor";
 import { parseRechordsDown } from "./parseRechordsDown";
+import { extractData } from "./extractData";
 
 const DATACHORD = "data-chord";
 
@@ -11,7 +12,7 @@ function isDefined<T>(a: T | null | undefined): a is T {
   return a !== null && a !== undefined;
 }
 
-export const rmd_version = 11;
+export const rmd_version = 12;
 
 export class Song {
   _id?: string;
@@ -32,6 +33,8 @@ export class Song {
   last_editor?: string;
 
   revision_cache?: Revision[];
+
+  video_data?: { ytId: string; anchors: [number, number][] };
   has_video: boolean = false;
 
   constructor(doc: { text: string }) {
@@ -144,6 +147,12 @@ export class Song {
         .getElementsByTagName("pre")?.[0]
         ?.childNodes[0]?.rawText.includes("language-yt") ?? false;
 
+    if (this.has_video) {
+      let data = dom.getElementsByTagName("pre")?.[0]?.childNodes[0]?.rawText;
+      data = data.replace('<code class="yt language-yt">', "");
+      data = data.replace("</code>", "");
+      this.video_data = extractData(data);
+    }
     this.tags = RmdHelpers.collectTags(dom);
     this.chords = RmdHelpers.collectChords(dom);
     this.parsed_rmd_version = rmd_version;
@@ -155,7 +164,7 @@ export class Song {
         { of: this._id },
         {
           sort: { timestamp: -1 },
-        },
+        }
       ).fetch();
     }
     return this.revision_cache;
@@ -192,7 +201,7 @@ export class RmdHelpers {
       .map((li) =>
         Array.from(li.childNodes)
           .map((child) => child.textContent)
-          .join(":"),
+          .join(":")
       );
   }
 
diff --git a/app/imports/api/extractData.ts b/app/imports/api/extractData.ts
new file mode 100644
index 0000000..730d9fa
--- /dev/null
+++ b/app/imports/api/extractData.ts
@@ -0,0 +1,9 @@
+
+export function extractData(data: string): {
+  ytId: string;
+  anchors: [number, number][];
+} {
+  const [ytId, ..._anchors] = data.split("\n");
+  const anchors = _anchors.map((line) => line.split(/\s+/).map(parseFloat));
+  return { ytId, anchors };
+}
diff --git a/app/imports/ui/Preview.tsx b/app/imports/ui/Preview.tsx
index 1ae0944..505f362 100644
--- a/app/imports/ui/Preview.tsx
+++ b/app/imports/ui/Preview.tsx
@@ -21,7 +21,7 @@ const nodeText = (node) => {
   return node.children.reduce(
     (out, child) =>
       (out += child.type == "text" ? child.data : nodeText(child)),
-    "",
+    ""
   );
 };
 
@@ -33,10 +33,18 @@ interface P {
 
 export default (props: P) => {
   const html = useRef<HTMLSelectElement>(null);
-  const [currentPlayTime, setCurrentPlayTime] = useState<number | undefined>(0);
 
+  const [currentPlayTime, setCurrentPlayTime] = useState<number | undefined>(0);
   const [isVideoActive, setVideoActive] = useState<boolean>(false);
 
+  const anchorTimeByLine = new Map()
+  if(props.song.has_video) {
+    props.song.video_data?.anchors?.forEach(
+      ([line,time]) => anchorTimeByLine.set(line,time)
+    )
+  }
+
+
   useEffect(() => {
     const traverse = (node: HTMLElement): void => {
       for (const child of node.children) {
@@ -85,7 +93,7 @@ export default (props: P) => {
       (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey)
     ) {
       const line = (event.target as HTMLElement).closest(
-        "span.line",
+        "span.line"
       ) as HTMLSpanElement;
       const selectedLine = Number.parseInt(line.dataset.lineCnt ?? "", 10);
       if (event.shiftKey) {
@@ -145,7 +153,7 @@ export default (props: P) => {
       node,
       guessedChord + "|",
       offset,
-      skipWhitespace,
+      skipWhitespace
     );
     props.updateHandler(md);
   };
@@ -180,7 +188,7 @@ export default (props: P) => {
 
   const offsetChordPosition = (
     event: React.SyntheticEvent<HTMLElement>,
-    offset: number,
+    offset: number
   ) => {
     console.log("offsetchorspos");
     event.currentTarget.removeAttribute("data-initial");
@@ -263,7 +271,7 @@ export default (props: P) => {
     segment: Element,
     chord: string,
     offset = 0,
-    skipWhitespace = true,
+    skipWhitespace = true
   ) => {
     const pos = locate(segment);
 
@@ -422,7 +430,11 @@ export default (props: P) => {
           "class" in node.attribs &&
           "line" == node.attribs.class
         ) {
+          const rowidx = parseInt(node.attribs["data-line-cnt"],10);
+          const timeEntry = anchorTimeByLine.get(rowidx)
+          const time = <span className="rowidx">{rowidx}<b>{timeEntry||0}</b></span>
           // Fakey syllable to allow appended chords
+          node.children.unshift(time);
           node.children.push(<i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</i>);
         } else if (node.name == "pre") {
           if (node.children.length != 1) return node;
@@ -430,6 +442,7 @@ export default (props: P) => {
           if (!("class" in code.attribs)) return node;
           const classes = code.attribs["class"];
 
+          // todo: use preparsed string from collection
           if (classes.includes("language-yt")) {
             const data = (code.firstChild as DH.DataNode).data as string;
             return (
@@ -465,7 +478,8 @@ export default (props: P) => {
                   <div>
                     <div>Click to a line in the song text</div>
                     <div>
-                      <b>Ctrl + Click: </b>Add Time Anchor<br />
+                      <b>Ctrl + Click: </b>Add Time Anchor
+                      <br />
                       <b>Shift + Click: </b>Play from here
                     </div>
                   </div>
@@ -510,12 +524,12 @@ export default (props: P) => {
 
   const [coords, setCoords] = useState({ x: 0, y: 0, h: 0 });
   const handleMouseMove = (
-    event: React.MouseEvent<HTMLElement, MouseEvent>,
+    event: React.MouseEvent<HTMLElement, MouseEvent>
   ) => {
     // next line
 
     const line = (event.target as HTMLElement).closest(
-      "span.line",
+      "span.line"
     ) as HTMLSpanElement;
 
     console.log(line);
@@ -586,9 +600,6 @@ export default (props: P) => {
             }}
             className="time-insert-indicator"
           >
-            <span>
-              {currentPlayTime?.toFixed(1)}
-            </span>
           </div>
         )}
       </div>
diff --git a/app/imports/ui/YtInter.tsx b/app/imports/ui/YtInter.tsx
index 46088a8..60dae9c 100644
--- a/app/imports/ui/YtInter.tsx
+++ b/app/imports/ui/YtInter.tsx
@@ -3,6 +3,7 @@ import * as React from "react";
 import YouTube from "react-youtube";
 import { linInterpolation } from "./time2line";
 import { VideoContext } from "/imports/ui/App";
+import { extractData } from "../api/extractData";
 
 export const YtInter: FC<{
   data: string;
@@ -59,15 +60,6 @@ export const YtInter: FC<{
   }
 };
 
-export function extractData(data: string): {
-  ytId: string;
-  anchors: [number, number][];
-} {
-  const [ytId, ..._anchors] = data.split("\n");
-  const anchors = _anchors.map((line) => line.split(/\s+/).map(parseFloat));
-  return { ytId, anchors };
-}
-
 export function appendTime(
   md: string,
   lastTime: number,

From fab024aa5e02ca33345ea6f489f9bdddd13b0e9c Mon Sep 17 00:00:00 2001
From: Moritz Vifian <moritz.vifian@gmx.ch>
Date: Mon, 20 Jan 2025 02:36:10 +0100
Subject: [PATCH 3/5] wip: better timeindication

---
 app/client/chordsheet.less | 9 +++++++++
 app/imports/ui/Preview.tsx | 2 +-
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/app/client/chordsheet.less b/app/client/chordsheet.less
index 43b4d72..1ecdacc 100644
--- a/app/client/chordsheet.less
+++ b/app/client/chordsheet.less
@@ -243,6 +243,15 @@
       width: 2em;
       line-height: 1.4em;
 
+      b {
+        color: var(--text-inverted);
+        padding: 2px;
+        width: 50px;
+        border-top-right-radius: 10px;
+        border-bottom-right-radius: 10px;
+        background-color: var(--accent-flat);
+      }
+
       &:hover {
         opacity: 1;
       }
diff --git a/app/imports/ui/Preview.tsx b/app/imports/ui/Preview.tsx
index 505f362..cef6d53 100644
--- a/app/imports/ui/Preview.tsx
+++ b/app/imports/ui/Preview.tsx
@@ -432,7 +432,7 @@ export default (props: P) => {
         ) {
           const rowidx = parseInt(node.attribs["data-line-cnt"],10);
           const timeEntry = anchorTimeByLine.get(rowidx)
-          const time = <span className="rowidx">{rowidx}<b>{timeEntry||0}</b></span>
+          const time = <span className="rowidx">{rowidx}{timeEntry&&<b>{timeEntry}</b>}</span>
           // Fakey syllable to allow appended chords
           node.children.unshift(time);
           node.children.push(<i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</i>);

From a2bd731dc1e27d811e62f2b0e39afa07c486ea4c Mon Sep 17 00:00:00 2001
From: Moritz Vifian <moritz.vifian@gmx.ch>
Date: Mon, 20 Jan 2025 22:29:29 +0100
Subject: [PATCH 4/5] removing duplicate lines

---
 app/imports/ui/YtInter.tsx | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/app/imports/ui/YtInter.tsx b/app/imports/ui/YtInter.tsx
index 60dae9c..2a82fd2 100644
--- a/app/imports/ui/YtInter.tsx
+++ b/app/imports/ui/YtInter.tsx
@@ -70,9 +70,13 @@ export function appendTime(
   if (!match) return;
   const data = match[1];
 
-  const { ytId, anchors } = extractData(data);
-  anchors.push([selectedLine, Math.round(lastTime * 10) / 10]);
+  const { ytId, anchors: anchors_ } = extractData(data);
+  const anchorMap = new Map(anchors_)
+  // anchors.push([selectedLine, Math.round(lastTime * 10) / 10]);
+  // avoid duplicate
+  anchorMap.set(selectedLine, Math.round(lastTime*10)/10)
 
+  const anchors = [...anchorMap.entries()]
   anchors.sort(([a, _], [b, __]) => a - b);
 
   const ytOut = `~~~yt

From 7a2cc9029857baa10d92d69619709de3d4df408f Mon Sep 17 00:00:00 2001
From: Moritz Vifian <moritz.vifian@gmx.ch>
Date: Wed, 22 Jan 2025 23:12:28 +0100
Subject: [PATCH 5/5] editing without special keys

---
 app/client/chordsheet.less |  80 +++++++++++++--------------
 app/imports/ui/Preview.tsx | 108 +++++++++----------------------------
 2 files changed, 63 insertions(+), 125 deletions(-)

diff --git a/app/client/chordsheet.less b/app/client/chordsheet.less
index 1ecdacc..9bdecd3 100644
--- a/app/client/chordsheet.less
+++ b/app/client/chordsheet.less
@@ -219,9 +219,7 @@
     }
   }
 
-  .interactive,
-  .addanchor,
-  .playfromline {
+  .interactive {
     .select(none);
     cursor: pointer;
 
@@ -235,52 +233,52 @@
 
     // interim UI for to know which datapoint to delete
     .line .rowidx {
-      content: attr(data-line-cnt);
+      width: 0;
+      overflow: hidden;
+      display: flex;
+      align-items: center;
+      justify-content: end;
       opacity: 0.5;
+    }
+    &.isVideoActive .line .rowidx {
+      width: 100px;
       position: absolute;
-      margin-left: -3.8em;
+      margin-left: -110px;
       text-align: right;
-      width: 2em;
       line-height: 1.4em;
 
       b {
+        &.newtime {
+          opacity: 0.1;
+        }
+        line-height: 15px;
         color: var(--text-inverted);
-        padding: 2px;
-        width: 50px;
-        border-top-right-radius: 10px;
-        border-bottom-right-radius: 10px;
+        padding: 3px;
+        margin-right: 2px;
+        border-top-left-radius: 10px;
+        border-bottom-left-radius: 10px;
         background-color: var(--accent-flat);
       }
 
       &:hover {
         opacity: 1;
+
+        &:not(.newtime) {
+        text-decoration: line-through;
+        }
       }
     }
   }
 
-  // .addanchor {
-  //   .line:hover {
-  //     border-top: 2px solid var(--accent);
-  //     &::before {
-  //       content: "+" attr(data-line-cnt);
-  //     }
-  //   }
-  // }
-  .playfromline {
-    .line {
-      i {
-        transition: color 0.2s;
-      }
-      &:hover i {
-        color: var(--accent);
-        transition: color 0s;
-      }
+  span.line :not(li) {
+    transition: color 0.2s;
+    &:hover :not(li) {
+      color: var(--accent);
+      transition: color 0s;
     }
   }
 
-  .interactive,
-  .addanchor,
-  .playfromline {
+  .interactive {
     i {
       line-height: 1.4em;
       text-indent: 0 !important;
@@ -400,6 +398,16 @@
     } // before aka. chords
   } // i
 
+  .interactive .line {
+    // https://stackoverflow.com/questions/22270078/css-hover-on-a-div-but-not-if-hover-on-his-children
+    &:hover:not(:has(*:hover)) {
+      &::after {
+        content: "++++";
+        border-top: solid 3px;
+      }
+    }
+  }
+
   .interactive .line i .before {
     // Re-allow chord text selection when editing.
     .select(text);
@@ -456,16 +464,4 @@
       aspect-ratio: 3/2;
     }
   }
-  .time-insert-indicator {
-    display: flex;
-    align-items: end;
-    span {
-      color: var(--text-inverted);
-      padding: 2px;
-      width: 50px;
-      border-top-right-radius: 10px;
-      border-bottom-right-radius: 10px;
-      background-color: var(--accent-flat);
-    }
-  }
 } // .chordsheet
diff --git a/app/imports/ui/Preview.tsx b/app/imports/ui/Preview.tsx
index cef6d53..998608f 100644
--- a/app/imports/ui/Preview.tsx
+++ b/app/imports/ui/Preview.tsx
@@ -37,14 +37,13 @@ export default (props: P) => {
   const [currentPlayTime, setCurrentPlayTime] = useState<number | undefined>(0);
   const [isVideoActive, setVideoActive] = useState<boolean>(false);
 
-  const anchorTimeByLine = new Map()
-  if(props.song.has_video) {
-    props.song.video_data?.anchors?.forEach(
-      ([line,time]) => anchorTimeByLine.set(line,time)
-    )
+  const anchorTimeByLine = new Map<number, number>();
+  if (props.song.has_video) {
+    props.song.video_data?.anchors?.forEach(([line, time]) =>
+      anchorTimeByLine.set(line, time)
+    );
   }
 
-
   useEffect(() => {
     const traverse = (node: HTMLElement): void => {
       for (const child of node.children) {
@@ -88,24 +87,23 @@ export default (props: P) => {
   const [selectLine, setSelectLine] = useState({ selectedLine: 0 });
 
   const handleClick = (event: React.MouseEvent<HTMLElement>) => {
-    if (
-      isVideoActive &&
-      (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey)
-    ) {
-      const line = (event.target as HTMLElement).closest(
-        "span.line"
-      ) as HTMLSpanElement;
+    if (isVideoActive) {
+      const target = event.target as HTMLElement;
+
+      const line = target.closest("span.line") as HTMLSpanElement;
       const selectedLine = Number.parseInt(line.dataset.lineCnt ?? "", 10);
-      if (event.shiftKey) {
+
+      if (target.matches("span.line")) {
         setSelectLine({ selectedLine });
-      } else {
+        return;
+      } else if (target.matches(".rowidx,.rowidx *")) {
         const md = props.md;
         const newMd = appendTime(md, currentPlayTime, selectedLine);
         if (newMd) {
           props.updateHandler ? props.updateHandler(newMd) : null;
         }
+        return;
       }
-      return;
     }
     const node: Element = event.target as Element;
     if (!(node instanceof HTMLElement) || node.tagName != "I") return;
@@ -430,9 +428,16 @@ export default (props: P) => {
           "class" in node.attribs &&
           "line" == node.attribs.class
         ) {
-          const rowidx = parseInt(node.attribs["data-line-cnt"],10);
-          const timeEntry = anchorTimeByLine.get(rowidx)
-          const time = <span className="rowidx">{rowidx}{timeEntry&&<b>{timeEntry}</b>}</span>
+          const rowidx = parseInt(node.attribs["data-line-cnt"], 10);
+          const timeEntry = anchorTimeByLine.get(rowidx);
+          const quark = 234.2;
+          quark.toFixed(1);
+          const time = (
+            <span className="rowidx">
+              {timeEntry ? <b>{timeEntry.toFixed(1)}</b> : <b className="newtime">+++</b>}
+              <span>{rowidx}</span>
+            </span>
+          );
           // Fakey syllable to allow appended chords
           node.children.unshift(time);
           node.children.push(<i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</i>);
@@ -522,52 +527,6 @@ export default (props: P) => {
     },
   });
 
-  const [coords, setCoords] = useState({ x: 0, y: 0, h: 0 });
-  const handleMouseMove = (
-    event: React.MouseEvent<HTMLElement, MouseEvent>
-  ) => {
-    // next line
-
-    const line = (event.target as HTMLElement).closest(
-      "span.line"
-    ) as HTMLSpanElement;
-
-    console.log(line);
-    if (line) {
-      const cl = line.getBoundingClientRect();
-      console.log(cl);
-      // setCoords({ x: cl.left, y: cl.top });
-      setCoords({
-        x: line.offsetLeft,
-        y: line.offsetTop,
-        h: line.offsetHeight,
-      });
-      handleSpecialKey(event);
-    }
-  };
-  const handleSpecialKey = (event: KeyboardEvent | MouseEvent) => {
-    if (!isVideoActive) {
-      return;
-    }
-    if (event.ctrlKey || event.metaKey) {
-      setSpecialKey("ctrl");
-    } else if (event.shiftKey) {
-      setSpecialKey("shift");
-    } else {
-      setSpecialKey("");
-    }
-  };
-  const [specialKey, setSpecialKey] = useState("");
-
-  useDocumentListener("keydown", handleSpecialKey);
-  useDocumentListener("keyup", handleSpecialKey);
-
-  // // changing window or going into iframe otherwise leaves last pressed key
-  // useDocumentListener("blur", () => {
-  //   setSpecialKey("");
-  // });
-  // needs a better / more general solution
-
   return (
     <VideoContext.Provider
       value={{
@@ -578,30 +537,13 @@ export default (props: P) => {
     >
       <div className="content" id="chordsheet">
         <section
-          className={classNames({
-            interactive: specialKey === "",
-            addanchor: specialKey === "ctrl",
-            playfromline: specialKey === "shift",
-          })}
+          className={classNames("interactive", { isVideoActive })}
           id="chordsheetContent"
           onClick={(e) => handleClick(e)}
-          onMouseMove={handleMouseMove}
           ref={html}
         >
           {vdom}
         </section>
-        {specialKey === "ctrl" && (
-          <div
-            style={{
-              position: "absolute",
-              left: `${coords.x - 10}px`,
-              top: `${coords.y}px`,
-              height: `${coords.h}px`,
-            }}
-            className="time-insert-indicator"
-          >
-          </div>
-        )}
       </div>
     </VideoContext.Provider>
   );