From b4eb5a145100a7cc78c6ea15448cc5456e983746 Mon Sep 17 00:00:00 2001 From: torcado194 Date: Thu, 14 Apr 2022 21:27:05 -0400 Subject: [PATCH] Locked tracks, preview tracks Preview stripe colors Better horizontal spacing Sticky media on horizontal layout Better responsive width for controls/tracks Better volume positioning Percentage-based scrubber values for better responsive positioning --- README.md | 47 +++++++- index.html | 256 ++++++++++++++++++++++++++++++++++++++----- src/fonts.css | 43 +++++++- src/fonts/icons.eot | Bin 6562 -> 9638 bytes src/fonts/icons.svg | 11 ++ src/fonts/icons.ttf | Bin 6404 -> 9480 bytes src/fonts/icons.woff | Bin 6480 -> 9556 bytes 7 files changed, 325 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index dbf2992..fc55187 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ And that's it! you can upload the files to your website, or zip them up and uplo `file` > The name of the file in the /media folder +> Not required if entry is set to "locked" \ `title` (optional) @@ -121,12 +122,48 @@ And that's it! you can upload the files to your website, or zip them up and uplo `type` (optional) > The type of the media. > Can be either `"audio"` or `"video"` -> Defaults to "audio" +> Defaults to `"audio"` \ `info` (optional) > Extra text for this file, such as for lyrics or artist attributions. This will be optionally shown over/under the cover image, or togglable below the track. +\ +`locked` (optional) +> If `true`, sets this file as "locked" preventing it from being playable. +> Can be either `true` or `false` +> Defaults to `false` +> Enable this if e.g. you want to show that this file would be available in the purchased download of your album. +> Note: the `file` field is not required if this is enabled, you shouldn't include the file in the player. + +***preview options*** + +*a preview is a section of a full track, used if e.g. you want to include a snippet of a track that would be available in the purchased download of your album.* +*note: it is highly recommended that the file you use for a preview track is clipped to the region you want to preview, because the file can be accessed by users. Scritch will support the file in either case, however.* + +> e.g. `{"file": "track1.mp3", "title": "Track 1", previewStart: 12, previewEnd: 42, originalDuration: "1:20"}` + +\ +`previewStart` (optional, required for preview) +> The start time for the preview section of the full track. +> Can be a number of seconds or an H:M:S string, e.g. `125.3`, `"2:05"`, `"00:21:13.5"` are all valid. + +\ +`previewEnd` (optional, required for preview) +> The end time for the preview section of the full track. +> Can be a number of seconds or an H:M:S string, e.g. `125.3`, `"2:05"`, `"00:21:13.5"` are all valid. + +\ +`originalDuration` (optional, required for preview) +> The duration of the full (unclipped) track. +> Can be a number of seconds or an H:M:S string, e.g. `125.3`, `"2:05"`, `"00:21:13.5"` are all valid. + +\ +`previewFade` (optional) +> If `true`, the player will automatically fade the audio at the start and end of the preview section. +> Can be either `true` or `false` +> Defaults to `true` + ----- **theme options** @@ -166,6 +203,14 @@ And that's it! you can upload the files to your website, or zip them up and uplo `primaryAltTextColor` > Color of e.g. the track numbers and duration, and the track info button +\ +`previewStripeColor1` +> First color of the striped bar on a preview track. + +\ +`previewStripeColor2` +> Second color of the striped bar on a preview track. + \ `linkColor` > Color of links in text diff --git a/index.html b/index.html index 64a3255..20fb3f3 100644 --- a/index.html +++ b/index.html @@ -17,7 +17,9 @@ --secondaryColor: #000000; --highlightColor: rgba(131, 131, 131, 0.3); --primaryTextColor: #ffffff; - --primaryAltTextColor: #838383; + --primaryAltTextColor: hsl(0, 0%, 51%); + --previewStripeColor1: #3f3f3f; + --previewStripeColor2: #2c2c2c; --linkColor: #838383; --font: sans-serif; } @@ -54,7 +56,7 @@ #mainContainer { position: relative; display: grid; - justify-content: space-around; + justify-content: space-evenly; width: 100%; max-width: 960px; left: 0; @@ -112,6 +114,8 @@ #mainContainer #mediaColumn { grid-row: 2; grid-column: 1; + position: relative; + top: 0; } } @@ -122,11 +126,16 @@ grid-row: 1/3; grid-column: 2; flex-direction: column; - position: relative; + position: sticky; + top: 10px; + margin-left: auto; + margin-right: auto; } #mainContainer.vertical #mediaColumn { grid-row: 2; grid-column: 1; + position: relative; + top: 0; } #mainContainer.title-span #mediaColumn { grid-row: 2/3; @@ -303,6 +312,7 @@ flex-wrap: wrap; flex: 1 0; align-items: center; + position: relative; } #controls .title { @@ -319,6 +329,7 @@ display: -webkit-box; -webkit-box-orient: vertical; user-select: none; + flex-basis: 8px; } #controls .riser { @@ -330,6 +341,7 @@ align-self: end; color: var(--primaryAltTextColor); user-select: none; + text-align: right; } #controls #scrubberTrackContainer { @@ -341,6 +353,26 @@ cursor: pointer; touch-action: none; } + #controls #scrubberTrackFull { + width: 100%; + height: 100%; + position: relative; + background: repeating-linear-gradient( 135deg, var(--previewStripeColor1), var(--previewStripeColor1) 8px, var(--previewStripeColor2) 8px, var(--previewStripeColor2) 16px); + border-radius: 12px; + } + #controls #scrubberTrackContainer #scrubberTrackPreview { + width: 100%; + height: 100%; + position: relative; + } + #controls #scrubberTrackContainer.preview #scrubberTrackPreview { + border: 2px solid var(--backgroundColor); + box-sizing: content-box; + top: -2px; + left: -2px; + margin-right: -4px; + border-radius: 12px; + } #controls #scrubberTrack { background-color: var(--primaryAltColor); overflow: hidden; @@ -388,6 +420,9 @@ align-items: center; user-select: none; height: 20px; + position: absolute; + right: 0; + bottom: 0; } #controls #volumeContainer #volumeTrackContainer { @@ -458,7 +493,7 @@ border-radius: 6px; } .track.locked { - opacity: 0.35; + opacity: 0.5; } .track.active:before { content: ''; @@ -573,6 +608,7 @@ color: var(--primaryAltTextColor); font-size: 14px; flex: 0 0 30px; + white-space: nowrap; } .track .spacer { flex: 1 1; @@ -667,10 +703,14 @@
____
@@ -700,6 +740,7 @@ let bigButtonEl = document.getElementById('bigButton'); let controlsEl = document.getElementById('controls'); let scrubberTrackContainerEl = document.getElementById('scrubberTrackContainer'); + let scrubberTrackPreviewEl = document.getElementById('scrubberTrackPreview'); let scrubberTrackEl = document.getElementById('scrubberTrack'); let scrubberFillEl = document.getElementById('scrubberFill'); let scrubberEl = document.getElementById('scrubber'); @@ -751,6 +792,8 @@ if(theme.secondaryColor) rootEl.style.setProperty('--secondaryColor', theme.secondaryColor); if(theme.highlightColor) rootEl.style.setProperty('--highlightColor', theme.highlightColor); if(theme.backgroundColor) rootEl.style.setProperty('--backgroundColor', theme.backgroundColor); + if(theme.previewStripeColor1) rootEl.style.setProperty('--previewStripeColor1', theme.previewStripeColor1); + if(theme.previewStripeColor2) rootEl.style.setProperty('--previewStripeColor2', theme.previewStripeColor2); if(theme.linkColor) rootEl.style.setProperty('--linkColor', theme.linkColor); if(theme.layoutStyle){ document.getElementById('mainContainer').classList.remove('vertical', 'horizontal'); @@ -764,7 +807,7 @@ document.getElementById('mainContainer').classList.remove('title-none', 'title-span'); document.getElementById('mainContainer').classList.add('title-' + theme.titleStyle); } - if(theme.contentWidth) document.getElementById('contentContainer').style.width = theme.contentWidth + 'px'; + if(theme.contentWidth) document.getElementById('contentContainer').style.maxWidth = theme.contentWidth + 'px'; if(theme.nativePlayer){ document.getElementById('audio').classList.add('native'); mediaVideoEl.setAttribute('controls', ''); @@ -849,6 +892,8 @@ updateDescription(description); updateTheme(theme); + updateTrackPreview(); + document.title = titleEl.textContent; volume = parseFloat(localStorageGet('volume')); @@ -916,6 +961,9 @@ infoContainerEl.classList.toggle('active'); }); } + if(!entry.previewFade && entry.previewFade !== false){ + entry.previewFade = true; //default true + } if(!entry.locked){ trackEl.querySelector('button').onclick = () => { loadedFirst = true; @@ -928,11 +976,51 @@ } else { loaderEl = document.createElement('audio'); } + if(entry.previewStart || entry.previewEnd){ + entry.preview = true; + } + let needsPreviewEnd = false; + if(!entry.previewEnd){ + needsPreviewEnd = true; + } + entry.originalDuration = (entry.originalDuration && fromHMS(entry.originalDuration)) || 1; + entry.previewStart = (entry.previewStart && fromHMS(entry.previewStart)) || 0; + entry.previewEnd = (entry.previewEnd && fromHMS(entry.previewEnd)) || entry.originalDuration; + entry.previewDuration = (entry.previewEnd - entry.previewStart); loaderEl.addEventListener('loadedmetadata', e => { entry.duration = e.target.duration; - trackEl.querySelector('.duration').textContent = toHMS(entry.duration); + entry.needsDuration = false; + entry.previewStart = entry.previewStart || 0; + if(needsPreviewEnd){ + entry.previewEnd = entry.duration; + } + entry.originalDuration = Math.max(entry.originalDuration, entry.duration, entry.previewEnd); + entry.previewDuration = (entry.previewEnd - entry.previewStart); + + //make assumptions about the audio start points + if(entry.duration < entry.previewDuration){ //smaller than given preview duration + //left align to preview + entry.audioStart = entry.previewStart; + } else if(entry.duration < entry.originalDuration - entry.previewStart){ //else, smaller than region between preview start and original end + //left align to preview + entry.audioStart = entry.previewStart; + } else if(entry.duration < entry.originalDuration){ //else, smaller than original duration + //right align to original + entry.audioStart = entry.originalDuration - entry.duration; + } else { //larger than original duration + //left align to original + entry.audioStart = 0; + } + entry.audioEnd = entry.audioStart + entry.duration; + + let durationText = toHMS(entry.duration); + if(entry.preview){ + durationText += ' (preview)'; + } + trackEl.querySelector('.duration').textContent = durationText; if(entry === currentEntry){ - updateScrubPosition(0); + updateTrackPreview(); + updateScrubPosition(entry.previewStart / entry.originalDuration); } }); loaderEl.addEventListener('error', e => { @@ -947,15 +1035,14 @@ }); loaderEl.volume = 0; loaderEl.src = entry.file; + entry.started = false; entry.loading = true; } }); - loadEntry(media[featureIndex]); + loadEntry(media[mod(featureIndex, media.length)]); } function loadCover(cover){ - // let el = document.createElement('img'); - // mediaImageEl.appendChild(el); let src = cover; if(!/:\/\//.test(src)){ //if not a url src = mediaDir + src; @@ -966,7 +1053,15 @@ playerEl.addEventListener('canplay', e => { removeLoading(currentEntry); - if(autoPlay){ + if(currentEntry.needsDuration){ + currentEntry.duration = playerEl.duration; + currentEntry.needsDuration = false; + } + if(autoPlay && !currentEntry.started){ + currentEntry.started = true; + if(!currentEntry.needsDuration){ + playerEl.currentTime = currentEntry.previewStart - currentEntry.audioStart; + } playerEl.play(); } if(!loadedFirst){ @@ -977,7 +1072,11 @@ setLoading(currentEntry, 0); }); playerEl.addEventListener('play', e => { + if(currentEntry.preview && playerEl.currentTime < currentEntry.previewStart - currentEntry.audioStart){ + playerEl.currentTime = currentEntry.previewStart - currentEntry.audioStart; + } bigButtonEl.classList.add('pause'); + currentEntry.started = true; currentEntry.trackEl.querySelector('button').classList.add('pause'); if(currentEntry.type === 'video'){ mediaVideoEl.play(); @@ -1011,8 +1110,20 @@ }); playerEl.addEventListener('timeupdate', e => { if(!scrubberDragged){ - let t = e.target.currentTime / currentEntry.duration; - updateScrubPosition(t); + if(currentEntry.duration && !currentEntry.needsDuration){ + if(currentEntry.started){ + let time = e.target.currentTime; + if(time > currentEntry.previewEnd - currentEntry.audioStart){ + nextTrack(false); + } else { + let t = time / currentEntry.duration; + updatePreviewVolume(t); + updateScrubPosition(t); + } + } else { + + } + } } }); playerEl.addEventListener('ended', e => { @@ -1108,6 +1219,7 @@ let scrubberDragged = false; scrubberTrackContainerEl.addEventListener('pointerdown', e => { scrubberDragged = true; + currentEntry.started = true; scrub(e.clientX); }); document.addEventListener('pointerup', e => { @@ -1135,15 +1247,63 @@ prevTrack(); }); + function updateTrackPreview(){ + if(currentEntry.needsDuration){ + return; + } + //reset to prevent compounding width calculation issues + scrubberTrackPreviewEl.style.removeProperty('margin-left'); + scrubberTrackPreviewEl.style.removeProperty('width'); + scrubberEl.style.removeProperty('left'); + scrubberFillEl.style.removeProperty('width'); + if(currentEntry.preview){ + let fullWidth = scrubberEl.parentElement.parentElement.clientWidth; + scrubberTrackPreviewEl.style.marginLeft = ((currentEntry.previewStart) / currentEntry.originalDuration) * 100 + '%'; + scrubberTrackPreviewEl.style.width = ((currentEntry.previewEnd - currentEntry.previewStart) / currentEntry.originalDuration) * 100 + '%'; + } + } + + function updatePreviewVolume(t){ + if(currentEntry.preview && currentEntry.previewFade && !muted){ + if(currentEntry.needsDuration){ + playEntry.volume = 0; + } else { + //rebase t to full (assumed) duration + t = currentEntry.audioStart + (t * currentEntry.duration); + //clip to preview + t = Math.max(currentEntry.previewStart, Math.min(currentEntry.previewEnd, t)); + let inTime = t - currentEntry.previewStart; + let outTime = currentEntry.previewEnd - t; + playerEl.volume = volume * Math.max(0, Math.min(1, inTime/1, outTime/1)); //1 second fade in/out + } + } + } + + //t: 0-1 value in relation to the actual audio start/end function updateScrubPosition(t){ - scrubPosition = t; + //rebase t to full (assumed) duration + t = currentEntry.audioStart + (t * currentEntry.duration); + //clip to preview + t = Math.max(currentEntry.previewStart, Math.min(currentEntry.previewEnd, t)); + scrubPosition = (t - currentEntry.audioStart) / currentEntry.duration; + let time = t; + //rebase + t = t / currentEntry.originalDuration; + //relative to preview region + let scrubberT = ((t * currentEntry.originalDuration) - currentEntry.previewStart) / currentEntry.previewDuration; let scrubberWidth = scrubberEl.clientWidth; let trackWidth = scrubberEl.parentElement.clientWidth; - //scrubberEl.style.left = `calc(${(t * 100).toFixed(0)}% - 8px)`; - scrubberEl.style.left = (scrubPosition * (trackWidth - scrubberWidth)) + 'px'; - scrubberFillEl.style.width = (scrubPosition * trackWidth) + 'px'; - let time = scrubPosition * currentEntry.duration; - controlsEl.querySelector('.time').textContent = `${toHMS(time)} / ${currentEntry.duration ? toHMS(currentEntry.duration) : "0:00"}`; + + scrubberEl.style.left = ((scrubberT * (trackWidth - scrubberWidth)) / trackWidth) * 100 + '%'; + scrubberFillEl.style.width = (scrubberT) * 100 + '%'; + + //let time = timePosition * currentEntry.duration; + let timeText = ''; + if(currentEntry.preview){ + timeText += '(preview)
'; + } + timeText += `${toHMS(time)} / ${currentEntry.originalDuration ? toHMS(currentEntry.originalDuration) : "0:00"}` + controlsEl.querySelector('.time').innerHTML = timeText; } function scrub(x){ @@ -1153,6 +1313,10 @@ let scrubberWidth = scrubberEl.clientWidth; let trackWidth = scrubberEl.parentElement.clientWidth; let t = Math.max(0, Math.min(1, (x - scrubberWidth/2) / (trackWidth - scrubberWidth))); + if(currentEntry.preview){ + //rebase t from preview to audio start/end + t = (t * currentEntry.previewDuration + (currentEntry.previewStart - currentEntry.audioStart)) / currentEntry.duration; + } updateScrubPosition(t); } } @@ -1189,8 +1353,8 @@ loadEntry(entry); } function prevTrack(){ - if(playerEl.currentTime > 5){ - playerEl.currentTime = 0; + if(playerEl.currentTime > (currentEntry.previewStart - currentEntry.audioStart) + 5){ + playerEl.currentTime = (currentEntry.previewStart - currentEntry.audioStart); } else { let idx = media.indexOf(currentEntry)-1; let entry = media[mod(idx, media.length)]; @@ -1244,7 +1408,15 @@ }); currentEntry = entry; if(!currentEntry.duration){ - currentEntry.duration = 0.01; + currentEntry.duration = 999; + currentEntry.needsDuration = true; + } + updateTrackPreview(); + if(entry.preview){ + scrubberTrackContainerEl.classList.add('preview'); + updatePreviewVolume(0); + } else { + scrubberTrackContainerEl.classList.remove('preview'); } Array.from(mediaContainerEl.children).forEach(el => { el.classList.remove('active'); @@ -1271,6 +1443,7 @@ mediaVideoEl.load(); } playerEl.src = entry.file; + entry.started = false; setLoading(currentEntry); entry.trackEl.classList.add('active'); controlsEl.querySelector('.title').innerHTML = entry.title; @@ -1301,7 +1474,7 @@ function toHMS(secs){ - let sec_num = parseInt(secs, 10); + let sec_num = Math.max(0, Math.round(secs)); //flooring is more correct, but rounding gives nicer results due to playback event buffering let hours = Math.floor(sec_num / 3600); let minutes = Math.floor(sec_num / 60) % 60; let seconds = sec_num % 60; @@ -1312,6 +1485,37 @@ .join(":") } + function fromHMS(hms){ + if(!hms){ + return 0; + } + let groups = (''+hms).split(':'); + if(groups.length < 1){ + return 0; + } + for(let i = 0; i < groups.length; i++){ + if(groups[i] === ''){ + groups[i] = '0'; + continue; + } + if(isNaN(groups[i])){ + return 0; + } + } + let seconds = 0; + let minutes = 0; + let hours = 0; + seconds = parseFloat(groups[groups.length-1]); + if(groups.length >= 2){ + minutes = parseFloat(groups[groups.length-2]); + } + if(groups.length >= 3){ + hours = parseFloat(groups[groups.length-3]); + } + + return (((hours * 60) + minutes) * 60) + seconds; + } + function mod(x, n){ return (((x % n) + n) % n); } diff --git a/src/fonts.css b/src/fonts.css index da4706a..9c0c425 100644 --- a/src/fonts.css +++ b/src/fonts.css @@ -1,10 +1,10 @@ @font-face { font-family: 'icons'; - src: url('fonts/icons.eot?qqpgvs'); - src: url('fonts/icons.eot?qqpgvs#iefix') format('embedded-opentype'), - url('fonts/icons.ttf?qqpgvs') format('truetype'), - url('fonts/icons.woff?qqpgvs') format('woff'), - url('fonts/icons.svg?qqpgvs#icons') format('svg'); + src: url('fonts/icons.eot?llsbwi'); + src: url('fonts/icons.eot?llsbwi#iefix') format('embedded-opentype'), + url('fonts/icons.ttf?llsbwi') format('truetype'), + url('fonts/icons.woff?llsbwi') format('woff'), + url('fonts/icons.svg?llsbwi#icons') format('svg'); font-weight: normal; font-style: normal; font-display: block; @@ -26,6 +26,39 @@ -moz-osx-font-smoothing: grayscale; } +.icon-duration-preview:before { + content: "\1f55b"; +} +.icon-duration:before { + content: "\1f553"; +} +.icon-duration-dot:before { + content: "\23f2"; +} +.icon-end:before { + content: "\e900"; +} +.icon-start:before { + content: "\23f4"; +} +.icon-cut:before { + content: "\2700"; +} +.icon-marker-help:before { + content: "\2753"; +} +.icon-marker-info:before { + content: "\1f6c8"; +} +.icon-marker-warning:before { + content: "\2757"; +} +.icon-lock-file:before { + content: "\1f513"; +} +.icon-lock:before { + content: "\1f512"; +} .icon-config:before { content: "\1f4c3"; } diff --git a/src/fonts/icons.eot b/src/fonts/icons.eot index 21c27f11c78a5b0902f02d4ea17b7c04be48564d..24d3e3b49382bbf09dce876a122b08d7deb33bf4 100644 GIT binary patch delta 3938 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 delta 877 zcmYk4ZAep59LAq>@7cYZZ;QEnOVf6@`Ld8%!#*VB3q+)ppCl--KDTP{D6u1Aq7pe!#Mzp#HGGSFe=q9&jPFx5V{A0V^ON_1H@rc>)`P9 z(1TE3gnSHeB*O#2{_1Q&Cs5*~@#ZiU46lI1(Gbm3HKj*VmaE9_n8%3f)ww83{%Pe%@&-B2N?8mFVbL3?hx7EUS-06nE5%tbt1X>TT8* zM;6Rz2$1S)X2~(q04eQDo@0)*jwW+hY3vYuIDxf;Bd#6gv$E&j_RM>hy^`1O9r7mY zi+!^8ThnL;XvV+JPk%ct|8@HGR;GQ>GTIBx|89D-Vxwxq`PQ2`m+8plufJb^xE@;* zR_*#vsrQ9?^_3vm^eFYOO5GCr;(LHXpn&q)HbEianKuDt;aN6;9q?KRiTWu-uipd? z(jgNRlO|0-mr_q&2m1k^Y=RQ7FN+3pI3f>Wb@#YE|5l@ft#EyV6jE zGdY?KBX{pi5}u2SVPMlnK + + @@ -24,8 +26,12 @@ + + + + @@ -36,8 +42,13 @@ + + + + + \ No newline at end of file diff --git a/src/fonts/icons.ttf b/src/fonts/icons.ttf index d5290e92c81188acd580bbafb038cf5472c3dd7c..9ecf9b6dc3fc37ec21e13dd19547109dc8f4eec7 100644 GIT binary patch delta 3923 zcmb_fYit}>6+U-ncE{e$?CfTDX8ef9`>@AkH|sdNj&}=-?MM_7J5ixFMT3 z&BG;83F608s33k-s>GD$_Z*o}-r{UARXcZ8|fbsM7q74XK zq4xzI$2?!1pbQPryY}JaKT=my*HSl9w>qwLp6i@;{7#Sah;yoox<@xl40z&GmdR`CiLx+ic5h^Q`%&FJ0Ps>E27)%;lLg zGmp(!7vH@2+Pmr>V%8cic;y2YuYAHH07d5tBzBp9@pM(f(jNC3^a3EvrEYjY8roYP z5cJk@#RJlbI?s8)4cIyD0Y!)$AHWYjtI@(R9H1O>9`S$-w5L2E6Psh2gG&}VBOXu_ z+8rKHGtt_p2h>88Q9YnmqK`nxfz}YaeqI1vhuDo@2_Ul3zAk{M9qoGpxX#&)PX)le zh}~4kV-XSRVXruviXSvUJC16!6a zr8A{cCS7_+_PJ7XHs)6o@gu`S+qVx5ABiWte)j`i5kqO)lo~7+N1E`A6pMpi-{)g% z4f{b|*E4!qZLke#(&JZ3Pcdy(bFO9k@X$mY&&1I1_R(Q5jYn5Uds;(!Q`2a%I3UUh(-RJDzaVT;cnZDTv{;xk9Kkv47HOdB?P z8Yg}Jw2*KbT~JP~Q-7zp!gf1gy9PLXKMg@j%ghz}de<|S^cykQ3$}7p&zR7&#YKTR zm<@|A*x4$;#N6u9MO0GXjM(vF?WgfAQ&eMMBHTdxg8L=$fooIO@ud*~w2?KYgXFw7XW3Z74D+ZyQ|yhMiyT zcESZ~&T?1v+?GvpO4HssIJh&LQUfxRIHPbd*s^L>$Tsp-kF(AGzRU{;-GO3pw5h39 z^}IEXa2Ot4?8PTN!3KBk99;K}nB1T+iG{g)gsqgOMg3}1$grzO11Jph0qyDS{-q%; zCs$IM>jm`PwYJ2^|94eE9gJ@4WPrW0Tq&QYlWE&HW45)KXAFDlR&bn+Dom%cOiQ9R z=||M^I6%}oNN0<%RAdRt{IPaquNWc-5sMIVlB!e{QCrLYpQ3iR_Ke?^d2*3?rEf=O zFTg%aoV5*^#)!Z_69~u%LU&_<%b7LAk94>Y`>9o^X42QWYYi0herc2YM zblsPwe5R8x;EOvmc^Av7+;vzUe!j1i$>%er+7u_u^I?<&f+xv&#O0FoG%j-^$qz@LNK|#FK!JW_W^7dqV*N9UTn*+7m&eACz2%j zlx?E8^zRCqLA*wbBqfZ7NCRFYAxSO9U$sm)(jL;3NH`q|SXxBUYEJ^wtUxFo1{P|M zgfl0NR*B(}S`Cmf32(xXUPa%%jVvtq>_DAUowsy&o|9JRU7NT=lm!tHO15#*-{|u- zC|gB!>t{a@`zH*u#gnAP#O}Mpu8&I?NoyfrShIp!n)id27WRdBA1Cu&#XgPU2E0Z> z#tFne0SJtKz*9@BBttFXM!eu!yQUO@#JbUdqD3dWAN@;;oWG)_< zOkIyAC3mH=F@J4E<0RP%L=SpzvO*hg$21Jw2jUSD@WMf z0t^D2;aLtv1Hpqlg1h$fZF9}EOE2A|Ait3l{rRyowfVMEGz+Y!GGXTYR8~MzIb}K~ z@6LnxS>scBW!g-;0YszbY9 zdrtdp$V!JE4$Wc`m7~_{);--l^Z@Op@6b^irz3P2S$+fEQ`y$^ G^1lIfU&hV= delta 847 zcmYL{T}V@57{~wTW9Mvst<;>En(nKnVM|8c2x<|8Ng9O&MK#p9(&@?tc4Ze`R2PyR zgAjsU^mQpL%)r3tA_^B2h`2PU26j^lwXh&g|8ow}3qSt+pXdF0p7*`hcC~R70)Tv6 zfrMbkvE7Xcc~_2+S#q{J7LF@B<{tn;B_MY8g@@yeZvvzNs=aUEa^ztoFGimLij&da zaL@K^!C|1Rl=;0;2Gl8OfxeyI8;vDKuBVIh=#70Q28Oy@!)mCpgk5IiK79R z=sW3a2E(!5-?v+f=(#|+5FZ*&@S+^$StAcgS{t9MW29&NYwK}jS&fknQH>3=bVhw4 zDo>@CF-6_Ur84Zy)uJBFSgam&uX(9O=q7UlhNQvPX++TJK#%AE2=9aCr2*Xp{x#Xe!5ag;kc&Hv-WFzniC&$27j5JVSx UagmX87{VZb(jJ7gquwWf0eyhT+W-In diff --git a/src/fonts/icons.woff b/src/fonts/icons.woff index aa07908374324b71857214ff1615ea1b067c2426..72b6a5f12bf899182937e8fa2d4fc9475ffb2e73 100644 GIT binary patch delta 3940 zcmb_feQXw}y2?>UY+&u-BB=Mdh1=qnY>fv)pKjvwZ}dg;KvJzpY9B|uZ)NLZ7( z1DM3z4vbfEDCgw24;+5<1m?bm@db|OYi;Ka9o_qR5T?tRc!=YfC%!juc+UwOoIZ_l zLg15h>ByeL`!II_LO#MV`}F3}v7^Ty#gzt{bTWFf{C>5SW#lkjt8QSMcC#36&KCyI zdayNy5!b~4+JG2)=xs4hV4YJZDN7saP3uVNpXnRv_tH1hAFR6GeZG6z_St>*!}jSO z>KX0XzB-y!-lghm%p6@v<6TNK`_b$yvDv@QUYWf#+xO?mx8hejug0&oUp4;ti7Vq* z?!BVUT$?#N^Vp1e`PIw6d{g;-%v@PbNV)2e)T*^YHe%QK8^50FPHC_69#jDk*3vg! zkct+zCr&;Z#FyC5CyX%}SRaFB91WTG?b zg4)n-cR}q$tD`O`PLx$#PzTYw*vLUEiM{`l06d7;&7TVpS!iDoAnHQGoILvG*e zWvg{-ho)&+t)loXU7B?Hl`~UJZEf$5ZyOmtmcZ}W@W{5Y5im_eS48_dLRwqfSgEv8 zuxW4SEDsLF?C6wbys$&dfMG?62B!^+JkU=?`j=FLL#~9%2cZ{!6`>n`3vULT1KmBcruz)v-GTPiPs-2eH-uMHA|MB zF)Yi-=oWhlw|(x6ka8Mb?3_BGyykI){kFq?emMOg4MS4X=r0xq)-smz=`q+0_Ocb% zpwP9+#eqqf1*^{6+APA%-15-{yQIR5IPr4*`-xmG(Ur@+J~1*p5e#l&TzpNdJG+Cy ziQ$oni4ij!-LfT`HAg0XI!DVoW`vP0vxB(=pG)NKAD);P=9JjaIn;My2cHAez?S<0 z;8p*6gP6Qc=L{kq?x9EMam4)sq2-h#Ty9zSWCOxkmv_!?e#`mWqt6+I*a92#-GdCe zOPeLOuEeCgrE|;ZR({?oorYHm53SovJ=yhYHp#ad*7(rScrL93WF~P=;b1V{+8VO- ze9dKTvA-{I(xHu|(pXztz2^FI0>LseHs|AxaPm{GWJBZQLu)=0ll>khu`u_Xu$flX zs84AN=~fL{0>z;b@2*N8SRCMr%^G@@OEdNV$j)zw|83hH3g(8-G*tCLmBDMjdH z+S1P&ot?&6-I}_mb%Kp~m_`+u76o<62ZQqvK~USsb_+)7$f6AWUnuBS@Yaiqd-&e z^(-F_m%8mt#_leK!(5&93V+u!<>_*!>CjR>+sz3$ad$TFfUL&dhvnhy`^(vUK3lF& z@r`*tjfz3=COMC^lR$bbEftVgj|CEbirtxn?L1A?odMpo-!5cjp^z2OO>#0;N#ttf zY^4Gb#ofe<`}%xs>wt(`A>3Ej_^}b?#l#Xe_99Uhuw*6+oT-~nU21qG=gY)G^FTh86invM z71`GGf`#Qkv;q#{yhTysjNFtWNs`xdzoBu1(k2xY<;g}Nf)>?yrNV)#iNI<_$hPg0j&M&Fqm%oBWcu*s>( zTRPIle!UiC5an2eRtULatR|DHRKJe zW>C%We$Z6I-VpEOX5Je(rzPyiqbFtDK%5hRz~~2DwRA`_)DmvN1FrQOYJ(YP)3pK@ zf-+Wg@k1Ky)n}nh#xy7;4yDLkJTRHs7)wdca;2m3Re=wO;vELplGyU5Z1G{}enT;Q z3vo>?s-g`?8`NpdD&pTe*`;@QjJFmo)gUUsJ!T*DnRV`1poDb{_d^>P5 z@NzH|{9^E2>nimD^;z{dp-kwZ&{c@VrS79wkr((+!k^@5^%v$V=7!!rdXV*PSm~+?TsL zIv|11_HGoLs={vc$nG1B&SXMu%=N{?qkt3xT<@zrE!Z3! z45sQ_4SN1}KF<~4ihq%xw<0b7MH*#tg{R_eaP`#ck=5dr4=WE=63gD!zMlJG$K{=@{Rq%N4S*3@?6rp>Z@v(Hsq7?8AVo( zDG!xbs;suFDfNdXW_hmlYV+DheL|nrpXnRc!`4abtgX`4Z62Kf#>^|ho5CKtj0N9< aC)Cu8UPN$_bLTOFVIGcs2pM<%kNyCQq{r|8