diff --git a/src/app/components/_overlay-common.scss b/src/app/components/_overlay-common.scss new file mode 100644 index 0000000..dfff3cd --- /dev/null +++ b/src/app/components/_overlay-common.scss @@ -0,0 +1,19 @@ +$mobile-breakpoint: 1024px; + +@mixin slide-font-size($scale: 1, $desktop-scale: $scale) { + font-size: calc(#{4vw * $scale} + #{1.5vh * $scale}); + + @media (orientation: landscape) { + font-size: #{6vmin * $scale}; + } + + @media (orientation: landscape) and (max-aspect-ratio: 16/9) { + font-size: #{3vw * $scale}; + } + + @media screen and (min-width: $mobile-breakpoint) { + font-size: calc(#{3.1vw * $desktop-scale} + #{1.5vh * $desktop-scale}); + //font-size: #{4vw * $scale}; + //font-size: #{5.6vmin * $scale}; + } +} diff --git a/src/app/components/chord-view/chord-view.component.html b/src/app/components/chord-view/chord-view.component.html index e907ee8..38738c7 100644 --- a/src/app/components/chord-view/chord-view.component.html +++ b/src/app/components/chord-view/chord-view.component.html @@ -1,26 +1,33 @@
-
+
{{ tag.text }}
-
+
+
- diff --git a/src/app/components/chord-view/chord-view.component.scss b/src/app/components/chord-view/chord-view.component.scss index 72ae06d..73b0da8 100644 --- a/src/app/components/chord-view/chord-view.component.scss +++ b/src/app/components/chord-view/chord-view.component.scss @@ -1,12 +1,19 @@ +@import '../overlay-common'; + .transpose { - margin-left: 25px; display: flex; - flex-direction: column; - font-size: 3rem; + justify-content: center; + align-items: center; + font-size: 2rem; + mat-icon { - font-size: 3rem; + transform: scale(1.5); } - span { - margin-left: 17px; + + @media screen and (min-width: $mobile-breakpoint) { + font-size: 3rem; + mat-icon { + transform: scale(2); + } } } diff --git a/src/app/components/chord-view/chordpro.pipe.ts b/src/app/components/chord-view/chordpro.pipe.ts index fa6c36b..754c110 100644 --- a/src/app/components/chord-view/chordpro.pipe.ts +++ b/src/app/components/chord-view/chordpro.pipe.ts @@ -52,6 +52,8 @@ export class ChordProPipe implements PipeTransform { notesSharpNotation = {}; notesFlatNotation = {}; + private fillHtml = ``; + decodeHTML(value: string) { const tempElement = document.createElement('div'); tempElement.innerHTML = value; @@ -146,40 +148,103 @@ export class ChordProPipe implements PipeTransform { // becuase it gets messed up when a chord is placed on it.. // shouldn't be relevant if we actually get chordpro format song = this.decodeHTML(song); - const comp = this; if (!song) { return ''; } let chordText = ''; - let lastChord = ''; - if (!song.match(comp.chordRegex)) { + if (!song.match(this.chordRegex)) { return `
${song}
`; } - song.split(comp.chordRegex).forEach((part, index) => { - if (index % 2 === 0) { - // text - if (lastChord) { - chordText += `${part.substring(0, 1)}${part.substring(1)}`; - lastChord = ''; - } else { - chordText += part; + // Processing backwards so we can better identify where chords should overlap lyric letters + // or insert a space in lyrics + song.split(/\n/).reverse().forEach((row, index) => { + + chordText = `
${index > 0 ? '
' : ''}` + chordText; + const rowParts = row.split(this.chordRegex); + let lastPart; + + for (let i = rowParts.length - 1, r = 0, isFirst = true; i >= -1; i--, r++ ) { + if (!isFirst) { + chordText = this._processChordRow(nHalfSteps, rowParts[i], i, lastPart, r) + chordText; } - } else { + + lastPart = rowParts[i]; + isFirst = false; + } + + chordText = '
' + chordText; + }); + return `
${chordText}
`; + } + + protected _processChordRow(nHalfSteps, part, index, lastPart, reverseIndex) { + if (index % 2 !== 0) { + if (index > 0) { // chord - lastChord = part.replace(/[[]]/, ''); + let chord = part.replace(/[[]]/, ''); if (nHalfSteps !== 0) { - lastChord = lastChord.split('/').map(chord => { - const chordRoot = comp.chordRoot(chord); - const newRoot = comp.transposeChord(chordRoot, nHalfSteps); - return newRoot + comp.restOfChord(chord); + chord = chord.split('/').map(chordPart => { + const chordRoot = this.chordRoot(chordPart); + const newRoot = this.transposeChord(chordRoot, nHalfSteps); + return newRoot + this.restOfChord(chordPart); }).join('/'); } // use proper symbols - lastChord = lastChord.replace(/b/g, '♭'); - lastChord = lastChord.replace(/#/g, '♯'); + chord = chord.replace(/b/g, '♭'); + chord = chord.replace(/#/g, '♯'); + const textFirstLetter = `${lastPart.substring(0, 1)}`; + const textRest = lastPart.substring(1); + const isChordMusicKey = chord.startsWith('='); + const chordLength = chord.length; + const isFirstChordAfterRealWord = (lastPart.length && (reverseIndex === 1)); + const shouldOverlapChord = (!isChordMusicKey) && (isFirstChordAfterRealWord || (lastPart.length > chord.length)); + const isChordOnly = !lastPart.trim().length; + + const chordClass = (shouldOverlapChord && 'overlap-chord' || '') + (isChordOnly && ' chord-only' || ''); + + const textLength = 1 + textRest.length; + const fillHtmlNeededLength = (!shouldOverlapChord && (textLength < chordLength)) ? chordLength - 1 : (chordLength - 1); + let fillHtml = !shouldOverlapChord && !isChordMusicKey ? this._makeFillHtml(fillHtmlNeededLength || 1) : ''; + let fillHtmlLength = fillHtml?.length ?? 0; + const finalTextLength = 1 + textRest.length + fillHtmlLength; + const needExtraFill = (!shouldOverlapChord && (finalTextLength <= chordLength)); + + if (needExtraFill) { + fillHtml = this._makeFillHtml(fillHtmlLength + 1); + fillHtmlLength++; + chord += ' '; + } + + if (!lastPart || (textFirstLetter === ' ')) { + fillHtml = ' '.repeat(fillHtmlLength); + } + + if ((fillHtmlLength === 1 && chordLength < 2)) { + // To match text separator + chord += ' '; + } + + return `${chord}${textFirstLetter}${fillHtml}${textRest}`; + } else { + return `${lastPart}`; } - }); - return `
${chordText}
`; + } + return ''; + } + + protected _makeFillHtml(length) { + if (length >= 3) { + let text = ''; + const middle = Math.floor(length / 2); + + for (let i = 0; i < length; i++) { + text += (i === middle) ? '\u2014' : ' '; + } + + return text; + } else { + return '\u00B7' + (length === 2 ? ' ' : ''); + } } } diff --git a/src/app/components/chord-view/chordpro.scss b/src/app/components/chord-view/chordpro.scss index 0540bcd..7757995 100644 --- a/src/app/components/chord-view/chordpro.scss +++ b/src/app/components/chord-view/chordpro.scss @@ -1,23 +1,93 @@ +@import '../overlay-common'; + .song { white-space: pre-wrap; + .with-chords { - line-height: 2; + line-height: 1; + font-family: monospace; + padding-top: 0.45em; // To avoid chord overlapping top bar + @include slide-font-size(0.85, 0.725); + + > span { + vertical-align: bottom; + } } - - span[data-chord]:before { - position: relative; - top: -1em; + + &-row { display: inline-block; - content: attr(data-chord); - width: 0; - color: yellow; + } + + span[data-chord]{ + display: inline; + position: relative; + white-space: nowrap; + + .text, .chord { + line-height: 1em; + height: 1em; + line-height: 2.4em; + } + + .first-letter { + white-space: pre-wrap; + } + + .chord { + color: yellow; + display: inline-block; + transform: translateY(-100%); + white-space: pre; + } + + // Chords that invades next text + &.overlap-chord { + position: relative; + + .chord { + width: 0; + } + } + + &:not(.overlap-chord) { + display: inline-flex; + flex-direction: column; + + .text { + position: absolute; + left: 0; + } + } + + // Chords without text + &.chord-only { + display: inline-flex; + flex-direction: column; + margin-right: 0.3em; + + .chord { + position: static; + } + } + } } .nextSlides { .song { - span[data-chord]:before { - color: gray; + span[data-chord] { + .chord { + color: gray; + } + + .fill .fill-inner { + background-color: gray; + } } + + } + + .slide .with-chords { + @include slide-font-size(0.75, 0.65); } } \ No newline at end of file diff --git a/src/app/components/overlay.scss b/src/app/components/overlay.scss index d3950e5..a620375 100644 --- a/src/app/components/overlay.scss +++ b/src/app/components/overlay.scss @@ -1,3 +1,5 @@ +@import "./overlay-common"; + .overlay { background: black; width: 100%; @@ -9,71 +11,158 @@ overflow: hidden; color: white; display: flex; + justify-content: flex-start; flex-direction: row; - justify-content: space-between; + + &-content { + width: 100%; + max-width: 100%; + flex: 1; + + @media (orientation: portrait) { + overflow: hidden; + flex: 1; + } + + .tags { + margin-top: 0.5rem; + margin-bottom: 0.5rem; + display: flex; + flex-direction: row; + justify-content: flex-start; + color: green; + align-items: center; + @include slide-font-size(1); + + @media screen and (min-width: $mobile-breakpoint) { + margin-top: 1rem; + margin-bottom: 1rem; + font-size: 3rem; + } + + span { + margin-left: 1rem; + &.active { + color: lightgreen; + font-weight: bold; + } + } + } + + } + + .sidebar { + padding: 0.8rem; + max-width: 30%; + margin-top: 1em; + overflow-y: auto; + + .notes { + @include slide-font-size(0.9); + line-height: 1; + + color: salmon; + text-align: right; + } + + @media screen and (min-width: $mobile-breakpoint) { + margin-top: 4rem; + } + + @media (orientation: portrait) { + max-width: none; + max-height: 20%; + margin-bottom: 3.75rem; + background-color: rgb(64, 64, 64); + + .notes { + text-align: left; + } + } + } + + @media (orientation: portrait) { + flex-direction: column; + flex-wrap: wrap; + } + + .slide { + line-height: 1.2; + white-space: pre-line; + margin: 0; + + &.first { + margin-top: 1rem; + } + + @include slide-font-size(); + } + } -.sidebar { - margin: 1rem; +.toolbar { + padding: 0.8rem; display: flex; - flex-direction: column; - justify-content: space-between; - width: 30%; -} + justify-content: flex-start; + align-items: center; + width: auto; + gap: 1.5rem; -.time { - font-size: 3rem; - color: yellow; - text-align: right; -} + .back-button { + background: #fff; + } -.notes { - margin-top: 1em; - font-size: 3rem; - line-height: 3rem; - color: salmon; - text-align: right; + @media screen and (max-width: ($mobile-breakpoint - 0.125px)){ + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: rgba(64, 64, 64, 0.5); + backdrop-filter: blur(2px); + padding: 0.6rem; + } + + @media screen and (min-width: $mobile-breakpoint) { + position: absolute; + top: 0; + right: 0; + } + + @media screen and (max-width: ($mobile-breakpoint - 0.125px)) and (orientation: landscape) { + left: auto; + border-top-left-radius: 0.5rem; + } } .close { text-align: right; } -.tags { - margin-top: 1rem; - margin-bottom: 1rem; +.time { + color: yellow; + text-align: right; + white-space: nowrap; + flex: 1; display: flex; - flex-direction: row; - justify-content: flex-start; - color: green; - font-size: 4rem; - span { - margin-left: 1rem; - &.active { - color: lightgreen; - font-weight: bold; - } - } -} + justify-content: flex-end; + align-items: center; + padding-right: 0.8rem; + font-size: 2rem; -.slide { - font-size: 3rem; - white-space: pre-line; - margin: 0; - &.first { - margin-top: 1rem; + @media screen and (min-width: $mobile-breakpoint) { + font-size: 3rem; } } .container { - margin-left: 1rem; + margin: 0 1rem; } .nextSlides { - font-size: 2rem; margin-top: 1rem; color: grey; + .slide { - font-size: 2rem; + @include slide-font-size(0.75); } } diff --git a/src/app/components/stage-view/stage-view.component.html b/src/app/components/stage-view/stage-view.component.html index 48ac9db..e750275 100644 --- a/src/app/components/stage-view/stage-view.component.html +++ b/src/app/components/stage-view/stage-view.component.html @@ -1,5 +1,5 @@
-
+
{{ tag.text }}
@@ -26,11 +26,23 @@
- diff --git a/src/app/components/stage-view/stage-view.component.scss b/src/app/components/stage-view/stage-view.component.scss index 330f55c..eb57666 100644 --- a/src/app/components/stage-view/stage-view.component.scss +++ b/src/app/components/stage-view/stage-view.component.scss @@ -18,3 +18,11 @@ .next-slides-text { font-size: 1.4rem; } + +.toolbar { + .show-notes { + &-disabled { + background: white; + } + } +} diff --git a/src/app/components/stage-view/stage-view.component.ts b/src/app/components/stage-view/stage-view.component.ts index cd81399..294b347 100644 --- a/src/app/components/stage-view/stage-view.component.ts +++ b/src/app/components/stage-view/stage-view.component.ts @@ -19,6 +19,7 @@ export class StageViewComponent implements OnInit { activeSlide = 0; tags: Tag[] = []; time = new Date(); + showNotes = true; constructor(public openlpService: OpenLPService) { setInterval(() => this.time = new Date(), 1000);