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> </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> </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> </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> );