wojack revisó este gist 22 hours ago. Ir a la revisión
Sin cambios
wojack revisó este gist 22 hours ago. Ir a la revisión
2 files changed, 6815 insertions
gr-details.js(archivo creado)
| @@ -0,0 +1,2356 @@ | |||
| 1 | + | ///////////////////////////////////////////////////////////////////////////////// | |
| 2 | + | // * Georgia-ReBORN: A Clean - Full Dynamic Color Reborn - Foobar2000 Player * // | |
| 3 | + | // * Description: Georgia-ReBORN Details * // | |
| 4 | + | // * Author: TT * // | |
| 5 | + | // * Website: https://github.com/TT-ReBORN/Georgia-ReBORN * // | |
| 6 | + | // * Version: 3.0-x64-DEV * // | |
| 7 | + | // * Dev. started: 22-12-2017 * // | |
| 8 | + | // * Last change: 02-05-2026 * // | |
| 9 | + | ///////////////////////////////////////////////////////////////////////////////// | |
| 10 | + | ||
| 11 | + | ||
| 12 | + | 'use strict'; | |
| 13 | + | ||
| 14 | + | ||
| 15 | + | //////////////////////////////// | |
| 16 | + | // * DETAILS USER INTERFACE * // | |
| 17 | + | //////////////////////////////// | |
| 18 | + | /** | |
| 19 | + | * A class that is responsible for the Details panel. | |
| 20 | + | */ | |
| 21 | + | class Details { | |
| 22 | + | /** | |
| 23 | + | * Creates the `Details` instance. | |
| 24 | + | */ | |
| 25 | + | constructor() { | |
| 26 | + | // * GEOMETRY * // | |
| 27 | + | // #region GEOMETRY | |
| 28 | + | /** @public @type {number} The size of the disc art shadow. */ | |
| 29 | + | this.discArtShadow = SCALE(6); | |
| 30 | + | /** @public @type {number} The margin width from the edge of the player to start of the metadata grid strings. */ | |
| 31 | + | this.gridMarginLeft = grm.ui.edgeMargin; | |
| 32 | + | /** @public @type {number} The margin width from the edge of the metadata grid to the end of the metadata grid strings. */ | |
| 33 | + | this.gridMarginRight = SCALE(20); | |
| 34 | + | /** @public @type {number} The spacing between grid lines in the metadata grid. */ | |
| 35 | + | this.gridLineSpacing = SCALE(30); | |
| 36 | + | /** @public @type {number} The horizontal spacing between the track number and the artist in the metadata grid. */ | |
| 37 | + | this.gridTrackNumSpacing = SCALE(8); | |
| 38 | + | /** @public @type {number} The height of the metadata grid tooltip area. */ | |
| 39 | + | this.gridTooltipHeight = SCALE(100); | |
| 40 | + | /** @public @type {number} The top starting fixed position of the metadata grid. */ | |
| 41 | + | this.gridTopStart = 0; | |
| 42 | + | /** @public @type {number} The top dynamic position of the metadata grid. */ | |
| 43 | + | this.gridTop = 0; | |
| 44 | + | /** @public @type {number} The width of the metadata grid content. */ | |
| 45 | + | this.gridContentWidth = 0; | |
| 46 | + | /** @public @type {number} The width of the country flag size in the metadata grid. */ | |
| 47 | + | this.gridFlagSizeW = 0; | |
| 48 | + | /** @public @type {number} The white space size for the country flag in the metadata grid. */ | |
| 49 | + | this.gridFlagSizeWhiteSpace = 0; | |
| 50 | + | /** @public @type {number} The text rectangle for string calculation in the metadata grid. */ | |
| 51 | + | this.gridTxtRec = 0; | |
| 52 | + | /** @public @type {number} The top position of the artist in the metadata grid. */ | |
| 53 | + | this.gridArtistTop = 0; | |
| 54 | + | /** @public @type {number} The bottom position of the artist in the metadata grid. */ | |
| 55 | + | this.gridArtistBottom = 0; | |
| 56 | + | /** @public @type {object} The calculated artist wrap info for the metadata grid. */ | |
| 57 | + | this.gridArtistWrapInfo = {}; | |
| 58 | + | /** @public @type {boolean} The state when the artist string exceeds the available lines in the metadata grid. */ | |
| 59 | + | this.gridArtistWrapLinesExceed = false; | |
| 60 | + | /** @public @type {number} The width of the wrap space within the artist string in the metadata grid. */ | |
| 61 | + | this.gridArtistWrapWidth = 0; | |
| 62 | + | /** @public @type {number} The width of the artist in the metadata grid. */ | |
| 63 | + | this.gridArtistWidth = 0; | |
| 64 | + | /** @public @type {number} The height of the artist in the metadata grid. */ | |
| 65 | + | this.gridArtistHeight = 0; | |
| 66 | + | /** @public @type {number} The text rectangle for artist string calculation in the metadata grid. */ | |
| 67 | + | this.gridArtistTxtRec = 0; | |
| 68 | + | /** @public @type {number} The number of lines for the artist text in the metadata grid. */ | |
| 69 | + | this.gridArtistNumLines = 0; | |
| 70 | + | /** @public @type {number} The height of the artist number of lines in the metadata grid. */ | |
| 71 | + | this.gridArtistNumLinesHeight = 0; | |
| 72 | + | /** @public @type {number} The top position of the track title in the metadata grid. */ | |
| 73 | + | this.gridTitleTop = 0; | |
| 74 | + | /** @public @type {number} The bottom position of the track title in the metadata grid. */ | |
| 75 | + | this.gridTitleBottom = 0; | |
| 76 | + | /** @public @type {number} The width of the track number in the metadata grid. */ | |
| 77 | + | this.gridTrackNumWidth = 0; | |
| 78 | + | /** @public @type {object} The calculated track title wrap info for the metadata grid. */ | |
| 79 | + | this.gridTitleWrapInfo = {}; | |
| 80 | + | /** @public @type {boolean} The state when the track title string exceeds the available lines in the metadata grid. */ | |
| 81 | + | this.gridTitleWrapLinesExceed = false; | |
| 82 | + | /** @public @type {number} The width of the wrap space within the track title string in the metadata grid. */ | |
| 83 | + | this.gridTitleWrapWidth = 0; | |
| 84 | + | /** @public @type {number} The width of the track title in the metadata grid. */ | |
| 85 | + | this.gridTitleWidth = 0; | |
| 86 | + | /** @public @type {number} The height of the track title in the metadata grid. */ | |
| 87 | + | this.gridTitleHeight = 0; | |
| 88 | + | /** @public @type {number} The text rectangle for track title string calculation in the metadata grid. */ | |
| 89 | + | this.gridTitleTxtRec = 0; | |
| 90 | + | /** @public @type {number} The number of lines for the track title text in the metadata grid. */ | |
| 91 | + | this.gridTitleNumLines = 0; | |
| 92 | + | /** @public @type {number} The height of the track title number of lines in the metadata grid. */ | |
| 93 | + | this.gridTitleNumLinesHeight = 0; | |
| 94 | + | /** @public @type {number} The top position of the album in the metadata grid. */ | |
| 95 | + | this.gridAlbumTop = 0; | |
| 96 | + | /** @public @type {number} The bottom position of the album in the metadata grid. */ | |
| 97 | + | this.gridAlbumBottom = 0; | |
| 98 | + | /** @public @type {object} The calculated album wrap info for the metadata grid. */ | |
| 99 | + | this.gridAlbumWrapInfo = {}; | |
| 100 | + | /** @public @type {boolean} The state when the album string exceeds the available lines in the metadata grid. */ | |
| 101 | + | this.gridAlbumWrapLinesExceed = false; | |
| 102 | + | /** @public @type {number} The width of the wrap space within the album string in the metadata grid. */ | |
| 103 | + | this.gridAlbumWrapWidth = 0; | |
| 104 | + | /** @public @type {number} The width of the album in the metadata grid. */ | |
| 105 | + | this.gridAlbumWidth = 0; | |
| 106 | + | /** @public @type {number} The height of the album in the metadata grid. */ | |
| 107 | + | this.gridAlbumHeight = 0; | |
| 108 | + | /** @public @type {number} The text rectangle for album string calculation in the metadata grid. */ | |
| 109 | + | this.gridAlbumTxtRec = 0; | |
| 110 | + | /** @public @type {number} The number of lines for the album text in the metadata grid. */ | |
| 111 | + | this.gridAlbumNumLines = 0; | |
| 112 | + | /** @public @type {number} The height of the album number of lines in the metadata grid. */ | |
| 113 | + | this.gridAlbumNumLinesHeight = 0; | |
| 114 | + | /** @public @type {number} The margin between grid columns in the metadata grid. */ | |
| 115 | + | this.gridColumnMargin = SCALE(10); | |
| 116 | + | /** @public @type {number} The top position of the grid columns in the metadata grid. */ | |
| 117 | + | this.gridColumnTop = 0; | |
| 118 | + | /** @public @type {number} The height of the grid column cell in the metadata grid. */ | |
| 119 | + | this.gridColumnCellHeight = 0; | |
| 120 | + | /** @public @type {number} The width of the key strings column in the metadata grid. */ | |
| 121 | + | this.gridColumnKeyWidth = 0; | |
| 122 | + | /** @public @type {number} The height of the key strings in the metadata grid. */ | |
| 123 | + | this.gridColumnKeyHeight = 0; | |
| 124 | + | /** @public @type {number} The bottom position of the key strings in the metadata grid. */ | |
| 125 | + | this.gridColumnKeyBottom = 0; | |
| 126 | + | /** @public @type {number} The width of the value strings column in the metadata grid. */ | |
| 127 | + | this.gridColumnValueWidth = 0; | |
| 128 | + | /** @public @type {number} The height of the value strings in the metadata grid. */ | |
| 129 | + | this.gridColumnValueHeight = 0; | |
| 130 | + | /** @public @type {number} The left position of the value strings column in the metadata grid. */ | |
| 131 | + | this.gridColumnValueLeft = 0; | |
| 132 | + | /** @public @type {number} The bottom position of the value strings in the metadata grid. */ | |
| 133 | + | this.gridColumnValueBottom = 0; | |
| 134 | + | ||
| 135 | + | // * TIMELINE * // | |
| 136 | + | // #region TIMELINE | |
| 137 | + | /** @public @type {number} The x-coordinate of the timeline. */ | |
| 138 | + | this.timelineX = 0; | |
| 139 | + | /** @public @type {number} The y-coordinate of the timeline. */ | |
| 140 | + | this.timelineY = 0; | |
| 141 | + | /** @public @type {number} The width of the timeline. */ | |
| 142 | + | this.timelineW = 0; | |
| 143 | + | /** @public @type {number} The height of the timeline. */ | |
| 144 | + | this.timelineH = SCALE(8); | |
| 145 | + | /** @public @type {number} The color of the played portion of the timeline. */ | |
| 146 | + | this.timelinePlayCol = RGBA(255, 255, 255, 150); | |
| 147 | + | /** @public @type {number} The ratio of the first played segment in the timeline. */ | |
| 148 | + | this.timelineFirstPlayedRatio = 0; | |
| 149 | + | /** @public @type {number} The ratio of the last played segment in the timeline. */ | |
| 150 | + | this.timelineLastPlayedRatio = 0; | |
| 151 | + | /** @public @type {number} The percentage of the first played segment in the timeline. */ | |
| 152 | + | this.timelineFirstPlayedPercent = 0.33; | |
| 153 | + | /** @public @type {number} The percentage of the last played segment in the timeline. */ | |
| 154 | + | this.timelineLastPlayedPercent = 0.66; | |
| 155 | + | /** @public @type {number[]} The percentages of the played times on the timeline. */ | |
| 156 | + | this.timelinePlayedTimesPercents = []; | |
| 157 | + | /** @public @type {number[]} The actual played times on the timeline. */ | |
| 158 | + | this.timelinePlayedTimes = []; | |
| 159 | + | /** @public @type {number} The width of the timeline line. */ | |
| 160 | + | this.timelineLineWidth = HD_4K(2, 3); | |
| 161 | + | /** @public @type {number} The extra left space on the timeline. */ | |
| 162 | + | this.timelineExtraLeftSpace = SCALE(3); | |
| 163 | + | /** @public @type {number} The draw width of the timeline. */ | |
| 164 | + | this.timelineDrawWidth = 0; | |
| 165 | + | /** @public @type {number} The leeway of the timeline. */ | |
| 166 | + | this.timelineLeeway = 0; | |
| 167 | + | // #endregion | |
| 168 | + | ||
| 169 | + | // * CACHE * // | |
| 170 | + | // #region CACHE | |
| 171 | + | /** @public @type {object} The caching object of the calculated text wrap space for the metadata grid. */ | |
| 172 | + | this.cachedGridWrapSpace = {}; | |
| 173 | + | /** @public @type {boolean} The calculated metadata grid metrics saved so we don't have to recalculate every on every on_paint unless size or metadata changed. */ | |
| 174 | + | this.cachedGridMetrics = false; | |
| 175 | + | /** @public @type {number} The left edge of the record labels in Details. Saved so we don't have to recalculate every on every on_paint unless size has changed. */ | |
| 176 | + | this.cachedLabelLastLeftEdge = 0; | |
| 177 | + | /** @public @type {number} The last label height of the record labels in Details. Saved so we don't have to recalculate every on every on_paint unless size has changed. */ | |
| 178 | + | this.cachedLabelLastHeight = 0; | |
| 179 | + | // #endregion | |
| 180 | + | ||
| 181 | + | // * IMAGES * // | |
| 182 | + | // #region IMAGES | |
| 183 | + | /** @public @type {GdiBitmap} The disc art image used in Details. */ | |
| 184 | + | this.discArt = null; | |
| 185 | + | /** @public @type {GdiBitmap} The disc art album cover image used in Details. */ | |
| 186 | + | this.discArtCover = null; | |
| 187 | + | /** @public @type {GdiBitmap[]} The array of disc art images used in Details. */ | |
| 188 | + | this.discArtArray = []; | |
| 189 | + | /** @public @type {number} The scale factor of the disc art used in Details. */ | |
| 190 | + | this.discArtScaleFactor = 0; | |
| 191 | + | /** @private @type {{image: GdiBitmap|null, size: number}} The shadow behind the disc art used in Details. */ | |
| 192 | + | this.discArtShadowImg = { image: null, size: 0 } | |
| 193 | + | /** @public @type {object} The disc art position used in Details (offset from albumArtSize). */ | |
| 194 | + | this.discArtSize = new ImageSize(0, 0, 0, 0); | |
| 195 | + | /** @public @type {GdiBitmap} The rotated disc art from the RotateImg helper used in Details. */ | |
| 196 | + | this.discArtRotation = null; | |
| 197 | + | /** @public @type {number} The global index of current discArtArray img to draw used in Details. */ | |
| 198 | + | this.discArtRotationIndex = 0; | |
| 199 | + | /** @private @type {GdiBitmap} The release country flag image shown in the metadata grid in Details. */ | |
| 200 | + | this.gridReleaseFlagImg = null; | |
| 201 | + | /** @private @type {GdiBitmap} The codec logo image shown in the metadata grid in Details. */ | |
| 202 | + | this.gridCodecLogo = null; | |
| 203 | + | /** @private @type {GdiBitmap} The channel logo image shown in the metadata grid in Details. */ | |
| 204 | + | this.gridChannelLogo = null; | |
| 205 | + | /** @public @type {GdiBitmap} The band logo image used in Details. */ | |
| 206 | + | this.bandLogo = null; | |
| 207 | + | /** @public @type {GdiBitmap} The inverted band logo image shown in Details. */ | |
| 208 | + | this.bandLogoInverted = null; | |
| 209 | + | /** @public @type {GdiBitmap[]} The array of record label images used in Details. */ | |
| 210 | + | this.labelLogo = []; | |
| 211 | + | /** @public @type {GdiBitmap[]} The array of inverted record label images used in Details. */ | |
| 212 | + | this.labelLogoInverted = []; | |
| 213 | + | /** @private @type {GdiBitmap} The shadow behind labels used in Details. */ | |
| 214 | + | this.labelShadowImg = null; | |
| 215 | + | // #endregion | |
| 216 | + | ||
| 217 | + | // * STATE * // | |
| 218 | + | // #region STATE | |
| 219 | + | /** @private @type {boolean} The state when disc art was found on hard drive used in Details. */ | |
| 220 | + | this.discArtFound = false; | |
| 221 | + | /** @public @type {boolean} The last.fm logo image displayed when we %lastfm_play_count% > 0, shown in the metadata grid in Details. */ | |
| 222 | + | this.playCountVerifiedByLastFm = false; | |
| 223 | + | /** @public @type {object} The boundary section object contains check functions for different sections of the metadata grid. */ | |
| 224 | + | this.gridSectionBounds = { | |
| 225 | + | artist: (x, y) => x >= this.gridMarginLeft && x <= (this.gridMarginLeft + this.gridContentWidth) && y >= this.gridArtistTop && y <= this.gridArtistBottom, | |
| 226 | + | title: (x, y) => x >= this.gridMarginLeft && x <= (this.gridMarginLeft + this.gridContentWidth) && y >= this.gridTitleTop && y <= this.gridTitleBottom, | |
| 227 | + | album: (x, y) => x >= this.gridMarginLeft && x <= (this.gridMarginLeft + this.gridContentWidth) && y >= this.gridAlbumTop && y <= this.gridAlbumBottom, | |
| 228 | + | tagKey: (x, y) => x >= this.gridMarginLeft && x <= (this.gridMarginLeft + this.gridColumnKeyWidth) && y >= this.gridAlbumBottom && y <= this.gridColumnKeyBottom, | |
| 229 | + | tagValue: (x, y) => x >= this.gridMarginLeft && x <= (this.gridMarginLeft + this.gridColumnKeyWidth + this.gridColumnValueWidth) && y >= this.gridAlbumBottom && y <= this.gridColumnValueBottom, | |
| 230 | + | timeline: (x, y) => x >= this.gridMarginLeft && x <= (this.gridMarginLeft + this.gridContentWidth) && y >= this.timelineY - SCALE(10) && y < this.timelineY + this.timelineH + SCALE(10), | |
| 231 | + | grid: (x, y) => x >= this.gridMarginLeft && x <= (this.gridMarginLeft + this.gridContentWidth) && y >= this.gridArtistTop && y <= this.gridColumnValueBottom | |
| 232 | + | }; | |
| 233 | + | /** @private @type {string} The text content of the grid tooltip. */ | |
| 234 | + | this.gridTooltipText = ''; | |
| 235 | + | /** @private @type {string} The text content of the grid timeline tooltip. */ | |
| 236 | + | this.gridTimelineTooltipText = ''; | |
| 237 | + | // #endregion | |
| 238 | + | ||
| 239 | + | // * TIMERS * // | |
| 240 | + | // #region TIMERS | |
| 241 | + | /** @public @type {number} The disc art rotation timer when disc art spins while song is playing. */ | |
| 242 | + | this.discArtRotationTimer = null; | |
| 243 | + | // #endregion | |
| 244 | + | } | |
| 245 | + | ||
| 246 | + | // * PLUBLIC METHODS - DRAW * // | |
| 247 | + | // #region PUBLIC METHODS - DRAW | |
| 248 | + | /** | |
| 249 | + | * Draws the Details panel. | |
| 250 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 251 | + | */ | |
| 252 | + | drawDetails(gr) { | |
| 253 | + | this.drawBackground(gr); | |
| 254 | + | this.drawDiscArt(gr); | |
| 255 | + | this.drawGrid(gr); | |
| 256 | + | this.drawBandLogo(gr); | |
| 257 | + | this.drawLabelLogo(gr); | |
| 258 | + | } | |
| 259 | + | ||
| 260 | + | /** | |
| 261 | + | * Draws the Details background. | |
| 262 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 263 | + | */ | |
| 264 | + | drawBackground(gr) { | |
| 265 | + | if (!fb.IsPlaying && !grSet.panelBrowseMode || !grm.ui.displayDetails) { | |
| 266 | + | return; | |
| 267 | + | } | |
| 268 | + | ||
| 269 | + | gr.SetTextRenderingHint(TextRenderingHint.AntiAliasGridFit); | |
| 270 | + | gr.SetSmoothingMode(SmoothingMode.None); | |
| 271 | + | ||
| 272 | + | if (grm.ui.isStreaming && grm.ui.noArtwork || !grm.ui.albumArt && grm.ui.noArtwork) { | |
| 273 | + | gr.FillSolidRect(0, grm.ui.topMenuHeight, grm.ui.ww, grm.ui.wh - grm.ui.topMenuHeight - grm.ui.lowerBarHeight, grCol.detailsBg); | |
| 274 | + | } else { | |
| 275 | + | gr.FillSolidRect(0, grm.ui.albumArtSize.y, grSet.noDiscArtBg && !this.discArt ? grm.ui.ww : grm.ui.albumArtSize.x, grm.ui.albumArtSize.h, grCol.detailsBg); | |
| 276 | + | } | |
| 277 | + | ||
| 278 | + | if (grm.ui.albumArt && grSet.styleBlend && grCol.imgBlended) { | |
| 279 | + | gr.DrawImage(grCol.imgBlended, 0, 0, grm.ui.ww, grm.ui.wh, 0, 0, grCol.imgBlended.Width, grCol.imgBlended.Height); | |
| 280 | + | } | |
| 281 | + | ||
| 282 | + | gr.SetSmoothingMode(SmoothingMode.HighQuality); | |
| 283 | + | } | |
| 284 | + | ||
| 285 | + | /** | |
| 286 | + | * Draws the disc art in Details. | |
| 287 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 288 | + | */ | |
| 289 | + | drawDiscArt(gr) { | |
| 290 | + | if (grSet.layout !== 'default' || !grSet.displayDiscArt || !grm.ui.displayDetails || grm.ui.noAlbumArtStub || | |
| 291 | + | this.discArtSize.y < grm.ui.albumArtSize.y || this.discArtSize.h > grm.ui.albumArtSize.h) { | |
| 292 | + | return; | |
| 293 | + | } | |
| 294 | + | ||
| 295 | + | grm.debug.setDebugProfile(grm.debug.showDrawExtendedTiming, 'create', '绘图 -> 碟片'); | |
| 296 | + | ||
| 297 | + | if (!this.discArtRotation) { | |
| 298 | + | this.setDiscArtRotation(); | |
| 299 | + | } | |
| 300 | + | ||
| 301 | + | const applyOpacity = !grm.ui.displayLyrics && grm.ui.albumArtSize.w < grm.ui.ww * 0.66; | |
| 302 | + | const albumArtOpacity = applyOpacity ? grSet.detailsAlbumArtOpacity : 255; | |
| 303 | + | ||
| 304 | + | if (!grSet.discArtOnTop || grm.ui.displayLyrics) { | |
| 305 | + | this.drawDiscArtImage(gr); | |
| 306 | + | if (this.discArtRotation && grSet.detailsAlbumArtDiscAreaOpacity !== 255) { | |
| 307 | + | const discArtOpacity = applyOpacity ? grSet.detailsAlbumArtDiscAreaOpacity : 255; | |
| 308 | + | this.createDiscArtAlbumArtMask(gr, grm.ui.albumArtSize.x, grm.ui.albumArtSize.y, grm.ui.albumArtSize.w, grm.ui.albumArtSize.h, 0, 0, grm.ui.albumArtScaled.Width, grm.ui.albumArtScaled.Height, 0, discArtOpacity); | |
| 309 | + | } else { | |
| 310 | + | grm.ui.drawAlbumArt(gr, albumArtOpacity); | |
| 311 | + | } | |
| 312 | + | } else { // * Draw discArt on top of front cover | |
| 313 | + | grm.ui.drawAlbumArt(gr, albumArtOpacity); | |
| 314 | + | this.drawDiscArtImage(gr); | |
| 315 | + | } | |
| 316 | + | ||
| 317 | + | grm.debug.setDebugProfile(false, 'print', '绘图 -> 碟片'); | |
| 318 | + | } | |
| 319 | + | ||
| 320 | + | /** | |
| 321 | + | * Draws the disc art image and its shadow (if applicable). | |
| 322 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 323 | + | */ | |
| 324 | + | drawDiscArtImage(gr) { | |
| 325 | + | const discArtImg = this.discArtArray[this.discArtRotationIndex] || this.discArtRotation; | |
| 326 | + | ||
| 327 | + | if (!grSet.filterAlbumArt && grm.ui.discArtImageDisplayed || !discArtImg) { | |
| 328 | + | return; | |
| 329 | + | } | |
| 330 | + | ||
| 331 | + | if (this.discArtShadowImg.image) { | |
| 332 | + | const shadowImg = this.discArtShadowImg.image; | |
| 333 | + | gr.DrawImage(shadowImg, -this.discArtShadow, grm.ui.albumArtSize.y - this.discArtShadow, shadowImg.Width, shadowImg.Height, 0, 0, shadowImg.Width, shadowImg.Height); | |
| 334 | + | } | |
| 335 | + | ||
| 336 | + | gr.DrawImage(discArtImg, this.discArtSize.x, this.discArtSize.y, this.discArtSize.w, this.discArtSize.h, 0, 0, discArtImg.Width, discArtImg.Height, 0); | |
| 337 | + | } | |
| 338 | + | ||
| 339 | + | /** | |
| 340 | + | * Draws the metadata grid on the left side in the Details panel. | |
| 341 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 342 | + | */ | |
| 343 | + | drawGrid(gr) { | |
| 344 | + | if (!fb.IsPlaying && !grSet.panelBrowseMode || !grm.ui.displayDetails) return; | |
| 345 | + | ||
| 346 | + | grm.debug.setDebugProfile(grm.debug.showDrawExtendedTiming, 'create', '绘图 -> 元数据表格'); | |
| 347 | + | ||
| 348 | + | gr.SetSmoothingMode(SmoothingMode.HighQuality); | |
| 349 | + | gr.SetInterpolationMode(InterpolationMode.HighQualityBicubic); | |
| 350 | + | ||
| 351 | + | this.setGridMetrics(gr); | |
| 352 | + | this.gridTop = this.gridTopStart; | |
| 353 | + | ||
| 354 | + | if (this.gridContentWidth > 150) { | |
| 355 | + | const spacing = Math.floor(this.gridLineSpacing * 0.33); | |
| 356 | + | const spacing2 = Math.floor(this.gridLineSpacing * 0.5); | |
| 357 | + | ||
| 358 | + | // * Artist | |
| 359 | + | if (grSet.showGridArtist_layout) { | |
| 360 | + | this.gridTop += this.drawGridArtist(gr) + spacing; | |
| 361 | + | } | |
| 362 | + | ||
| 363 | + | // * Title | |
| 364 | + | if (grSet.showGridTitle_layout) { | |
| 365 | + | this.gridTop += this.drawGridTitle(gr) + spacing; | |
| 366 | + | } else if (!grSet.showGridArtist_layout) { | |
| 367 | + | this.gridTop += this.drawGridAlbum(gr) + spacing; | |
| 368 | + | } | |
| 369 | + | ||
| 370 | + | // * Timeline | |
| 371 | + | if (grSet.showGridTimeline_layout) { | |
| 372 | + | this.setGridTimelineSize(this.gridMarginLeft, this.gridTop + spacing, grm.ui.albumArtSize.x - this.gridMarginLeft * 2, this.timelineH); | |
| 373 | + | this.drawGridTimeline(gr); | |
| 374 | + | this.gridTop += this.timelineH + this.gridLineSpacing; | |
| 375 | + | } | |
| 376 | + | ||
| 377 | + | // * Album | |
| 378 | + | if (grSet.showGridArtist_layout || grSet.showGridTitle_layout) { | |
| 379 | + | this.gridTop += this.drawGridAlbum(gr) + spacing2; | |
| 380 | + | } | |
| 381 | + | ||
| 382 | + | // * Columns key and value | |
| 383 | + | this.drawGridColumns(gr); | |
| 384 | + | } | |
| 385 | + | ||
| 386 | + | gr.SetInterpolationMode(InterpolationMode.Default); | |
| 387 | + | ||
| 388 | + | grm.debug.setDebugProfile(false, 'print', '绘图 -> 元数据表格'); | |
| 389 | + | } | |
| 390 | + | ||
| 391 | + | /** | |
| 392 | + | * Draws the custom metadata grid menu. | |
| 393 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 394 | + | */ | |
| 395 | + | drawGridMenu(gr) { | |
| 396 | + | if (!grm.ui.displayMetadataGridMenu || grSet.layout !== 'default') return; | |
| 397 | + | ||
| 398 | + | const x = grm.ui.albumArtSize.x - 1; | |
| 399 | + | const y = grm.ui.topMenuHeight; | |
| 400 | + | const width = grm.ui.ww; | |
| 401 | + | const height = grm.ui.wh - grm.ui.topMenuHeight - grm.ui.lowerBarHeight; | |
| 402 | + | ||
| 403 | + | gr.FillSolidRect(x, y, width, height, pl.col.bg); | |
| 404 | + | for (const c of CustomMenu.controlList) c.draw(gr); | |
| 405 | + | ||
| 406 | + | if (CustomMenu.activeControl && CustomMenu.activeControl instanceof CustomMenuDropDown && CustomMenu.activeControl.isSelectUp) { | |
| 407 | + | CustomMenu.activeControl.draw(gr); | |
| 408 | + | } | |
| 409 | + | } | |
| 410 | + | ||
| 411 | + | /** | |
| 412 | + | * Draws the artist on the metadata grid in the Details panel. | |
| 413 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 414 | + | * @returns {number} The height of the artist. | |
| 415 | + | */ | |
| 416 | + | drawGridArtist(gr) { | |
| 417 | + | if (!grStr.artist) return 0; | |
| 418 | + | ||
| 419 | + | // * Apply better anti-aliasing on smaller font sizes in HD res | |
| 420 | + | gr.SetTextRenderingHint(!RES._4K && (grSet.gridArtistFontSize_layout < 18 || grSet.displayScale < 100) ? TextRenderingHint.ClearTypeGridFit : TextRenderingHint.AntiAliasGridFit); | |
| 421 | + | ||
| 422 | + | const artistColor = ['white', 'black', 'reborn', 'random'].includes(grSet.theme) ? grCol.detailsText : grSet.theme === 'cream' ? pl.col.header_artist_normal : pl.col.header_artist_playing; | |
| 423 | + | DrawString(gr, grm.ui.getFormattedString('gridArtist'), grFont.gridArtist, artistColor, this.gridMarginLeft, Math.round(this.gridTop), this.gridContentWidth, this.gridArtistNumLinesHeight, Stringformat.Trim_Ellipsis_Char); | |
| 424 | + | ||
| 425 | + | // * Artist country flags | |
| 426 | + | if (grStr.artist && grSet.showGridArtistFlags_layout) { | |
| 427 | + | grm.ui.drawArtistCountryFlag(gr, 'metadataGrid'); | |
| 428 | + | } | |
| 429 | + | ||
| 430 | + | this.gridArtistTop = this.gridTop; | |
| 431 | + | this.gridArtistBottom = this.gridTop + this.gridArtistNumLinesHeight; | |
| 432 | + | return this.gridArtistNumLinesHeight; | |
| 433 | + | } | |
| 434 | + | ||
| 435 | + | /** | |
| 436 | + | * Draws the track title on the metadata grid in the Details panel. | |
| 437 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 438 | + | * @returns {number} The height of the title. | |
| 439 | + | */ | |
| 440 | + | drawGridTitle(gr) { | |
| 441 | + | if (!grStr.title) return 0; | |
| 442 | + | ||
| 443 | + | // * Apply better anti-aliasing on smaller font sizes in HD res | |
| 444 | + | gr.SetTextRenderingHint(!RES._4K && (grSet.gridTitleFontSize_layout < 18 || grSet.displayScale < 100) ? TextRenderingHint.ClearTypeGridFit : TextRenderingHint.AntiAliasGridFit); | |
| 445 | + | ||
| 446 | + | DrawString(gr, grm.ui.getFormattedString('gridTitle'), grFont.gridTitle, grCol.detailsText, this.gridMarginLeft, Math.round(this.gridTop), this.gridContentWidth, this.gridTitleNumLinesHeight, Stringformat.Trim_Ellipsis_Char); | |
| 447 | + | ||
| 448 | + | this.gridTitleTop = this.gridTop; | |
| 449 | + | this.gridTitleBottom = this.gridTop + this.gridTitleNumLinesHeight; | |
| 450 | + | return this.gridTitleNumLinesHeight; | |
| 451 | + | } | |
| 452 | + | ||
| 453 | + | /** | |
| 454 | + | * Draws the album on the metadata grid in the Details panel. | |
| 455 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 456 | + | * @returns {number} The height of the album. | |
| 457 | + | */ | |
| 458 | + | drawGridAlbum(gr) { | |
| 459 | + | if (!grStr.album) return 0; | |
| 460 | + | ||
| 461 | + | // * Apply better anti-aliasing on smaller font sizes in HD res | |
| 462 | + | gr.SetTextRenderingHint(!RES._4K && (grSet.gridAlbumFontSize_layout < 18 || grSet.displayScale < 100) ? TextRenderingHint.ClearTypeGridFit : TextRenderingHint.AntiAliasGridFit); | |
| 463 | + | ||
| 464 | + | DrawString(gr, grStr.album, grFont.gridAlbum, grCol.detailsText, this.gridMarginLeft, Math.round(this.gridTop), this.gridContentWidth, this.gridAlbumNumLinesHeight, Stringformat.Trim_Ellipsis_Char); | |
| 465 | + | ||
| 466 | + | this.gridAlbumTop = this.gridTop; | |
| 467 | + | this.gridAlbumBottom = this.gridTop + this.gridAlbumNumLinesHeight; | |
| 468 | + | return this.gridAlbumNumLinesHeight; | |
| 469 | + | } | |
| 470 | + | ||
| 471 | + | /** | |
| 472 | + | * Draws the column key and column value on the metadata grid in the Details panel. | |
| 473 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 474 | + | */ | |
| 475 | + | drawGridColumns(gr) { | |
| 476 | + | for (let k = 0; k < grStr.grid.length; k++) { | |
| 477 | + | this.gridColumnKey = grStr.grid[k].label; | |
| 478 | + | this.gridColumnValue = grStr.grid[k].val; | |
| 479 | + | this.gridTxtRec = gr.MeasureString(this.gridColumnValue, grFont.gridVal, 0, 0, this.gridColumnValueWidth, grm.ui.wh); | |
| 480 | + | this.gridColumnCellHeight = this.gridTxtRec.Height + 5; | |
| 481 | + | this.gridColumnTop = this.gridTop; | |
| 482 | + | ||
| 483 | + | let gridShowLastFmImage = false; | |
| 484 | + | let gridShowReleaseFlagImage = false; | |
| 485 | + | let gridShowCodecLogoImage = false; | |
| 486 | + | let gridShowChannelLogoImage = false; | |
| 487 | + | let gridDropShadow = false; | |
| 488 | + | let gridValueColor = grCol.detailsText; | |
| 489 | + | ||
| 490 | + | if (this.gridColumnValue.length) { | |
| 491 | + | const columnKey = { | |
| 492 | + | '目录': () => { | |
| 493 | + | gridShowReleaseFlagImage = grSet.showGridReleaseFlags_layout; | |
| 494 | + | if (grSet.showGridReleaseFlags_layout === 'logo') { | |
| 495 | + | this.gridColumnValue = this.gridColumnValue.replace($('%releasecountry%'), ''); | |
| 496 | + | } | |
| 497 | + | }, | |
| 498 | + | '发行地区': () => { | |
| 499 | + | gridShowReleaseFlagImage = grSet.showGridReleaseFlags_layout; | |
| 500 | + | if (grSet.showGridReleaseFlags_layout === 'logo') this.gridColumnValue = ''; | |
| 501 | + | }, | |
| 502 | + | '编解码': () => { | |
| 503 | + | gridShowCodecLogoImage = grSet.showGridCodecLogo_layout; | |
| 504 | + | this.gridColumnValue = grSet.showGridCodecLogo_layout === 'logo' ? '' : this.getCodecString(); | |
| 505 | + | this.gridColumnCellHeight = this.gridColumnValueHeight + 5; | |
| 506 | + | }, | |
| 507 | + | '声道': () => { | |
| 508 | + | gridShowChannelLogoImage = grSet.showGridChannelLogo_layout; | |
| 509 | + | this.gridColumnValue = grSet.showGridChannelLogo_layout === 'logo' ? '' : this.getChannelString($('%channels%')); | |
| 510 | + | this.gridColumnCellHeight = this.gridColumnValueHeight + 5; | |
| 511 | + | }, | |
| 512 | + | '热门': () => { | |
| 513 | + | gridValueColor = grCol.detailsHotness; | |
| 514 | + | gridDropShadow = true; | |
| 515 | + | }, | |
| 516 | + | '播放次数': () => { | |
| 517 | + | gridShowLastFmImage = true; | |
| 518 | + | }, | |
| 519 | + | '评级': () => { | |
| 520 | + | gridValueColor = grCol.detailsRating; | |
| 521 | + | gridDropShadow = true; | |
| 522 | + | }, | |
| 523 | + | 'default': () => { | |
| 524 | + | let matchCount = 0; | |
| 525 | + | // * On small player sizes, there is no space for all metadata entries. | |
| 526 | + | // * Hide them and only display entries from basicMeta. | |
| 527 | + | if (this.basicMetadataDisplay(this.gridColumnKey)) { | |
| 528 | + | this.gridColumnValue = ''; | |
| 529 | + | this.gridColumnKey = ''; | |
| 530 | + | matchCount++; | |
| 531 | + | } | |
| 532 | + | this.gridTop -= this.gridColumnCellHeight * matchCount; | |
| 533 | + | } | |
| 534 | + | }; | |
| 535 | + | (columnKey[this.gridColumnKey] || columnKey.default)(); | |
| 536 | + | ||
| 537 | + | if (this.gridTop + this.gridTxtRec.Height < grm.ui.albumArtSize.y + grm.ui.albumArtSize.h) { | |
| 538 | + | // * Apply better anti-aliasing on smaller font sizes in HD res | |
| 539 | + | gr.SetTextRenderingHint(!RES._4K && (grSet.gridKeyFontSize_layout < 17 || grSet.gridValueFontSize_layout + SCALE(1) < 18 || grSet.displayScale < 100) ? TextRenderingHint.ClearTypeGridFit : TextRenderingHint.AntiAliasGridFit); | |
| 540 | + | ||
| 541 | + | if (gridDropShadow) { | |
| 542 | + | const gridBorderWidth = SCALE(0.5); | |
| 543 | + | gr.DrawString(this.gridColumnValue, grFont.gridVal, grCol.primary_rgb_s050, Math.round(this.gridColumnValueLeft + gridBorderWidth), Math.round(this.gridTop + gridBorderWidth), this.gridColumnValueWidth, this.gridColumnCellHeight, StringFormat(0, 0, 4)); | |
| 544 | + | gr.DrawString(this.gridColumnValue, grFont.gridVal, grCol.primary_rgb_s050, Math.round(this.gridColumnValueLeft - gridBorderWidth), Math.round(this.gridTop + gridBorderWidth), this.gridColumnValueWidth, this.gridColumnCellHeight, StringFormat(0, 0, 4)); | |
| 545 | + | gr.DrawString(this.gridColumnValue, grFont.gridVal, grCol.primary_rgb_s050, Math.round(this.gridColumnValueLeft + gridBorderWidth), Math.round(this.gridTop - gridBorderWidth), this.gridColumnValueWidth, this.gridColumnCellHeight, StringFormat(0, 0, 4)); | |
| 546 | + | gr.DrawString(this.gridColumnValue, grFont.gridVal, grCol.primary_rgb_s050, Math.round(this.gridColumnValueLeft - gridBorderWidth), Math.round(this.gridTop - gridBorderWidth), this.gridColumnValueWidth, this.gridColumnCellHeight, StringFormat(0, 0, 4)); | |
| 547 | + | } | |
| 548 | + | gr.DrawString(this.gridColumnKey, grFont.gridKey, grCol.detailsText, this.gridMarginLeft, Math.round(this.gridTop), this.gridColumnKeyWidth, this.gridColumnCellHeight, Stringformat.Trim_Ellipsis_Char); | |
| 549 | + | gr.DrawString(this.gridColumnValue, grFont.gridVal, gridValueColor, this.gridColumnValueLeft, Math.round(this.gridTop), this.gridColumnValueWidth, this.gridColumnCellHeight, StringFormat(0, 0, 4)); | |
| 550 | + | ||
| 551 | + | // * Release flag | |
| 552 | + | if (gridShowReleaseFlagImage) { | |
| 553 | + | this.drawGridReleaseFlag(gr); | |
| 554 | + | } | |
| 555 | + | // * Codec logo | |
| 556 | + | if (gridShowCodecLogoImage) { | |
| 557 | + | this.drawGridCodecLogo(gr); | |
| 558 | + | } | |
| 559 | + | // * Channel logo | |
| 560 | + | if (gridShowChannelLogoImage) { | |
| 561 | + | this.drawGridChannelLogo(gr); | |
| 562 | + | } | |
| 563 | + | // * Last.fm logo | |
| 564 | + | if (gridShowLastFmImage) { | |
| 565 | + | this.drawGridLastfmLogo(gr); | |
| 566 | + | } | |
| 567 | + | this.gridTop += this.gridColumnCellHeight + 5; | |
| 568 | + | } | |
| 569 | + | } | |
| 570 | + | } | |
| 571 | + | } | |
| 572 | + | ||
| 573 | + | /** | |
| 574 | + | * Draws an image on the metadata grid in the Details panel. | |
| 575 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 576 | + | * @param {GdiBitmap} image - The image to draw. | |
| 577 | + | * @param {boolean} showLogoOnly - Whether to show only the logo. | |
| 578 | + | * @param {number} xOffset - The offset added to x position. | |
| 579 | + | * @param {number} yOffset - The offset added to y position. | |
| 580 | + | * @param {number} cellHeightAdjustment - The adjustment applied to cell height. | |
| 581 | + | */ | |
| 582 | + | drawGridImage(gr, image, showLogoOnly, xOffset = 0, yOffset = 0, cellHeightAdjustment = 0) { | |
| 583 | + | if (image == null) return; | |
| 584 | + | ||
| 585 | + | // Calculate metrics and ratios | |
| 586 | + | const gridColumnValueMetrics = gr.MeasureString(showLogoOnly ? '' : this.gridColumnValue, grFont.gridVal, 0, 0, this.gridColumnValueWidth, grm.ui.wh); | |
| 587 | + | const heightRatio = (gr.CalcTextHeight(showLogoOnly ? 'Ag' : this.gridColumnValue, grFont.gridVal) - cellHeightAdjustment) / image.Height; | |
| 588 | + | const logoHeight = Math.round(image.Height * heightRatio); | |
| 589 | + | const logoWidth = Math.round(image.Width * heightRatio); | |
| 590 | + | ||
| 591 | + | // Get the width of the last line | |
| 592 | + | const newLineWidth = gr.EstimateLineWrap(this.gridColumnValue, grFont.gridVal, this.gridTxtRec.Lines === 1 ? this.gridColumnValueWidth : this.gridTxtRec.Width); | |
| 593 | + | const lastLineIndex = newLineWidth.length - 1; | |
| 594 | + | const lastLineWidth = newLineWidth[lastLineIndex] || gridColumnValueMetrics.Width; | |
| 595 | + | ||
| 596 | + | // Initial positions | |
| 597 | + | const stringWidth = lastLineWidth + xOffset; | |
| 598 | + | let xPos = this.gridColumnValueLeft + stringWidth; | |
| 599 | + | let yPos = this.gridTop + yOffset; | |
| 600 | + | ||
| 601 | + | // Adjust positions if the logo width exceeds the grid column width and move logo to the next line | |
| 602 | + | if (xPos + logoWidth > this.gridColumnValueLeft + this.gridColumnValueWidth) { | |
| 603 | + | const textHeight = gr.CalcTextHeight('Ag', grFont.gridVal); | |
| 604 | + | xPos = this.gridColumnValueLeft; | |
| 605 | + | yPos += textHeight; | |
| 606 | + | this.gridTxtRec = { ...this.gridTxtRec, Lines: this.gridTxtRec.Lines + 1, Height: this.gridTxtRec.Height + textHeight }; | |
| 607 | + | this.gridColumnCellHeight = this.gridTxtRec.Height + 5; | |
| 608 | + | } | |
| 609 | + | ||
| 610 | + | gr.DrawImage(image, xPos, yPos, logoWidth, logoHeight, 0, 0, image.Width, image.Height); | |
| 611 | + | } | |
| 612 | + | ||
| 613 | + | /** | |
| 614 | + | * Draws the release flag on the metadata grid in the Details panel. | |
| 615 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 616 | + | */ | |
| 617 | + | drawGridReleaseFlag(gr) { | |
| 618 | + | if (this.gridReleaseFlagImg == null) return; | |
| 619 | + | ||
| 620 | + | const logoOnly = grSet.showGridReleaseFlags_layout === 'logo' && this.gridColumnKey === '发行地区'; | |
| 621 | + | const lineHeight = this.gridTxtRec.Height / this.gridTxtRec.Lines; | |
| 622 | + | const yCorr = (this.gridTxtRec.Lines - 1) * lineHeight; | |
| 623 | + | ||
| 624 | + | this.drawGridImage(gr, this.gridReleaseFlagImg, logoOnly, SCALE(logoOnly ? 0 : 8), logoOnly ? 0 : yCorr); | |
| 625 | + | } | |
| 626 | + | ||
| 627 | + | /** | |
| 628 | + | * Draws the codec logo on the metadata grid in the Details panel. | |
| 629 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 630 | + | */ | |
| 631 | + | drawGridCodecLogo(gr) { | |
| 632 | + | if (this.gridCodecLogo == null) { | |
| 633 | + | this.loadGridCodecLogo(); | |
| 634 | + | if (this.gridCodecLogo == null) return; | |
| 635 | + | } | |
| 636 | + | ||
| 637 | + | const logoOnly = grSet.showGridCodecLogo_layout === 'logo'; | |
| 638 | + | this.drawGridImage(gr, this.gridCodecLogo, logoOnly, SCALE(logoOnly ? 0 : 8), logoOnly ? -1 : 2); | |
| 639 | + | } | |
| 640 | + | ||
| 641 | + | /** | |
| 642 | + | * Draws the channel logo on the metadata grid in the Details panel. | |
| 643 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 644 | + | */ | |
| 645 | + | drawGridChannelLogo(gr) { | |
| 646 | + | if (this.gridChannelLogo == null) { | |
| 647 | + | this.loadGridChannelLogo(); | |
| 648 | + | if (this.gridChannelLogo == null) return; | |
| 649 | + | } | |
| 650 | + | ||
| 651 | + | const logoOnly = grSet.showGridChannelLogo_layout === 'logo'; | |
| 652 | + | this.drawGridImage(gr, this.gridChannelLogo, logoOnly, SCALE(logoOnly ? 0 : 8), logoOnly ? -1 : 2); | |
| 653 | + | } | |
| 654 | + | ||
| 655 | + | /** | |
| 656 | + | * Draws the last.fm logo on the metadata grid in the Details panel. | |
| 657 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 658 | + | */ | |
| 659 | + | drawGridLastfmLogo(gr) { | |
| 660 | + | if (!this.playCountVerifiedByLastFm) return; | |
| 661 | + | ||
| 662 | + | const lastFmImg = gdi.Image(grPath.lastFmImageRed); | |
| 663 | + | const lastFmWhiteImg = gdi.Image(grPath.lastFmImageWhite); | |
| 664 | + | const lastFmLogo = grCol.lightBgDetails ? lastFmImg : lastFmWhiteImg; | |
| 665 | + | const lineHeight = this.gridTxtRec.Height / this.gridTxtRec.Lines; | |
| 666 | + | const yCorr = (this.gridTxtRec.Lines - 1) * lineHeight; | |
| 667 | + | ||
| 668 | + | this.drawGridImage(gr, lastFmLogo, false, SCALE(8), yCorr, 6); | |
| 669 | + | } | |
| 670 | + | ||
| 671 | + | /** | |
| 672 | + | * Draws the band logo on the bottom left side in the Details panel. | |
| 673 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 674 | + | */ | |
| 675 | + | drawBandLogo(gr) { | |
| 676 | + | if (!fb.IsPlaying && !grSet.panelBrowseMode || !grm.ui.albumArt || grSet.layout !== 'default' || !grm.ui.displayDetails || | |
| 677 | + | grSet.lyricsLayout === 'full' && grm.ui.displayLyrics) { | |
| 678 | + | return; | |
| 679 | + | } | |
| 680 | + | ||
| 681 | + | grm.debug.setDebugProfile(grm.debug.showDrawExtendedTiming, 'create', '绘图 -> 艺术家标识'); | |
| 682 | + | ||
| 683 | + | const availableSpace = grm.ui.albumArtSize.y + grm.ui.albumArtSize.h - this.gridTop; | |
| 684 | + | const logo = grCol.lightBgDetails || grm.ui.noAlbumArtStub ? (this.bandLogoInverted || this.bandLogo) : this.bandLogo; | |
| 685 | + | ||
| 686 | + | if (logo && availableSpace > 75) { | |
| 687 | + | let logoWidth = Math.min(HD_4K(logo.Width / 2, logo.Width), grm.ui.albumArtSize.x - grm.ui.ww * 0.05); | |
| 688 | + | const heightScale = Math.min(logoWidth / logo.Width, availableSpace / logo.Height); | |
| 689 | + | logoWidth = logo.Width * heightScale; // Adjust logoWidth after heightScale is potentially updated | |
| 690 | + | ||
| 691 | + | const logoX = Math.round(grm.ui.isStreaming ? SCALE(40) : grm.ui.albumArtSize.x / 2 - logoWidth / 2); | |
| 692 | + | const logoY = Math.round(grm.ui.albumArtSize.y + grm.ui.albumArtSize.h - (logo.Height * heightScale)) - HD_4K(4, 24); | |
| 693 | + | const logoW = Math.round(logoWidth); | |
| 694 | + | const logoH = Math.round(logo.Height * heightScale); | |
| 695 | + | ||
| 696 | + | gr.DrawImage(logo, logoX, logoY, logoW, logoH, 0, 0, logo.Width, logo.Height, 0); | |
| 697 | + | } | |
| 698 | + | ||
| 699 | + | grm.debug.setDebugProfile(false, 'print', '绘图 -> 艺术家标识'); | |
| 700 | + | } | |
| 701 | + | ||
| 702 | + | /** | |
| 703 | + | * Draws the label logo on the bottom right side in the Details panel. | |
| 704 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 705 | + | */ | |
| 706 | + | drawLabelLogo(gr) { | |
| 707 | + | if (!fb.IsPlaying && !grSet.panelBrowseMode || !grm.ui.albumArt || grSet.layout !== 'default' || !grm.ui.displayDetails || | |
| 708 | + | grSet.lyricsLayout === 'full' && grm.ui.displayLyrics) { | |
| 709 | + | return; | |
| 710 | + | } | |
| 711 | + | ||
| 712 | + | grm.debug.setDebugProfile(grm.debug.showDrawExtendedTiming, 'create', '绘图 -> 唱片公司标识'); | |
| 713 | + | ||
| 714 | + | if (this.labelLogo.length > 0) { | |
| 715 | + | const lightBg = grSet.labelArtOnBg ? grCol.lightBgMain : grCol.lightBgDetails; | |
| 716 | + | const labels = lightBg || grm.ui.noAlbumArtStub ? (this.labelLogoInverted.length ? this.labelLogoInverted : this.labelLogo) : this.labelLogo; | |
| 717 | + | const rightSideGap = 20; // How close last label is to right edge | |
| 718 | + | const leftEdgeGap = (grm.ui.albumArtOffCenter ? 20 : 40) * HD_4K(1, 1.8); // Space between art and label | |
| 719 | + | const leftEdgeWidth = HD_4K(30, 45); // How far label background extends on left | |
| 720 | + | const maxLabelWidth = SCALE(200); | |
| 721 | + | let leftEdge = 0; | |
| 722 | + | let topEdge = 0; | |
| 723 | + | let totalLabelWidth = 0; | |
| 724 | + | let labelAreaWidth = 0; | |
| 725 | + | let labelSpacing = 0; | |
| 726 | + | let labelWidth; | |
| 727 | + | let labelHeight; | |
| 728 | + | ||
| 729 | + | for (const label of labels) { | |
| 730 | + | if (label.Width > maxLabelWidth) { | |
| 731 | + | totalLabelWidth += maxLabelWidth; | |
| 732 | + | } else { | |
| 733 | + | totalLabelWidth += RES._4K && label.Width < 200 ? label.Width * 2 : label.Width; | |
| 734 | + | } | |
| 735 | + | } | |
| 736 | + | if (!this.cachedLabelLastLeftEdge) { // We don't want to recalculate this every screen refresh | |
| 737 | + | grm.debug.debugLog('图标 => 重新计算最后左边缘'); | |
| 738 | + | this.shadowImgLabel = null; | |
| 739 | + | labelWidth = Math.round(totalLabelWidth / labels.length); | |
| 740 | + | labelHeight = Math.round(labels[0].Height * labelWidth / labels[0].Width); // Might be recalc'd below | |
| 741 | + | if (grm.ui.albumArt) { | |
| 742 | + | if (this.discArt && grSet.displayDiscArt) { | |
| 743 | + | leftEdge = Math.round(Math.max(grm.ui.albumArtSize.x + grm.ui.albumArtScaled.Width + 5, grm.ui.ww * 0.975 - totalLabelWidth + 1)); | |
| 744 | + | const discCenter = {}; | |
| 745 | + | discCenter.x = Math.round(this.discArtSize.x + this.discArtSize.w / 2); | |
| 746 | + | discCenter.y = Math.round(this.discArtSize.y + this.discArtSize.h / 2); | |
| 747 | + | const radius = discCenter.y - this.discArtSize.y; | |
| 748 | + | const radiusSquared = radius * radius; | |
| 749 | + | let posValid = false; | |
| 750 | + | ||
| 751 | + | while (!posValid) { | |
| 752 | + | const allLabelsWidth = Math.max(Math.min(Math.round((grm.ui.ww - leftEdge - rightSideGap) / labels.length), maxLabelWidth), 50); | |
| 753 | + | //console.log("leftEdge = " + leftEdge + ", grm.ui.ww-leftEdge-10 = " + (grm.ui.ww-leftEdge-10) + ", allLabelsWidth=" + allLabelsWidth); | |
| 754 | + | const maxWidth = RES._4K && labels[0].Width < 200 ? labels[0].Width * 2 : labels[0].Width; | |
| 755 | + | labelWidth = (allLabelsWidth > maxWidth) ? maxWidth : allLabelsWidth; | |
| 756 | + | labelHeight = Math.round(labels[0].Height * labelWidth / labels[0].Width); // Width is based on height scale | |
| 757 | + | topEdge = Math.round(grm.ui.albumArtSize.y + grm.ui.albumArtSize.h - labelHeight); | |
| 758 | + | ||
| 759 | + | const a = topEdge - discCenter.y + 1; // Adding 1 to a and b so that the border just touches the edge of the discArt | |
| 760 | + | const b = leftEdge - discCenter.x + 1; | |
| 761 | + | ||
| 762 | + | if ((a * a + b * b) > radiusSquared) { | |
| 763 | + | posValid = true; | |
| 764 | + | } else { | |
| 765 | + | leftEdge += 4; | |
| 766 | + | } | |
| 767 | + | } | |
| 768 | + | } else { | |
| 769 | + | leftEdge = Math.round(Math.max(grm.ui.albumArtSize.x + grm.ui.albumArtSize.w + leftEdgeWidth + leftEdgeGap, grm.ui.ww * 0.975 - totalLabelWidth + 1)); | |
| 770 | + | } | |
| 771 | + | } else { | |
| 772 | + | leftEdge = Math.round(grm.ui.ww * 0.975 - totalLabelWidth); | |
| 773 | + | } | |
| 774 | + | labelAreaWidth = grm.ui.ww - leftEdge - rightSideGap; | |
| 775 | + | this.cachedLabelLastLeftEdge = leftEdge; | |
| 776 | + | this.cachedLabelLastHeight = labelHeight; | |
| 777 | + | } | |
| 778 | + | else { // Already calculated | |
| 779 | + | leftEdge = this.cachedLabelLastLeftEdge; | |
| 780 | + | labelHeight = this.cachedLabelLastHeight; | |
| 781 | + | labelAreaWidth = grm.ui.ww - leftEdge - rightSideGap; | |
| 782 | + | } | |
| 783 | + | if (labelAreaWidth >= SCALE(50)) { | |
| 784 | + | if (labels.length > 1) { | |
| 785 | + | labelSpacing = Math.min(12, Math.max(3, Math.round((labelAreaWidth / (labels.length - 1)) * 0.048))); // Spacing should be proportional, and between 3 and 12 pixels | |
| 786 | + | } | |
| 787 | + | // console.log('labelAreaWidth = ' + labelAreaWidth + ", labelSpacing = " + labelSpacing); | |
| 788 | + | const allLabelsWidth = Math.max(Math.min(Math.round((labelAreaWidth - (labelSpacing * (labels.length - 1))) / labels.length), maxLabelWidth), 50); // allLabelsWidth must be between 50 and 200 pixels wide | |
| 789 | + | const origLabelHeight = labelHeight; | |
| 790 | + | let labelX = leftEdge; | |
| 791 | + | topEdge = grm.ui.albumArtSize.y + grm.ui.albumArtSize.h - labelHeight - 20; | |
| 792 | + | ||
| 793 | + | if (!grSet.labelArtOnBg && !grSet.noDiscArtBg || grSet.noDiscArtBg && grSet.displayDiscArt && this.discArt) { | |
| 794 | + | if (!['black', 'nblue', 'ngreen', 'nred', 'ngold'].includes(grSet.theme)) { | |
| 795 | + | if (!this.shadowImgLabel) { | |
| 796 | + | this.shadowImgLabel = ShadowRect(this.discArtShadow, this.discArtShadow, grm.ui.ww - labelX + leftEdgeWidth, labelHeight + 40, this.discArtShadow, grCol.shadow); | |
| 797 | + | } | |
| 798 | + | gr.DrawImage(this.shadowImgLabel, labelX - leftEdgeWidth - this.discArtShadow, topEdge - 20 - this.discArtShadow, grm.ui.ww - labelX + leftEdgeWidth + 2 * this.discArtShadow, labelHeight + 40 + 2 * this.discArtShadow, | |
| 799 | + | 0, 0, this.shadowImgLabel.Width, this.shadowImgLabel.Height); | |
| 800 | + | } | |
| 801 | + | gr.SetSmoothingMode(SmoothingMode.None); // Disable smoothing | |
| 802 | + | gr.FillSolidRect(labelX - leftEdgeWidth, topEdge - 20, grm.ui.ww - labelX + leftEdgeWidth, labelHeight + 40, grCol.detailsBg); | |
| 803 | + | gr.DrawRect(labelX - leftEdgeWidth, topEdge - 20, grm.ui.ww - labelX + leftEdgeWidth, labelHeight + 40 - 1, 1, grCol.shadow); | |
| 804 | + | gr.SetSmoothingMode(SmoothingMode.HighQuality); | |
| 805 | + | } | |
| 806 | + | for (let i = 0; i < labels.length; i++) { | |
| 807 | + | // allLabelsWidth can never be greater than 200, so if a label image is 161 pixels wide, never draw it wider than 161 | |
| 808 | + | const maxWidth = RES._4K && labels[i].Width < 200 ? labels[i].Width * 2 : labels[i].Width; | |
| 809 | + | labelWidth = (allLabelsWidth > maxWidth) ? maxWidth : allLabelsWidth; | |
| 810 | + | labelHeight = Math.round(labels[i].Height * labelWidth / labels[i].Width); // Width is based on height scale | |
| 811 | + | ||
| 812 | + | gr.DrawImage(labels[i], labelX, Math.round(topEdge + origLabelHeight / 2 - labelHeight / 2), labelWidth, labelHeight, 0, 0, this.labelLogo[i].Width, this.labelLogo[i].Height); | |
| 813 | + | labelX += labelWidth + labelSpacing; | |
| 814 | + | } | |
| 815 | + | } | |
| 816 | + | } | |
| 817 | + | ||
| 818 | + | grm.debug.setDebugProfile(false, 'print', '绘图 -> 唱片公司标识'); | |
| 819 | + | } | |
| 820 | + | // #endregion | |
| 821 | + | ||
| 822 | + | // * PLUBLIC METHODS - METRICS * // | |
| 823 | + | // #region PUBLIC METHODS - METRICS | |
| 824 | + | /** | |
| 825 | + | * Sets the metadata grid metrics and caches all calculated values. | |
| 826 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 827 | + | */ | |
| 828 | + | setGridMetrics(gr) { | |
| 829 | + | if (this.cachedGridMetrics) return; | |
| 830 | + | ||
| 831 | + | const metricsPromises = [ | |
| 832 | + | new Promise((resolve) => this.setGridMainMetrics(gr, resolve)), | |
| 833 | + | new Promise((resolve) => this.setGridTextMetrics(gr, resolve)) | |
| 834 | + | ]; | |
| 835 | + | ||
| 836 | + | Promise.all(metricsPromises).then(() => { | |
| 837 | + | this.cachedGridMetrics = this.gridColumnValueBottom > this.gridColumnTop && !grm.display.hasPlayerSizeChanged(); | |
| 838 | + | }); | |
| 839 | + | } | |
| 840 | + | ||
| 841 | + | /** | |
| 842 | + | * Sets the metadata grid main sizes. | |
| 843 | + | * This includes calculating margins, content width, and column dimensions. | |
| 844 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 845 | + | * @param {Function} metricsCalculated - The callback function to be executed after calculations are finished. | |
| 846 | + | */ | |
| 847 | + | setGridMainMetrics(gr, metricsCalculated) { | |
| 848 | + | this.discArtShadow = SCALE(6); | |
| 849 | + | this.gridTooltipHeight = SCALE(100); | |
| 850 | + | this.timelineH = SCALE(8); | |
| 851 | + | ||
| 852 | + | this.gridMarginLeft = grm.ui.edgeMargin; | |
| 853 | + | this.gridTopStart = grm.ui.albumArtSize.y ? grm.ui.albumArtSize.y + grm.ui.edgeMargin : grm.ui.topMenuHeight + grm.ui.edgeMargin; | |
| 854 | + | this.gridTop = this.gridTopStart; | |
| 855 | + | this.gridContentWidth = Math.floor((!grm.ui.albumArt && this.discArt ? this.discArtSize.x : grm.ui.albumArtSize.x) - grm.ui.edgeMargin * 1.5); | |
| 856 | + | ||
| 857 | + | this.gridColumnKeyWidth = CalcGridMaxTextWidth(gr, grStr.grid, grFont.gridKey); | |
| 858 | + | this.gridColumnKeyHeight = gr.MeasureString('Ag', grFont.gridKey, 0, 0, this.gridContentWidth, grm.ui.wh).Height; | |
| 859 | + | this.gridColumnKeyBottom = this.gridColumnTop + this.gridColumnKeyHeight; | |
| 860 | + | ||
| 861 | + | this.gridColumnValueWidth = this.gridContentWidth - this.gridColumnMargin - this.gridColumnKeyWidth + SCALE(5); | |
| 862 | + | this.gridColumnValueHeight = gr.MeasureString('Ag', grFont.gridVal, 0, 0, this.gridContentWidth, grm.ui.wh).Height; | |
| 863 | + | this.gridColumnValueLeft = this.gridMarginLeft + this.gridColumnKeyWidth + this.gridColumnMargin; | |
| 864 | + | this.gridColumnValueBottom = this.gridColumnTop + this.gridColumnValueHeight; | |
| 865 | + | ||
| 866 | + | metricsCalculated(); | |
| 867 | + | } | |
| 868 | + | ||
| 869 | + | /** | |
| 870 | + | * Sets the metadata grid text sizes. | |
| 871 | + | * This includes calculating wrap information and dimensions for artist, title, album, and other text elements based on the grid configuration. | |
| 872 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 873 | + | * @param {Function} metricsCalculated - The callback function to be executed after calculations are finished. | |
| 874 | + | */ | |
| 875 | + | setGridTextMetrics(gr, metricsCalculated) { | |
| 876 | + | if (grSet.showGridArtist_layout) { | |
| 877 | + | this.gridFlagSizeW = grm.ui.getFlagSizeWidth('metadataGrid'); | |
| 878 | + | this.gridFlagSizeWhiteSpace = grm.ui.getFlagSizeWhiteSpace('metadataGrid'); | |
| 879 | + | this.gridArtistWrapInfo = CalcWrapSpace(gr, grStr.artist, grFont.gridArtist, this.gridContentWidth, this.cachedGridWrapSpace); | |
| 880 | + | this.gridArtistWrapLinesExceed = this.gridArtistWrapInfo.lineCount > 2; | |
| 881 | + | this.gridArtistWrapWidth = this.gridArtistWrapInfo.totalWrapSpace - this.gridFlagSizeW; | |
| 882 | + | this.gridArtistWidth = Math.ceil(gr.MeasureString(grStr.artist, grFont.gridArtist, 0, 0, 0, 0, Stringformat.Trim_Ellipsis_Char | Stringformat.Measure_Trailing_Spaces).Width + this.gridFlagSizeW + this.gridArtistWrapWidth); | |
| 883 | + | this.gridArtistHeight = gr.MeasureString(grStr.artist, grFont.gridArtist, 0, 0, 0, 0).Height; | |
| 884 | + | this.gridArtistTxtRec = gr.MeasureString(grStr.artist, grFont.gridArtist, 0, 0, grSet.showGridArtistFlags_layout && grm.ui.flagImgs.length ? this.gridContentWidth - this.gridFlagSizeW : this.gridContentWidth, grm.ui.wh); | |
| 885 | + | this.gridArtistNumLines = Math.min(2, this.gridArtistTxtRec.Lines); | |
| 886 | + | this.gridArtistNumLinesHeight = gr.CalcTextHeight(grStr.artist, grFont.gridArtist) * this.gridArtistNumLines; | |
| 887 | + | } | |
| 888 | + | if (grSet.showGridTitle_layout) { | |
| 889 | + | this.gridTrackNumWidth = Math.ceil(gr.MeasureString(grStr.tracknum, grFont.gridTrackNumber, 0, 0, 0, 0).Width); | |
| 890 | + | this.gridTitleWrapInfo = CalcWrapSpace(gr, grm.ui.getFormattedString('gridTitle'), grFont.gridTitle, this.gridContentWidth, this.cachedGridWrapSpace); | |
| 891 | + | this.gridTitleWrapLinesExceed = this.gridTitleWrapInfo.lineCount > 2; | |
| 892 | + | this.gridTitleWrapWidth = this.gridTitleWrapInfo.totalWrapSpace; | |
| 893 | + | this.gridTitleWidth = Math.ceil(gr.MeasureString(grStr.title, grFont.gridTitle, 0, 0, 0, 0, Stringformat.Trim_Ellipsis_Char | Stringformat.Measure_Trailing_Spaces).Width + this.gridTrackNumWidth + this.gridTrackNumSpacing + this.gridTitleWrapWidth); | |
| 894 | + | this.gridTitleHeight = gr.MeasureString(grStr.title, grFont.gridTitle, 0, 0, 0, 0).Height; | |
| 895 | + | this.gridTitleTxtRec = gr.MeasureString(grm.ui.getFormattedString('gridTitle'), grFont.gridTitle, 0, 0, this.gridContentWidth, grm.ui.wh); | |
| 896 | + | this.gridTitleNumLines = Math.min(2, this.gridTitleTxtRec.Lines); | |
| 897 | + | this.gridTitleNumLinesHeight = gr.CalcTextHeight(grStr.title, grFont.gridTitle) * this.gridTitleNumLines; | |
| 898 | + | } | |
| 899 | + | this.gridAlbumWrapInfo = CalcWrapSpace(gr, grStr.album, grFont.gridAlbum, this.gridContentWidth, this.cachedGridWrapSpace); | |
| 900 | + | this.gridAlbumWrapLinesExceed = this.gridAlbumWrapInfo.lineCount > (grSet.showGridArtist_layout || grSet.showGridTitle_layout ? 2 : 3); | |
| 901 | + | this.gridAlbumWrapWidth = this.gridAlbumWrapInfo.totalWrapSpace; | |
| 902 | + | this.gridAlbumWidth = Math.ceil(gr.MeasureString(grStr.album, grFont.gridAlbum, 0, 0, 0, 0, Stringformat.Trim_Ellipsis_Char | Stringformat.Measure_Trailing_Spaces).Width) + this.gridAlbumWrapWidth; | |
| 903 | + | this.gridAlbumHeight = gr.MeasureString(grStr.album, grFont.gridAlbum, 0, 0, 0, 0).Height; | |
| 904 | + | this.gridAlbumTxtRec = gr.MeasureString(grStr.album, grFont.gridAlbum, 0, 0, this.gridContentWidth, grm.ui.wh); | |
| 905 | + | this.gridAlbumNumLines = Math.min(grSet.showGridArtist_layout || grSet.showGridTitle_layout ? 2 : 3, this.gridAlbumTxtRec.Lines); | |
| 906 | + | this.gridAlbumNumLinesHeight = gr.CalcTextHeight(grStr.album, grFont.gridAlbum) * this.gridAlbumNumLines; | |
| 907 | + | ||
| 908 | + | metricsCalculated(); | |
| 909 | + | } | |
| 910 | + | // #endregion | |
| 911 | + | ||
| 912 | + | // * PUBLIC METHODS - COMMON * // | |
| 913 | + | // #region PUBLIC METHODS - COMMON | |
| 914 | + | /** | |
| 915 | + | * Clears individual cache properties, the specified cache type, or all caches. | |
| 916 | + | * @param {string} [type] - The type of cache to clear. Can be 'metrics', 'discArt', 'codecLogo', 'channelLogo', 'bandLogo', 'labelLogo'. If not provided, all caches will be cleared. | |
| 917 | + | * @param {string} [property] - The specific property to clear within the cache type. Applicable only if `type` is provided. | |
| 918 | + | * @param {boolean} [clearArtCache] - Whether to clear everything in the artCache object. | |
| 919 | + | * @param {boolean} [keepDiscArt] - Whether to keep the disc art. This is considered only when `type` is 'discArt' or not provided (clearing all caches). | |
| 920 | + | * @example | |
| 921 | + | * // Clear an individual property within a specific cache type | |
| 922 | + | * clearCache('metrics', 'cachedGridMetrics'); | |
| 923 | + | * @example | |
| 924 | + | * // Clear a specific cache type | |
| 925 | + | * clearCache('metrics'); | |
| 926 | + | * @example | |
| 927 | + | * // Clear all caches | |
| 928 | + | * clearCache(); | |
| 929 | + | * @example | |
| 930 | + | * // Clear all caches and the artCache | |
| 931 | + | * clearCache(undefined, undefined, true); | |
| 932 | + | */ | |
| 933 | + | clearCache(type, property, clearArtCache, keepDiscArt) { | |
| 934 | + | const cacheActions = { | |
| 935 | + | metrics: () => { | |
| 936 | + | this.cachedGridWrapSpace = {}; | |
| 937 | + | this.cachedGridMetrics = false; | |
| 938 | + | this.cachedLabelLastLeftEdge = 0; | |
| 939 | + | this.cachedLabelLastHeight = 0; | |
| 940 | + | }, | |
| 941 | + | discArt: () => { | |
| 942 | + | this.discArt = keepDiscArt ? this.discArt : null; | |
| 943 | + | this.discArtCover = null; | |
| 944 | + | this.discArtArray = []; | |
| 945 | + | this.discArtRotation = null; | |
| 946 | + | }, | |
| 947 | + | codecLogo: () => { | |
| 948 | + | this.gridCodecLogo = null; | |
| 949 | + | }, | |
| 950 | + | channelLogo: () => { | |
| 951 | + | this.gridChannelLogo = null; | |
| 952 | + | }, | |
| 953 | + | bandLogo: () => { | |
| 954 | + | this.bandLogo = null; | |
| 955 | + | this.bandLogoInverted = null; | |
| 956 | + | }, | |
| 957 | + | labelLogo: () => { | |
| 958 | + | this.labelLogo = []; | |
| 959 | + | this.labelLogoInverted = []; | |
| 960 | + | } | |
| 961 | + | }; | |
| 962 | + | ||
| 963 | + | if (clearArtCache) { | |
| 964 | + | grm.artCache && grm.artCache.clear(); | |
| 965 | + | grm.debug.debugLog('详情缓存 => 已清除图片缓存'); | |
| 966 | + | } | |
| 967 | + | ||
| 968 | + | if (type) { | |
| 969 | + | // * Clear individual cache property | |
| 970 | + | if (property && Object.hasOwnProperty.call(this, property)) { | |
| 971 | + | this[property] = null; | |
| 972 | + | grm.debug.debugLog(`详情缓存 => 已清除缓存类型 "${type}" 中的属性 "${property}"`); | |
| 973 | + | } | |
| 974 | + | // * Clear specific cache type | |
| 975 | + | else if (cacheActions[type]) { | |
| 976 | + | cacheActions[type](); | |
| 977 | + | grm.debug.debugLog(`详情缓存 => 已清除缓存类型 "${type}"`); | |
| 978 | + | } | |
| 979 | + | return; | |
| 980 | + | } | |
| 981 | + | ||
| 982 | + | // * Clear all caches | |
| 983 | + | for (const action in cacheActions) { | |
| 984 | + | cacheActions[action](); | |
| 985 | + | } | |
| 986 | + | grm.debug.debugLog('详情缓存 => 已清除全部缓存'); | |
| 987 | + | } | |
| 988 | + | ||
| 989 | + | /** | |
| 990 | + | * Clears timers based on the timer type. | |
| 991 | + | * @param {string} [type] - The type of timer to clear. If not provided, all timers will be cleared. | |
| 992 | + | * - 'discArt'. | |
| 993 | + | */ | |
| 994 | + | clearTimer(type) { | |
| 995 | + | const timers = { | |
| 996 | + | discArt: { | |
| 997 | + | timer: this.discArtRotationTimer, | |
| 998 | + | clear: clearInterval, | |
| 999 | + | log: '计时器 => 碟片旋转计时器已清除' | |
| 1000 | + | } | |
| 1001 | + | }; | |
| 1002 | + | ||
| 1003 | + | const clearTimerByType = (type) => { | |
| 1004 | + | const { timer, clear, log } = timers[type]; | |
| 1005 | + | if (timer) { | |
| 1006 | + | clear(timer); | |
| 1007 | + | timers[type].timer = null; | |
| 1008 | + | } | |
| 1009 | + | grm.debug.debugLog(log); | |
| 1010 | + | }; | |
| 1011 | + | ||
| 1012 | + | if (type && timers[type]) { | |
| 1013 | + | clearTimerByType(type); | |
| 1014 | + | } else { | |
| 1015 | + | for (const key in timers) { | |
| 1016 | + | clearTimerByType(key); | |
| 1017 | + | } | |
| 1018 | + | } | |
| 1019 | + | } | |
| 1020 | + | // #endregion | |
| 1021 | + | ||
| 1022 | + | // * PUBLIC METHODS - METADATA GRID * // | |
| 1023 | + | // #region PUBLIC METHODS - METADATA GRID | |
| 1024 | + | /** | |
| 1025 | + | * Initializes the metadata grid menu and toggles its open/close state. | |
| 1026 | + | */ | |
| 1027 | + | initGridMenuState() { | |
| 1028 | + | if (grSet.layout !== 'default') { | |
| 1029 | + | const msg = grm.msg.getMessage('main', 'metadataGridLiveEdit'); | |
| 1030 | + | fb.ShowPopupMessage(msg, '元数据表格实时编辑'); | |
| 1031 | + | return; | |
| 1032 | + | } | |
| 1033 | + | ||
| 1034 | + | grm.ui.displayMetadataGridMenu = !grm.ui.displayMetadataGridMenu; | |
| 1035 | + | grm.ui.displayCustomThemeMenu = false; | |
| 1036 | + | ||
| 1037 | + | if (grm.ui.displayMetadataGridMenu) { | |
| 1038 | + | if (!grm.ui.displayDetails) { | |
| 1039 | + | grm.ui.displayDetails = true; | |
| 1040 | + | grm.ui.displayPlaylist = false; | |
| 1041 | + | grm.ui.displayLibrary = false; | |
| 1042 | + | grm.ui.displayBiography = false; | |
| 1043 | + | grm.ui.resizeArtwork(true); | |
| 1044 | + | } | |
| 1045 | + | ||
| 1046 | + | grm.gridMenu.initMetadataGridMenu(1); | |
| 1047 | + | } | |
| 1048 | + | ||
| 1049 | + | grm.button.initButtonState(); | |
| 1050 | + | window.Repaint(); | |
| 1051 | + | } | |
| 1052 | + | ||
| 1053 | + | /** | |
| 1054 | + | * Determines whether basic metadata should be displayed based on the grid column width. | |
| 1055 | + | * @param {string} gridColumnKey - The grid column key. | |
| 1056 | + | * @returns {boolean} True if basic metadata should be displayed, otherwise false. | |
| 1057 | + | */ | |
| 1058 | + | basicMetadataDisplay(gridColumnKey) { | |
| 1059 | + | const resolutions = [ | |
| 1060 | + | { displayRes: 'HD', maxW: 1250, maxH: 800 }, | |
| 1061 | + | { displayRes: 'QHD', maxW: 1350, maxH: 900 }, | |
| 1062 | + | { displayRes: '4K', maxW: 2350, maxH: 1550 } | |
| 1063 | + | ]; | |
| 1064 | + | ||
| 1065 | + | const basicMeta = ['年份', '唱片公司', '流派', '编解码', '声道', '来源', '数据', '播放次数', '评级']; | |
| 1066 | + | const smallRes = resolutions.some(res => grSet.displayRes === res.displayRes && (grm.ui.ww < res.maxW || grm.ui.wh < res.maxH)); | |
| 1067 | + | ||
| 1068 | + | return grSet.autoHideGridMetadata && grSet.layout === 'default' && smallRes && !basicMeta.includes(gridColumnKey); | |
| 1069 | + | } | |
| 1070 | + | ||
| 1071 | + | /** | |
| 1072 | + | * 获取编解码器字符串,如果编解码器是DTS,则返回'DCA'. | |
| 1073 | + | * @returns {string} 编解码器字符串或'DCA' 如果编解码器是 DTS. | |
| 1074 | + | */ | |
| 1075 | + | getCodecString() { | |
| 1076 | + | const codec = $('$lower($if2(%codec%,$ext(%path%)))'); | |
| 1077 | + | if (['dts', 'dca (dts coherent acoustics)'].includes(codec)) { | |
| 1078 | + | return 'DCA'; // 如果编解码是DTS,则仅显示DCA缩写 | |
| 1079 | + | } | |
| 1080 | + | return codec; | |
| 1081 | + | } | |
| 1082 | + | ||
| 1083 | + | /** | |
| 1084 | + | * Gets the channel string based on the provided channel type. | |
| 1085 | + | * @param {string} channelType - The type of the channel (e.g., 'mono', 'stereo'). | |
| 1086 | + | * @returns {string} The channel string or an empty string if the channel type is not found. | |
| 1087 | + | */ | |
| 1088 | + | getChannelString(channelType) { | |
| 1089 | + | const channelMapping = { | |
| 1090 | + | 'mono': { number: 1, string: '单声道' }, | |
| 1091 | + | '单声道': { number: 1, string: '单声道' }, | |
| 1092 | + | 'stereo': { number: 2, string: '立体声' }, | |
| 1093 | + | '立体声': { number: 2, string: '立体声' }, | |
| 1094 | + | '3ch': { number: 3, string: '中置' }, | |
| 1095 | + | '3 声道': { number: 3, string: '中置' }, | |
| 1096 | + | '4ch': { number: 4, string: '四声道' }, | |
| 1097 | + | '4 声道': { number: 4, string: '四声道' }, | |
| 1098 | + | '5ch': { number: 5, string: '环绕' }, | |
| 1099 | + | '5 声道': { number: 5, string: '环绕' }, | |
| 1100 | + | '6ch': { number: 6, string: '环绕' }, | |
| 1101 | + | '6 声道': { number: 6, string: '环绕' }, | |
| 1102 | + | '7ch': { number: 7, string: '环绕' }, | |
| 1103 | + | '7 声道': { number: 7, string: '环绕' }, | |
| 1104 | + | '8ch': { number: 8, string: '环绕' }, | |
| 1105 | + | '8 声道': { number: 8, string: '环绕' }, | |
| 1106 | + | '10ch': { number: 10, string: '环绕' }, | |
| 1107 | + | '10 声道':{ number: 10, string: '环绕' }, | |
| 1108 | + | '12ch': { number: 12, string: '环绕' }, | |
| 1109 | + | '12 声道':{ number: 12, string: '环绕' } | |
| 1110 | + | }; | |
| 1111 | + | ||
| 1112 | + | const channel = channelMapping[channelType]; | |
| 1113 | + | if (!channel) return ''; | |
| 1114 | + | ||
| 1115 | + | if (grSet.showGridChannelLogo_layout === 'textlogo') { | |
| 1116 | + | return channel.string; | |
| 1117 | + | } else if (grSet.showGridChannelLogo_layout === false) { | |
| 1118 | + | return `${channel.number} ${Unicode.MiddleDot} ${channel.string}`; | |
| 1119 | + | } else { | |
| 1120 | + | return ''; | |
| 1121 | + | } | |
| 1122 | + | } | |
| 1123 | + | ||
| 1124 | + | /** | |
| 1125 | + | * Gets the grid tooltip string based on the specified type. | |
| 1126 | + | * @param {string} type - The type of metadata ('artist', 'title', 'album'). | |
| 1127 | + | * @returns {string} The tooltip string. | |
| 1128 | + | */ | |
| 1129 | + | getGridTooltip(type) { | |
| 1130 | + | const tooltipType = { | |
| 1131 | + | artist: grStr.artist, | |
| 1132 | + | title: `${grStr.tracknum} ${grStr.title} ${grStr.composer}`, | |
| 1133 | + | album: `${grStr.album} ${grStr.composer}` | |
| 1134 | + | }; | |
| 1135 | + | return tooltipType[type]; | |
| 1136 | + | } | |
| 1137 | + | ||
| 1138 | + | /** | |
| 1139 | + | * Handles the grid tooltip. If a tooltip is ready, it displays and then clears it. | |
| 1140 | + | * @param {number} x - The x-coordinate. | |
| 1141 | + | * @param {number} y - The y-coordinate. | |
| 1142 | + | */ | |
| 1143 | + | handleGridTooltip(x, y) { | |
| 1144 | + | const artistTooltipRange = this.mouseInMetadataGrid(x, y, 'artist'); | |
| 1145 | + | const titleTooltipRange = this.mouseInMetadataGrid(x, y, 'title'); | |
| 1146 | + | const albumTooltipRange = this.mouseInMetadataGrid(x, y, 'album'); | |
| 1147 | + | ||
| 1148 | + | if (!artistTooltipRange && !titleTooltipRange && !albumTooltipRange) return; | |
| 1149 | + | ||
| 1150 | + | const showArtistToolTip = artistTooltipRange && grSet.showGridArtist_layout && ( | |
| 1151 | + | this.gridArtistWidth > this.gridContentWidth * 2 | |
| 1152 | + | || | |
| 1153 | + | this.gridArtistWrapLinesExceed | |
| 1154 | + | ); | |
| 1155 | + | ||
| 1156 | + | const showTitleToolTip = titleTooltipRange && grSet.showGridTitle_layout && ( | |
| 1157 | + | this.gridTitleWidth > this.gridContentWidth * 2 | |
| 1158 | + | || | |
| 1159 | + | this.gridTitleWrapLinesExceed | |
| 1160 | + | ); | |
| 1161 | + | ||
| 1162 | + | const showAlbumToolTip = albumTooltipRange && ( | |
| 1163 | + | !grSet.showGridArtist_layout && !grSet.showGridTitle_layout && (this.gridAlbumWidth > this.gridContentWidth * 3) | |
| 1164 | + | || | |
| 1165 | + | (grSet.showGridArtist_layout || grSet.showGridTitle_layout) && (this.gridAlbumWidth > this.gridContentWidth * 2) | |
| 1166 | + | || | |
| 1167 | + | this.gridAlbumWrapLinesExceed | |
| 1168 | + | ); | |
| 1169 | + | ||
| 1170 | + | const tooltip = | |
| 1171 | + | showArtistToolTip ? this.getGridTooltip('artist') : | |
| 1172 | + | showTitleToolTip ? this.getGridTooltip('title') : | |
| 1173 | + | showAlbumToolTip ? this.getGridTooltip('album') : ''; | |
| 1174 | + | ||
| 1175 | + | if (tooltip.length) { // * Display tooltip | |
| 1176 | + | const offset = SCALE(30); | |
| 1177 | + | this.gridTooltipText = tooltip; | |
| 1178 | + | grm.ttip.showDelayed(this.gridTooltipText); | |
| 1179 | + | grm.ui.repaintStyledTooltips(grm.ui.styledToolTipX - offset * 2, grm.ui.styledToolTipY - offset, grm.ui.styledToolTipW + offset * 4, grm.ui.styledToolTipH + offset * 2); | |
| 1180 | + | } else { // * Clear tooltip | |
| 1181 | + | this.gridTooltipText = ''; | |
| 1182 | + | grm.ttip.stop(); | |
| 1183 | + | window.Repaint(); | |
| 1184 | + | } | |
| 1185 | + | } | |
| 1186 | + | ||
| 1187 | + | /** | |
| 1188 | + | * Loads the codec logo of the now playing track, displayed in the metadata grid in Details. | |
| 1189 | + | * @param {FbMetadbHandle} metadb - The metadb of the track. | |
| 1190 | + | */ | |
| 1191 | + | loadGridCodecLogo(metadb = grm.ui.initMetadb()) { | |
| 1192 | + | let codec = $('$lower($if2(%codec%,$ext(%path%)))', metadb); | |
| 1193 | + | let format = $('$lower($ext(%path%))', metadb); | |
| 1194 | + | ||
| 1195 | + | // Foobar bug showing wrong metadata when DTS is in wav file format | |
| 1196 | + | if (codec === 'pcm' && (format === 'cue' || format === 'wav')) { | |
| 1197 | + | codec = $('$lower($if2(%codec%,$ext(%path%)))'); | |
| 1198 | + | format = $('$lower($ext(%path%))'); | |
| 1199 | + | } | |
| 1200 | + | ||
| 1201 | + | const codecFormat = { | |
| 1202 | + | 'aac': 'aac', 'aac acm codec': 'aac', 'mp4': 'aac', | |
| 1203 | + | 'ac3': 'ac3', 'atsc a/52': 'ac3', 'e-ac3': 'ac3', 'atsc a/52a (ac-3)': 'ac3', | |
| 1204 | + | 'aiff': 'pcm-aiff', | |
| 1205 | + | 'alac': 'alac', | |
| 1206 | + | 'alaw': 'alaw', 'ccitt a-law': 'alaw', | |
| 1207 | + | 'amr': 'amr', | |
| 1208 | + | 'ape': 'ape', 'monkey\'s audio': 'ape', | |
| 1209 | + | 'caf': 'caf', | |
| 1210 | + | 'dsd': format === 'iso' ? 'dsd-sacd' : 'dsd', | |
| 1211 | + | 'dst': 'dsd-sacd', | |
| 1212 | + | 'dts': 'dts', 'dca (dts coherent acoustics)': 'dts', | |
| 1213 | + | 'dxd': format === 'iso' ? 'dsd-sacd' : 'dxd', | |
| 1214 | + | 'flac': 'flac', | |
| 1215 | + | 'gsm': 'gsm', 'gsm 6.10': 'gsm', | |
| 1216 | + | 'imaadpcm': 'imaadpcm', 'ima adpcm': 'imaadpcm', | |
| 1217 | + | 'la': 'la', | |
| 1218 | + | 'mid': 'mid', | |
| 1219 | + | 'mlp': 'mlp', | |
| 1220 | + | 'mod': 'mod', | |
| 1221 | + | 'mp2': 'mp2', | |
| 1222 | + | 'mp3': 'mp3', 'mpeg layer-3': 'mp3', | |
| 1223 | + | 'mpc': 'musepack', 'musepack': 'musepack', | |
| 1224 | + | 'msadpcm': 'msadpcm', 'microsoft adpcm': 'msadpcm', | |
| 1225 | + | 'ofr': 'ofr', 'optimfrog': 'ofr', | |
| 1226 | + | 'ogg': 'ogg', 'vorbis': 'ogg', | |
| 1227 | + | 'opus': 'opus', | |
| 1228 | + | 'pcm': format === 'aiff' ? 'pcm-aiff' : ['w64', 'wav'].includes(format) ? 'pcm-wav' : 'pcm', | |
| 1229 | + | 'qoa': 'qoa', | |
| 1230 | + | 'shn': 'shn', 'shorten': 'shn', | |
| 1231 | + | 'spx': 'spx', 'speex': 'spx', | |
| 1232 | + | 'tak': 'tak', | |
| 1233 | + | 'tta': 'tta', 'true audio': 'tta', | |
| 1234 | + | 'ulaw': 'ulaw', 'ccitt u-law': 'ulaw', | |
| 1235 | + | 'usac': 'usac', | |
| 1236 | + | 'wav': 'pcm-wav', | |
| 1237 | + | 'w64': 'pcm-wav', | |
| 1238 | + | 'wma': 'wma', | |
| 1239 | + | 'wv': 'wavpack', 'wavpack': 'wavpack' | |
| 1240 | + | }; | |
| 1241 | + | ||
| 1242 | + | let logoName; | |
| 1243 | + | const HDCD = $('%__hdcd%') === 'yes'; | |
| 1244 | + | const codecName = codecFormat[codec] || codecFormat[format]; | |
| 1245 | + | ||
| 1246 | + | if (codec.startsWith('dsd')) { | |
| 1247 | + | logoName = codecFormat.dsd; | |
| 1248 | + | } else if (codec.startsWith('dxd')) { | |
| 1249 | + | logoName = codecFormat.dxd; | |
| 1250 | + | } else if (codec.startsWith('dst')) { | |
| 1251 | + | logoName = codecFormat.dst; | |
| 1252 | + | } else { | |
| 1253 | + | logoName = HDCD && codecName === 'pcm-wav' ? 'pcm-hdcd' : HDCD ? `${codecName}-hdcd` : codecName; | |
| 1254 | + | } | |
| 1255 | + | ||
| 1256 | + | const bw = grCol.lightBgDetails ? 'black' : 'white'; | |
| 1257 | + | const path = `${grPath.images}codec\\${logoName}-${bw}.png`; | |
| 1258 | + | ||
| 1259 | + | this.gridCodecLogo = gdi.Image(path); | |
| 1260 | + | } | |
| 1261 | + | ||
| 1262 | + | /** | |
| 1263 | + | * Loads the channel logo of the now playing track, displayed in the metadata grid in Details. | |
| 1264 | + | * @param {FbMetadbHandle} metadb - The metadb of the track. | |
| 1265 | + | */ | |
| 1266 | + | loadGridChannelLogo(metadb = grm.ui.initMetadb()) { | |
| 1267 | + | const codec = $('$lower($if2(%codec%,$ext(%path%)))', metadb); | |
| 1268 | + | const format = $('$lower($ext(%path%))', metadb); | |
| 1269 | + | ||
| 1270 | + | // Foobar bug showing wrong metadata when DTS is in wav file format | |
| 1271 | + | const channels = codec === 'pcm' && (format === 'cue' || format === 'wav') ? $('%channels%') : $('%channels%', metadb); | |
| 1272 | + | ||
| 1273 | + | const type = | |
| 1274 | + | (grSet.layout === 'default' && grSet.showGridChannelLogo_default === 'textlogo' || | |
| 1275 | + | grSet.layout === 'artwork' && grSet.showGridChannelLogo_artwork === 'textlogo') ? '_text' : ''; | |
| 1276 | + | ||
| 1277 | + | const bw = grCol.lightBgDetails ? 'black' : 'white'; | |
| 1278 | + | ||
| 1279 | + | const channelFormat = { | |
| 1280 | + | 'mono': '10_mono', | |
| 1281 | + | '单声道': '10_mono', | |
| 1282 | + | 'stereo': '20_stereo', | |
| 1283 | + | '立体声': '20_stereo', | |
| 1284 | + | '3ch': '30_center', | |
| 1285 | + | '3 声道': '30_center', | |
| 1286 | + | '4ch': '40_quad', | |
| 1287 | + | '4 声道': '40_quad', | |
| 1288 | + | '5ch': '50_surround', | |
| 1289 | + | '5 声道': '50_surround', | |
| 1290 | + | '6ch': '51_surround', | |
| 1291 | + | '6 声道': '51_surround', | |
| 1292 | + | '7ch': '61_surround', | |
| 1293 | + | '7 声道': '61_surround', | |
| 1294 | + | '8ch': '71_surround', | |
| 1295 | + | '8 声道': '71_surround', | |
| 1296 | + | '10ch': '91_surround', | |
| 1297 | + | '10 声道':'91_surround', | |
| 1298 | + | '12ch': '111_surround', | |
| 1299 | + | '12 声道':'111_surround' | |
| 1300 | + | }; | |
| 1301 | + | ||
| 1302 | + | const channelName = channelFormat[channels]; | |
| 1303 | + | const channelLogoPath = (channelName) => `${grPath.images}channels\\${channelName}${type}-${bw}.png`; | |
| 1304 | + | if (channelName) this.gridChannelLogo = gdi.Image(channelLogoPath(channelName)); | |
| 1305 | + | } | |
| 1306 | + | ||
| 1307 | + | /** | |
| 1308 | + | * Loads the release country flags, displayed in the metadata grid in Details. | |
| 1309 | + | * @param {FbMetadbHandle} metadb - The metadb of the track. | |
| 1310 | + | */ | |
| 1311 | + | loadGridReleaseCountryFlag(metadb = undefined) { | |
| 1312 | + | if (!grSet.showGridReleaseFlags_layout) return; | |
| 1313 | + | this.gridReleaseFlagImg = grm.ui.loadFlagImage($(grTF.releaseCountry, metadb)); | |
| 1314 | + | } | |
| 1315 | + | ||
| 1316 | + | /** | |
| 1317 | + | * Updates the metadata grid in Details, reuses last value for last played unless provided one. | |
| 1318 | + | * @param {string} currentLastPlayed - The current value of the "Last Played" metadata field. | |
| 1319 | + | * @param {string} currentPlayingPlaylist - The current active playlist that is being played from. | |
| 1320 | + | * @param {FbMetadbHandle} metadb - The metadb of the track. | |
| 1321 | + | * @returns {Array|null} The updated metadata grid, which is an array of objects with properties `label`, `val` and `age`. | |
| 1322 | + | */ | |
| 1323 | + | updateGrid(currentLastPlayed, currentPlayingPlaylist, metadb = undefined) { | |
| 1324 | + | if (!grCfg.metadataGrid) return null; | |
| 1325 | + | ||
| 1326 | + | currentLastPlayed = (grStr && grStr.grid ? grStr.grid.find(value => value.label === '最近播放') || {} : {}).val; | |
| 1327 | + | grStr.grid = []; | |
| 1328 | + | ||
| 1329 | + | for (const key of grCfg.metadataGrid) { | |
| 1330 | + | let val = $(key.val, metadb); | |
| 1331 | + | if (val && key.label) { | |
| 1332 | + | if (key.age) { | |
| 1333 | + | val = $(`$date(${val})`, metadb); // Never show time | |
| 1334 | + | const age = CalcAgeDateString(val); | |
| 1335 | + | if (age) val += ` (${age})`; | |
| 1336 | + | } | |
| 1337 | + | grStr.grid.push({ | |
| 1338 | + | age: key.age, | |
| 1339 | + | label: key.label, | |
| 1340 | + | val | |
| 1341 | + | }); | |
| 1342 | + | } | |
| 1343 | + | } | |
| 1344 | + | if (typeof currentLastPlayed !== 'undefined') { | |
| 1345 | + | const lp = grStr.grid.find(value => value.label === '最近播放'); | |
| 1346 | + | if (lp) { | |
| 1347 | + | lp.val = $Date(currentLastPlayed); | |
| 1348 | + | if (CalcAgeDateString(lp.val)) { | |
| 1349 | + | lp.val += ` (${CalcAgeDateString(lp.val)})`; | |
| 1350 | + | } | |
| 1351 | + | } | |
| 1352 | + | } | |
| 1353 | + | if (typeof currentPlayingPlaylist !== 'undefined') { | |
| 1354 | + | const pl = grStr.grid.find(value => value.label === '播放列表'); | |
| 1355 | + | if (pl) { | |
| 1356 | + | pl.val = currentPlayingPlaylist; | |
| 1357 | + | } | |
| 1358 | + | } | |
| 1359 | + | ||
| 1360 | + | return grStr.grid; | |
| 1361 | + | } | |
| 1362 | + | ||
| 1363 | + | /** | |
| 1364 | + | * Updates the metadata grid codec and channel logo in Details. | |
| 1365 | + | * This method is primarily used to refresh the colors of the logos. | |
| 1366 | + | */ | |
| 1367 | + | updateGridLogos() { | |
| 1368 | + | this.clearCache('codecLogo'); | |
| 1369 | + | this.clearCache('channelLogo'); | |
| 1370 | + | } | |
| 1371 | + | ||
| 1372 | + | /** | |
| 1373 | + | * Updates the metadata grid positions in Details. | |
| 1374 | + | * This method is primarily used to refresh the coordinates for mouseInMetadataGrid. | |
| 1375 | + | */ | |
| 1376 | + | updateGridPos() { | |
| 1377 | + | this.gridTop = 0; | |
| 1378 | + | this.gridArtistTop = 0; | |
| 1379 | + | this.gridArtistBottom = 0; | |
| 1380 | + | this.gridTitleTop = 0; | |
| 1381 | + | this.gridTitleBottom = 0; | |
| 1382 | + | this.gridAlbumTop = 0; | |
| 1383 | + | this.gridAlbumBottom = 0; | |
| 1384 | + | } | |
| 1385 | + | // #endregion | |
| 1386 | + | ||
| 1387 | + | // * PUBLIC METHODS - METADATA GRID TIMELINE * // | |
| 1388 | + | // #region PUBLIC METHODS - METADATA GRID TIMELINE | |
| 1389 | + | /** | |
| 1390 | + | * Draws the timeline above the metadata grid in Details. | |
| 1391 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 1392 | + | */ | |
| 1393 | + | drawGridTimeline(gr) { | |
| 1394 | + | gr.SetSmoothingMode(SmoothingMode.None); // Disable smoothing | |
| 1395 | + | gr.FillSolidRect(this.gridMarginLeft, this.timelineY, this.timelineDrawWidth + this.timelineExtraLeftSpace + this.timelineLineWidth, this.timelineH, grCol.timelineAdded); | |
| 1396 | + | ||
| 1397 | + | if (grSet.theme.startsWith('custom')) { | |
| 1398 | + | gr.DrawRect(this.timelineX - 2, this.timelineY - 2, this.timelineW + 3, this.timelineH + 3, 1, grCol.timelineFrame); | |
| 1399 | + | } | |
| 1400 | + | ||
| 1401 | + | if (this.timelineFirstPlayedPercent >= 0 && this.timelineLastPlayedPercent >= 0) { | |
| 1402 | + | const x1 = Math.floor(this.timelineDrawWidth * this.timelineFirstPlayedPercent) + this.timelineExtraLeftSpace; | |
| 1403 | + | const x2 = Math.floor(this.timelineDrawWidth * this.timelineLastPlayedPercent) + this.timelineExtraLeftSpace; | |
| 1404 | + | gr.FillSolidRect(x1 + this.gridMarginLeft, this.timelineY, this.timelineDrawWidth - x1 + this.timelineExtraLeftSpace, this.timelineH, grCol.timelinePlayed); | |
| 1405 | + | gr.FillSolidRect(x2 + this.gridMarginLeft, this.timelineY, this.timelineDrawWidth - x2 + this.timelineExtraLeftSpace + this.timelineLineWidth, this.timelineH, grCol.timelineUnplayed); | |
| 1406 | + | } | |
| 1407 | + | ||
| 1408 | + | for (let i = 0; i < this.timelinePlayedTimesPercents.length; i++) { | |
| 1409 | + | const x = Math.floor(this.timelineDrawWidth * this.timelinePlayedTimesPercents[i]) + this.gridMarginLeft + this.timelineExtraLeftSpace; | |
| 1410 | + | if (!Number.isNaN(x) && x <= this.timelineW + this.gridMarginLeft * 2) { | |
| 1411 | + | const linePos = Math.max(this.gridMarginLeft, Math.min(x, x)); | |
| 1412 | + | gr.DrawLine(linePos, this.timelineY, linePos, this.timelineY + this.timelineH, this.timelineLineWidth, this.timelinePlayCol); | |
| 1413 | + | } else { | |
| 1414 | + | // console.log('Played Times Error! ratio: ' + this.playedTimesPercents[i], 'x: ' + x); | |
| 1415 | + | } | |
| 1416 | + | } | |
| 1417 | + | ||
| 1418 | + | gr.SetSmoothingMode(SmoothingMode.AntiAlias); | |
| 1419 | + | } | |
| 1420 | + | ||
| 1421 | + | /** | |
| 1422 | + | * Handles the grid timeline tooltip. If a tooltip is ready, it displays and then clears it. | |
| 1423 | + | * @param {number} x - The x-coordinate. | |
| 1424 | + | * @param {number} y - The y-coordinate. | |
| 1425 | + | */ | |
| 1426 | + | handleGridTimelineTooltip(x, y) { | |
| 1427 | + | if (!this.mouseInMetadataGrid(x, y, 'timeline') || !grSet.showGridTimeline_layout || this.timelinePlayedTimesPercents.length === 0) { | |
| 1428 | + | return; | |
| 1429 | + | } | |
| 1430 | + | ||
| 1431 | + | let tooltip = ''; | |
| 1432 | + | const percent = ToFixed((x + this.timelineX - this.gridMarginLeft * 2 - this.timelineExtraLeftSpace) / this.timelineDrawWidth, 3); | |
| 1433 | + | const timezoneOffset = UpdateTimezoneOffset(); | |
| 1434 | + | ||
| 1435 | + | for (let i = 0; i < this.timelinePlayedTimesPercents.length; i++) { | |
| 1436 | + | if (Math.abs(percent - this.timelinePlayedTimesPercents[i]) <= this.timelineLeeway) { | |
| 1437 | + | const date = new Date(this.timelinePlayedTimes[i]); | |
| 1438 | + | tooltip += tooltip.length ? '\n' : ''; | |
| 1439 | + | tooltip += date.toLocaleString(); | |
| 1440 | + | } | |
| 1441 | + | else if (percent < this.timelinePlayedTimesPercents[i]) { | |
| 1442 | + | if (!tooltip.length) { | |
| 1443 | + | const added = i === 0 ? DateDiff($Date('[%added%]'), this.timelinePlayedTimes[0], timezoneOffset) : DateDiff(new Date(this.timelinePlayedTimes[i - 1]).toISOString(), this.timelinePlayedTimes[i], timezoneOffset); | |
| 1444 | + | tooltip = added ? (i === 0 ? `添加 ${added} 后首次播放` : `已 ${added} 无播放`) : ''; | |
| 1445 | + | } | |
| 1446 | + | break; | |
| 1447 | + | } | |
| 1448 | + | } | |
| 1449 | + | ||
| 1450 | + | if (tooltip.length) { | |
| 1451 | + | this.gridTimelineTooltipText = tooltip; | |
| 1452 | + | grm.ttip.showImmediate(tooltip); | |
| 1453 | + | window.RepaintRect(this.timelineX, this.timelineY, this.timelineW, this.timelineH); | |
| 1454 | + | } else { | |
| 1455 | + | this.gridTimelineTooltipText = ''; | |
| 1456 | + | grm.ttip.stop(); | |
| 1457 | + | window.Repaint(); | |
| 1458 | + | } | |
| 1459 | + | } | |
| 1460 | + | ||
| 1461 | + | /** | |
| 1462 | + | * Sets the width and position of the timeline. | |
| 1463 | + | * @param {number} x - The x-coordinate. | |
| 1464 | + | * @param {number} y - The y-coordinate. | |
| 1465 | + | * @param {number} width - The width of the timeline. | |
| 1466 | + | * @param {number} height - The height of the timeline. | |
| 1467 | + | */ | |
| 1468 | + | setGridTimelineSize(x, y, width, height) { | |
| 1469 | + | if (this.timelineX === x && this.timelineY === y && this.timelineW === width) { | |
| 1470 | + | return; | |
| 1471 | + | } | |
| 1472 | + | ||
| 1473 | + | this.timelineX = x; | |
| 1474 | + | this.timelineY = y; | |
| 1475 | + | this.timelineW = width; | |
| 1476 | + | this.timelineH = height; | |
| 1477 | + | ||
| 1478 | + | this.timelineLineWidth = HD_4K(2, 3); | |
| 1479 | + | this.timelineExtraLeftSpace = SCALE(3); // Add a little space to the left so songs that were played a long time ago show more in the "added" stage | |
| 1480 | + | this.timelineDrawWidth = Math.floor(this.timelineW - this.timelineExtraLeftSpace - 1 - this.timelineLineWidth / 2); | |
| 1481 | + | this.timelineLeeway = (1 / this.timelineDrawWidth) * (this.timelineLineWidth + SCALE(2)) / 2; | |
| 1482 | + | } | |
| 1483 | + | ||
| 1484 | + | /** | |
| 1485 | + | * Sets the first and last played percentages, as well as the played time ratios and values. | |
| 1486 | + | * @param {number} firstPlayed - The percentage of the total play time that represents the first time the item was played. | |
| 1487 | + | * @param {number} lastPlayed - The percentage of the total play time that represents the last time the item was played. | |
| 1488 | + | * @param {number} playedTimeRatios - The percentage of time played for each playedTimesValues. | |
| 1489 | + | * @param {number} playedTimesValues - Contains the actual played times for each interval. | |
| 1490 | + | * For example, if the intervals are divided into 5 parts, playedTimesValues would be an | |
| 1491 | + | * array of 5 numbers representing the played times for each interval. | |
| 1492 | + | */ | |
| 1493 | + | setGridTimelinePlayTimes(firstPlayed, lastPlayed, playedTimeRatios, playedTimesValues) { | |
| 1494 | + | this.timelineFirstPlayedPercent = firstPlayed; | |
| 1495 | + | this.timelineLastPlayedPercent = lastPlayed; | |
| 1496 | + | this.timelinePlayedTimesPercents = playedTimeRatios; | |
| 1497 | + | this.timelinePlayedTimes = playedTimesValues; | |
| 1498 | + | } | |
| 1499 | + | ||
| 1500 | + | /** | |
| 1501 | + | * Sets date ratios based on various time-related properties of a music track. | |
| 1502 | + | * @param {boolean} dontUpdateLastPlayed - Whether the last played date should be updated or not. | |
| 1503 | + | * @param {string} currentLastPlayed - The current value of the last played time. | |
| 1504 | + | * @param {FbMetadbHandle} metadb - The metadb of the track. | |
| 1505 | + | */ | |
| 1506 | + | setGridTimelineDateRatios(dontUpdateLastPlayed = false, currentLastPlayed, metadb = undefined) { | |
| 1507 | + | const newDate = new Date(); | |
| 1508 | + | const timezoneOffset = UpdateTimezoneOffset(); | |
| 1509 | + | ||
| 1510 | + | let ratio; | |
| 1511 | + | let lfmPlayedTimesJsonLast = ''; | |
| 1512 | + | let playedTimesJsonLast = ''; | |
| 1513 | + | let playedTimesRatios = []; | |
| 1514 | + | let lfmPlayedTimes = []; | |
| 1515 | + | let playedTimes = []; | |
| 1516 | + | ||
| 1517 | + | let added = ToTime($('$if2(%added_enhanced%,%added%)', metadb), timezoneOffset); | |
| 1518 | + | let lastPlayed = ToTime($('$if2(%last_played_enhanced%,%last_played%)', metadb), timezoneOffset); | |
| 1519 | + | const firstPlayed = ToTime($('$if2(%first_played_enhanced%,%first_played%)', metadb), timezoneOffset); | |
| 1520 | + | const today = DateToYMD(newDate); | |
| 1521 | + | ||
| 1522 | + | if (dontUpdateLastPlayed && $Date(lastPlayed) === today) { | |
| 1523 | + | lastPlayed = ToTime(currentLastPlayed, timezoneOffset); | |
| 1524 | + | } | |
| 1525 | + | ||
| 1526 | + | if (Component.EnhancedPlaycount) { | |
| 1527 | + | const playedTimesJson = $('[%played_times_js%]', metadb); | |
| 1528 | + | const lastfmJson = $('[%lastfm_played_times_js%]', metadb); | |
| 1529 | + | const log = ''; // ! Don't need this crap to flood the console // playedTimesJson === playedTimesJsonLast && lastfmJson === lfmPlayedTimesJsonLast ? false : grCfg.settings.showDebugLog; | |
| 1530 | + | lfmPlayedTimesJsonLast = lastfmJson; | |
| 1531 | + | playedTimesJsonLast = playedTimesJson; | |
| 1532 | + | lfmPlayedTimes = ParseJson(lastfmJson, 'lastfm: ', log); | |
| 1533 | + | playedTimes = ParseJson(playedTimesJson, 'foobar: ', log); | |
| 1534 | + | } | |
| 1535 | + | else { | |
| 1536 | + | playedTimes.push(firstPlayed); | |
| 1537 | + | playedTimes.push(lastPlayed); | |
| 1538 | + | } | |
| 1539 | + | ||
| 1540 | + | if (firstPlayed) { | |
| 1541 | + | if (!added) { | |
| 1542 | + | added = firstPlayed; | |
| 1543 | + | } | |
| 1544 | + | const age = CalcAge(added); | |
| 1545 | + | ||
| 1546 | + | this.timelineFirstPlayedRatio = CalcAgeRatio(firstPlayed, age); | |
| 1547 | + | this.timelineLastPlayedRatio = CalcAgeRatio(lastPlayed, age); | |
| 1548 | + | if (this.timelineLastPlayedRatio < this.timelineFirstPlayedRatio) { | |
| 1549 | + | // Due to daylight savings time, if there's a single play before the time changed lastPlayed could be < firstPlayed | |
| 1550 | + | this.timelineLastPlayedRatio = this.timelineFirstPlayedRatio; | |
| 1551 | + | } | |
| 1552 | + | ||
| 1553 | + | if (playedTimes.length) { | |
| 1554 | + | for (let i = 0; i < playedTimes.length; i++) { | |
| 1555 | + | ratio = CalcAgeRatio(playedTimes[i], age); | |
| 1556 | + | playedTimesRatios.push(ratio); | |
| 1557 | + | } | |
| 1558 | + | } else { | |
| 1559 | + | playedTimesRatios = [this.timelineFirstPlayedRatio, this.timelineLastPlayedRatio]; | |
| 1560 | + | playedTimes = [firstPlayed, lastPlayed]; | |
| 1561 | + | } | |
| 1562 | + | ||
| 1563 | + | let j = 0; | |
| 1564 | + | const tempPlayedTimesRatios = playedTimesRatios.slice(); | |
| 1565 | + | tempPlayedTimesRatios.push(1.0001); // Pick up every last.fm time after lastPlayed fb knows about | |
| 1566 | + | for (let i = 0; i < tempPlayedTimesRatios.length; i++) { | |
| 1567 | + | while (j < lfmPlayedTimes.length && (ratio = CalcAgeRatio(lfmPlayedTimes[j], age)) < tempPlayedTimesRatios[i]) { | |
| 1568 | + | playedTimesRatios.push(ratio); | |
| 1569 | + | playedTimes.push(lfmPlayedTimes[j]); | |
| 1570 | + | j++; | |
| 1571 | + | } | |
| 1572 | + | if (ratio === tempPlayedTimesRatios[i]) { // Skip one instance | |
| 1573 | + | // console.log('skipped -->', ratio); | |
| 1574 | + | j++; | |
| 1575 | + | } | |
| 1576 | + | } | |
| 1577 | + | playedTimesRatios.sort((a, b) => a - b); | |
| 1578 | + | playedTimes.sort((a, b) => a - b); | |
| 1579 | + | ||
| 1580 | + | this.timelineFirstPlayedRatio = playedTimesRatios[0]; | |
| 1581 | + | this.timelineLastPlayedRatio = playedTimesRatios[Math.max(0, playedTimesRatios.length - (dontUpdateLastPlayed ? 2 : 1))]; | |
| 1582 | + | } | |
| 1583 | + | else { | |
| 1584 | + | this.timelineFirstPlayedRatio = 0.33; | |
| 1585 | + | this.timelineLastPlayedRatio = 0.66; | |
| 1586 | + | } | |
| 1587 | + | this.setGridTimelinePlayTimes(this.timelineFirstPlayedRatio, this.timelineLastPlayedRatio, playedTimesRatios, playedTimes); | |
| 1588 | + | } | |
| 1589 | + | ||
| 1590 | + | /** | |
| 1591 | + | * Updates the timeline by setting the sizes, colors, and last played dates. | |
| 1592 | + | * @param {boolean} updateLastPlayed - Whether to update the last played date. | |
| 1593 | + | * @param {FbMetadbHandle} metadb - The metadb of the track. | |
| 1594 | + | */ | |
| 1595 | + | updateGridTimeline(updateLastPlayed, metadb) { | |
| 1596 | + | this.setGridTimelineSize(this.gridMarginLeft, this.gridTop + Math.floor(this.gridLineSpacing * 0.33), grm.ui.albumArtSize.x - this.gridMarginLeft * 2, this.timelineH); | |
| 1597 | + | ||
| 1598 | + | if (!updateLastPlayed) return; | |
| 1599 | + | ||
| 1600 | + | const lastPlayed = $(grTF.last_played, metadb); | |
| 1601 | + | this.setGridTimelineDateRatios($Date(grm.ui.currentLastPlayed) !== $Date(lastPlayed), grm.ui.currentLastPlayed, metadb); | |
| 1602 | + | ||
| 1603 | + | if (lastPlayed.length) { | |
| 1604 | + | const today = DateToYMD(new Date()); | |
| 1605 | + | if (!grm.ui.currentLastPlayed.length || $Date(lastPlayed) !== today) { | |
| 1606 | + | grm.ui.currentLastPlayed = lastPlayed; | |
| 1607 | + | } | |
| 1608 | + | } | |
| 1609 | + | } | |
| 1610 | + | // #endregion | |
| 1611 | + | ||
| 1612 | + | // * PUBLIC METHODS - DISC ART * // | |
| 1613 | + | // #region PUBLIC METHODS - DISC ART | |
| 1614 | + | /** | |
| 1615 | + | * Creates and masks an image to the disc art. | |
| 1616 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 1617 | + | * @param {number} x - The X-coordinate of the disc area. | |
| 1618 | + | * @param {number} y - The Y-coordinate of the disc area. | |
| 1619 | + | * @param {number} w - The width of the mask. | |
| 1620 | + | * @param {number} h - The height of the mask. | |
| 1621 | + | * @param {number} srcX - The X-coordinate of the source image. | |
| 1622 | + | * @param {number} srcY - The Y-coordinate of the source image. | |
| 1623 | + | * @param {number} srcW - The width of the source image. | |
| 1624 | + | * @param {number} srcH - The height of the source image. | |
| 1625 | + | * @param {number} [angle] - The angle of the mask in degrees. Default 0. | |
| 1626 | + | * @param {number} [alpha] - The alpha of the mask. Values 0-255. | |
| 1627 | + | * @returns {GdiGraphics} The rounded masked image. | |
| 1628 | + | */ | |
| 1629 | + | createDiscArtAlbumArtMask(gr, x, y, w, h, srcX, srcY, srcW, srcH, angle, alpha) { | |
| 1630 | + | if (w < 1 || h < 1) return null; | |
| 1631 | + | ||
| 1632 | + | // * First draw album art in the background | |
| 1633 | + | gr.DrawImage(grm.ui.albumArtScaled, x, y, w, h, 0, 0, w, h, 0, alpha); | |
| 1634 | + | ||
| 1635 | + | // * Mask | |
| 1636 | + | const maskImg = gdi.CreateImage(w, h); | |
| 1637 | + | let g = maskImg.GetGraphics(); | |
| 1638 | + | g.FillEllipse(this.discArtSize.x - grm.ui.albumArtSize.x + this.discArtShadow - SCALE(4), this.discArtSize.y - grm.ui.albumArtSize.y + SCALE(2), | |
| 1639 | + | this.discArtSize.w - this.discArtShadow + SCALE(4), this.discArtSize.h - this.discArtShadow + SCALE(2), 0xffffffff); | |
| 1640 | + | maskImg.ReleaseGraphics(g); | |
| 1641 | + | ||
| 1642 | + | // * Album art | |
| 1643 | + | const albumArtImg = gdi.CreateImage(w, h); | |
| 1644 | + | g = albumArtImg.GetGraphics(); | |
| 1645 | + | g.DrawImage(grm.ui.albumArtScaled, 0, 0, w, h, 0, 0, grm.ui.albumArtScaled.Width, grm.ui.albumArtScaled.Height); | |
| 1646 | + | albumArtImg.ReleaseGraphics(g); | |
| 1647 | + | ||
| 1648 | + | const mask = maskImg.Resize(w, h); | |
| 1649 | + | albumArtImg.ApplyMask(mask); | |
| 1650 | + | ||
| 1651 | + | return gr.DrawImage(albumArtImg, x, y, w, h, 0, 0, w, h, 0, 255); | |
| 1652 | + | } | |
| 1653 | + | ||
| 1654 | + | /** | |
| 1655 | + | * Creates the album cover mask for the disc art stub. | |
| 1656 | + | * @param {GdiBitmap} img - The image to apply the mask to. | |
| 1657 | + | * @param {number} w - The width of the mask. | |
| 1658 | + | * @param {number} h - The height of the mask. | |
| 1659 | + | */ | |
| 1660 | + | createDiscArtCoverMask(img, w, h) { | |
| 1661 | + | const { w: discArtW, h: discArtH } = this.discArtSize; | |
| 1662 | + | const lineW = SCALE(25); | |
| 1663 | + | ||
| 1664 | + | const outerRingX = lineW * 0.5; | |
| 1665 | + | const outerRingY = lineW * 0.5; | |
| 1666 | + | const outerRingW = discArtW - lineW; | |
| 1667 | + | const outerRingH = discArtH - lineW; | |
| 1668 | + | ||
| 1669 | + | const innerRingSize = discArtH * 0.666 + lineW * 0.5; | |
| 1670 | + | const innerCenterX = discArtW * 0.5; | |
| 1671 | + | const innerCenterY = discArtH * 0.5; | |
| 1672 | + | const innerRadiusX = discArtW * 0.5 - innerRingSize * 0.5; | |
| 1673 | + | const innerRadiusY = discArtH * 0.5 - innerRingSize * 0.5; | |
| 1674 | + | ||
| 1675 | + | const innerRingX = innerCenterX - innerRadiusX; | |
| 1676 | + | const innerRingY = innerCenterY - innerRadiusY; | |
| 1677 | + | const innerRingW = innerRadiusX * 2; | |
| 1678 | + | const innerRingH = innerRadiusY * 2; | |
| 1679 | + | ||
| 1680 | + | const mask = GDI(discArtW, discArtH, true, g => { | |
| 1681 | + | g.SetSmoothingMode(SmoothingMode.AntiAlias); | |
| 1682 | + | g.FillSolidRect(0, 0, discArtW, discArtH, RGB(255, 255, 255)); | |
| 1683 | + | g.FillEllipse(outerRingX, outerRingY, outerRingW, outerRingH, RGB(0, 0, 0)); // Outer ring | |
| 1684 | + | g.FillEllipse(innerRingX, innerRingY, innerRingW, innerRingH, RGB(255, 255, 255)); // Inner ring | |
| 1685 | + | }); | |
| 1686 | + | ||
| 1687 | + | img.ApplyMask(mask.Resize(w, h)); | |
| 1688 | + | } | |
| 1689 | + | ||
| 1690 | + | /** | |
| 1691 | + | * Combines disc art with album cover art if conditions are met. | |
| 1692 | + | * @param {boolean} applyMask - Whether to apply the disc art cover mask or not. | |
| 1693 | + | * @returns {GdiBitmap} The combined image. | |
| 1694 | + | */ | |
| 1695 | + | combineDiscArtWithCover(applyMask) { | |
| 1696 | + | if (['cdAlbumCover', 'vinylAlbumCover'].includes(grSet.discArtStub) && | |
| 1697 | + | (!this.discArtFound && (!grSet.noDiscArtStub || grSet.showDiscArtStub)) && | |
| 1698 | + | this.discArtCover && this.discArtCover.Width > 0 && this.discArtCover.Height > 0) { | |
| 1699 | + | if (applyMask) { | |
| 1700 | + | this.createDiscArtCoverMask(this.discArtCover, this.discArtCover.Width, this.discArtCover.Height); | |
| 1701 | + | } | |
| 1702 | + | return CombineImages(this.discArt, this.discArtCover, this.discArtSize.w, this.discArtSize.h); | |
| 1703 | + | } | |
| 1704 | + | return this.discArt; | |
| 1705 | + | } | |
| 1706 | + | ||
| 1707 | + | /** | |
| 1708 | + | * Disposes the disc art image when changing or deactivating disc art. | |
| 1709 | + | * @param {GdiBitmap} discArtImg - The loaded disc art image. | |
| 1710 | + | */ | |
| 1711 | + | disposeDiscArt(discArtImg) { | |
| 1712 | + | this.discArtSize = new ImageSize(0, 0, 0, 0); | |
| 1713 | + | discArtImg = null; | |
| 1714 | + | } | |
| 1715 | + | ||
| 1716 | + | /** | |
| 1717 | + | * Fetches new disc art when a new album is being played. | |
| 1718 | + | */ | |
| 1719 | + | fetchDiscArt() { | |
| 1720 | + | if (!grm.ui.displayDetails) { | |
| 1721 | + | this.clearCache('discArt'); | |
| 1722 | + | return; | |
| 1723 | + | } | |
| 1724 | + | ||
| 1725 | + | grm.debug.setDebugProfile(grm.debug.showDebugTiming || grCfg.settings.showDebugPerformanceOverlay, 'create', '获取碟片'); | |
| 1726 | + | ||
| 1727 | + | if (grSet.displayDiscArt && !grm.ui.isStreaming) { | |
| 1728 | + | this.loadDiscArt(this.findDiscArtPath()); | |
| 1729 | + | } | |
| 1730 | + | ||
| 1731 | + | grm.debug.setDebugProfile(false, 'print', '获取碟片'); | |
| 1732 | + | } | |
| 1733 | + | ||
| 1734 | + | /** | |
| 1735 | + | * Finds the path to the disc art or disc art stub. | |
| 1736 | + | * @returns {string} The path to the disc art or disc art stub. | |
| 1737 | + | */ | |
| 1738 | + | findDiscArtPath() { | |
| 1739 | + | const discArtImagePaths = grPath.discArtImagePaths(); | |
| 1740 | + | const discArtStubPaths = grPath.discArtStubPaths(); | |
| 1741 | + | ||
| 1742 | + | if (grSet.noDiscArtStub || grSet.showDiscArtStub) { | |
| 1743 | + | for (const path of discArtImagePaths) { | |
| 1744 | + | if (IsFile(path)) { | |
| 1745 | + | this.discArtFound = true; | |
| 1746 | + | return path; | |
| 1747 | + | } | |
| 1748 | + | } | |
| 1749 | + | } | |
| 1750 | + | ||
| 1751 | + | this.discArtFound = false; | |
| 1752 | + | ||
| 1753 | + | return grSet.noDiscArtStub ? '' : discArtStubPaths[grSet.discArtStub] || grPath.discArtCustomStub; | |
| 1754 | + | } | |
| 1755 | + | ||
| 1756 | + | /** | |
| 1757 | + | * Initializes the disc art when the Details panel is opened or closed. | |
| 1758 | + | */ | |
| 1759 | + | initDiscArt() { | |
| 1760 | + | if (!grm.ui.displayDetails) { | |
| 1761 | + | this.clearCache('discArt'); | |
| 1762 | + | this.clearTimer('discArt'); | |
| 1763 | + | return; | |
| 1764 | + | } | |
| 1765 | + | ||
| 1766 | + | if (!this.discArtCover && grm.ui.albumArtList.length) { | |
| 1767 | + | const artIndex = grm.ui.albumArtList[grm.ui.albumArtIndex]; | |
| 1768 | + | if (artIndex && grm.artCache) { | |
| 1769 | + | this.discArtCover = grm.artCache.getImage(artIndex, 2) || | |
| 1770 | + | (grm.ui.albumArt && grm.artCache.encache(grm.ui.albumArt, artIndex, 2)); | |
| 1771 | + | } | |
| 1772 | + | } | |
| 1773 | + | ||
| 1774 | + | if (grSet.displayDiscArt && !grm.ui.isStreaming) { | |
| 1775 | + | if (this.discArt) { | |
| 1776 | + | this.updateDiscArt(); | |
| 1777 | + | } else { | |
| 1778 | + | this.fetchDiscArt(); | |
| 1779 | + | } | |
| 1780 | + | } | |
| 1781 | + | } | |
| 1782 | + | ||
| 1783 | + | initDiscArtStub() { | |
| 1784 | + | if (!grSet.displayDiscArt || grSet.noDiscArtStub) return; | |
| 1785 | + | ||
| 1786 | + | const stubPath = grPath.discArtStubPaths()[grSet.discArtStub] || grPath.discArtCustomStub; | |
| 1787 | + | if (!stubPath || grm.artCache.getImage(stubPath)) return; // already cached | |
| 1788 | + | ||
| 1789 | + | gdi.LoadImageAsyncV2(window.ID, stubPath).then(img => { | |
| 1790 | + | if (img) grm.artCache.encache(img, stubPath); | |
| 1791 | + | }); | |
| 1792 | + | } | |
| 1793 | + | ||
| 1794 | + | /** | |
| 1795 | + | * Loads the disc art from the given path. | |
| 1796 | + | * @param {string} discArtPath - The path to the disc art. | |
| 1797 | + | */ | |
| 1798 | + | loadDiscArt(discArtPath) { | |
| 1799 | + | const tempDiscArt = grm.ui.albumArtFromCache ? grm.artCache.getImage(discArtPath) : null; | |
| 1800 | + | ||
| 1801 | + | if (tempDiscArt) { | |
| 1802 | + | this.disposeDiscArt(this.discArt); | |
| 1803 | + | this.discArt = tempDiscArt; | |
| 1804 | + | if (grm.ui.displayDetails) this.updateDiscArt(); | |
| 1805 | + | return; | |
| 1806 | + | } | |
| 1807 | + | ||
| 1808 | + | gdi.LoadImageAsyncV2(window.ID, discArtPath).then(discArtImg => { | |
| 1809 | + | this.disposeDiscArt(this.discArt); // Delay disposal so we don't get flashing | |
| 1810 | + | this.discArt = grm.artCache.encache(discArtImg, discArtPath); | |
| 1811 | + | ||
| 1812 | + | if (!this.discArt && !grSet.noDiscArtStub) { | |
| 1813 | + | grm.ui.handleArtworkError('discArt'); | |
| 1814 | + | } else { | |
| 1815 | + | this.updateDiscArt(); | |
| 1816 | + | } | |
| 1817 | + | ||
| 1818 | + | this.clearCache('metrics', 'cachedLabelLastLeftEdge'); // Recalc label location | |
| 1819 | + | grm.debug.repaintWindow(); | |
| 1820 | + | }); | |
| 1821 | + | } | |
| 1822 | + | ||
| 1823 | + | /** | |
| 1824 | + | * Resizes and resets the size and position of the disc art. | |
| 1825 | + | * @param {boolean} resetDiscArtPosition - Whether the position of the disc art should be reset. | |
| 1826 | + | */ | |
| 1827 | + | resizeDiscArt(resetDiscArtPosition) { | |
| 1828 | + | if (!this.discArt) { | |
| 1829 | + | this.discArtSize = new ImageSize(0, 0, 0, 0); | |
| 1830 | + | return; | |
| 1831 | + | } | |
| 1832 | + | ||
| 1833 | + | this.setDiscArtScaleFactor(); | |
| 1834 | + | this.setDiscArtSize(resetDiscArtPosition); | |
| 1835 | + | this.setDiscArtPosition(resetDiscArtPosition); | |
| 1836 | + | this.setDiscArtShadow(); | |
| 1837 | + | } | |
| 1838 | + | ||
| 1839 | + | /** | |
| 1840 | + | * Repaints the disc art area to only cover the necessary region based on album art opacity settings and disc art layering. | |
| 1841 | + | */ | |
| 1842 | + | repaintDiscArt() { | |
| 1843 | + | const discArtLeftEdge = ( | |
| 1844 | + | grSet.detailsAlbumArtOpacity !== 255 || grSet.detailsAlbumArtDiscAreaOpacity !== 255 || grSet.discArtOnTop | |
| 1845 | + | ) ? this.discArtSize.x : grm.ui.albumArtSize.x + grm.ui.albumArtSize.w - 1; | |
| 1846 | + | ||
| 1847 | + | window.RepaintRect( | |
| 1848 | + | discArtLeftEdge, this.discArtSize.y, | |
| 1849 | + | this.discArtSize.w - (discArtLeftEdge - this.discArtSize.x), this.discArtSize.h, | |
| 1850 | + | !grSet.discArtOnTop && !grm.ui.displayLyrics | |
| 1851 | + | ); | |
| 1852 | + | } | |
| 1853 | + | ||
| 1854 | + | /** | |
| 1855 | + | * Repaints the metadata grid area to only cover the necessary region. | |
| 1856 | + | */ | |
| 1857 | + | repaintMetadataGrid() { | |
| 1858 | + | if (!grm.ui.displayDetails) return; | |
| 1859 | + | window.RepaintRect(0, grm.ui.topMenuHeight, Math.max(grm.ui.albumArtSize.x, SCALE(40)), grm.ui.wh - grm.ui.topMenuHeight - grm.ui.lowerBarHeight); | |
| 1860 | + | } | |
| 1861 | + | ||
| 1862 | + | /** | |
| 1863 | + | * Set the scale factor for the disc art based on the window size and layout. | |
| 1864 | + | */ | |
| 1865 | + | setDiscArtScaleFactor() { | |
| 1866 | + | const discArtMaxHeight = grm.ui.wh - grm.ui.topMenuHeight - grm.ui.lowerBarHeight; | |
| 1867 | + | const scaleFactor = grm.ui.displayPlaylist || grm.ui.displayLibrary ? 0.5 : 0.75; | |
| 1868 | + | const discScale = Math.min(grm.ui.ww * scaleFactor / this.discArt.Width, (discArtMaxHeight - SCALE(16)) / this.discArt.Height); | |
| 1869 | + | this.discArtScaleFactor = discScale; | |
| 1870 | + | } | |
| 1871 | + | ||
| 1872 | + | /** | |
| 1873 | + | * Set the size of the disc art based on its scale, window state, and layout settings. | |
| 1874 | + | * @param {boolean} resetDiscArtPosition - Whether the position of the disc art should be reset. | |
| 1875 | + | */ | |
| 1876 | + | setDiscArtSize(resetDiscArtPosition) { | |
| 1877 | + | const discArtSizeCorr = SCALE(4); | |
| 1878 | + | ||
| 1879 | + | const discArtSize = | |
| 1880 | + | grm.ui.hasArtwork ? grm.ui.albumArtSize.h - discArtSizeCorr : | |
| 1881 | + | Math.floor(this.discArt.Width * this.discArtScaleFactor) - discArtSizeCorr; | |
| 1882 | + | ||
| 1883 | + | if (resetDiscArtPosition) { | |
| 1884 | + | this.discArtSize = { w: discArtSize, h: discArtSize }; | |
| 1885 | + | } else { | |
| 1886 | + | this.discArtSize.w = Math.max(this.discArtSize.w, discArtSize); | |
| 1887 | + | this.discArtSize.h = this.discArtSize.w; | |
| 1888 | + | } | |
| 1889 | + | } | |
| 1890 | + | ||
| 1891 | + | /** | |
| 1892 | + | * Set the position of the disc art based on the window size and layout settings. | |
| 1893 | + | * @param {boolean} resetDiscArtPosition - Whether the position of the disc art should be reset. | |
| 1894 | + | */ | |
| 1895 | + | setDiscArtPosition(resetDiscArtPosition) { | |
| 1896 | + | const discArtSizeCorr = SCALE(4); | |
| 1897 | + | const discArtMargin = SCALE(2); | |
| 1898 | + | const discArtMarginRight = SCALE(36); | |
| 1899 | + | const discArtMaxHeight = grm.ui.wh - grm.ui.topMenuHeight - grm.ui.lowerBarHeight; | |
| 1900 | + | ||
| 1901 | + | if (grm.ui.hasArtwork) { | |
| 1902 | + | const baseX = grm.ui.ww - grm.ui.albumArtSize.h - discArtMarginRight; | |
| 1903 | + | ||
| 1904 | + | const adjustedX = grm.ui.albumArtSize.x + grm.ui.albumArtSize.w - | |
| 1905 | + | (grm.ui.albumArtSize.h - discArtSizeCorr) * (1 - grSet.discArtDisplayAmount) - | |
| 1906 | + | (grSet.discArtDisplayAmount === 1 || grSet.discArtDisplayAmount === 0.5 ? 0 : discArtMarginRight); | |
| 1907 | + | ||
| 1908 | + | const discArtX = Math.floor( | |
| 1909 | + | grSet.discArtDisplayAmount === 1 ? baseX : | |
| 1910 | + | grSet.discArtDisplayAmount === 0.5 ? Math.min(baseX, adjustedX) : | |
| 1911 | + | adjustedX | |
| 1912 | + | ); | |
| 1913 | + | ||
| 1914 | + | this.discArtSize.x = resetDiscArtPosition ? discArtX : Math.max(this.discArtSize.x, discArtX); | |
| 1915 | + | this.discArtSize.y = resetDiscArtPosition ? (grm.ui.albumArtSize.y + discArtMargin) : | |
| 1916 | + | Math.min(this.discArtSize.y > 0 ? this.discArtSize.y : | |
| 1917 | + | (grm.ui.albumArtSize.y + discArtMargin), grm.ui.albumArtSize.y + discArtMargin); | |
| 1918 | + | ||
| 1919 | + | if (this.discArtSize.x + this.discArtSize.w > grm.ui.ww) { | |
| 1920 | + | this.discArtSize.x = grm.ui.ww - this.discArtSize.w - discArtMarginRight; | |
| 1921 | + | } | |
| 1922 | + | ||
| 1923 | + | return; | |
| 1924 | + | } | |
| 1925 | + | ||
| 1926 | + | // * Set no disc art x-coordinate | |
| 1927 | + | const discArtOffCenter = this.discArtScaleFactor === (grm.ui.ww * 0.75 / this.discArt.Width); | |
| 1928 | + | ||
| 1929 | + | const discArtCenterX = | |
| 1930 | + | discArtOffCenter ? Math.round(grm.ui.ww * 0.66 - grm.ui.edgeMargin) : | |
| 1931 | + | (grm.ui.displayPlaylist || grm.ui.displayLibrary) ? grm.ui.ww * 0.25 : | |
| 1932 | + | grm.ui.ww * 0.5; | |
| 1933 | + | ||
| 1934 | + | this.discArtSize.x = Math.floor(discArtCenterX - this.discArtSize.w * 0.5); | |
| 1935 | + | ||
| 1936 | + | // * Set no disc art y-coordinate | |
| 1937 | + | const restrictedWidth = this.discArtScaleFactor !== (discArtMaxHeight - SCALE(16)) / this.discArt.Height; | |
| 1938 | + | const discArtCenterY = grm.ui.topMenuHeight + Math.floor(((discArtMaxHeight - SCALE(16)) / 2) - this.discArtSize.h / 2); | |
| 1939 | + | this.discArtSize.y = restrictedWidth ? Math.min(discArtCenterY, 160) : grm.ui.topMenuHeight + discArtMargin; | |
| 1940 | + | ||
| 1941 | + | grm.ui.hasArtwork = true; | |
| 1942 | + | } | |
| 1943 | + | ||
| 1944 | + | /** | |
| 1945 | + | * Sets up async precomputation of disc art frames, prioritized from current index. | |
| 1946 | + | * @param {GdiBitmap} combinedImg - The base image to rotate. | |
| 1947 | + | * @param {string} currentAlbumId - Unique ID to detect album changes. | |
| 1948 | + | * @param {number} rotationDegreeIncrement - Degrees per frame. | |
| 1949 | + | */ | |
| 1950 | + | setDiscArtPrecomputation(combinedImg, currentAlbumId, rotationDegreeIncrement) { | |
| 1951 | + | let batchCount = 0; | |
| 1952 | + | let frameTimeAvg = 0; | |
| 1953 | + | let precomputeIndex = (this.discArtRotationIndex + 1) % grSet.spinDiscArtImageCount; | |
| 1954 | + | let precomputeTimer = null; | |
| 1955 | + | let performanceTierCurrent = 'medium'; | |
| 1956 | + | ||
| 1957 | + | const performanceTiers = { | |
| 1958 | + | low: { batchSize: 1, batchDelay: 75 }, // 50-100ms | |
| 1959 | + | medium: { batchSize: 2, batchDelay: 25 }, // 20-40ms | |
| 1960 | + | high: { batchSize: 4, batchDelay: 10 } // 10-20ms | |
| 1961 | + | }; | |
| 1962 | + | ||
| 1963 | + | const updatePerformanceTier = (frameTime) => { | |
| 1964 | + | frameTimeAvg = (frameTimeAvg + frameTime) / 2; | |
| 1965 | + | const performanceTierNew = frameTimeAvg > 50 ? 'low' : frameTimeAvg < 10 ? 'high' : 'medium'; | |
| 1966 | + | if (performanceTierNew === performanceTierCurrent) return; | |
| 1967 | + | performanceTierCurrent = performanceTierNew; | |
| 1968 | + | const tier = performanceTiers[performanceTierCurrent]; | |
| 1969 | + | grm.debug.debugLog(`碟片 => 适应 ${performanceTierCurrent} 性能: 批量大小=${tier.batchSize}, 批量延迟=${tier.batchDelay} 毫秒 (avgFrameTime=${Math.round(frameTimeAvg)} 毫秒)`); | |
| 1970 | + | }; | |
| 1971 | + | ||
| 1972 | + | const precomputeBatch = () => { | |
| 1973 | + | if (this.discArt.Path !== currentAlbumId) return; | |
| 1974 | + | ||
| 1975 | + | const tier = performanceTiers[performanceTierCurrent]; | |
| 1976 | + | let computedInBatch = 0; | |
| 1977 | + | ||
| 1978 | + | while (computedInBatch < tier.batchSize && !this.discArtArray[precomputeIndex]) { | |
| 1979 | + | const rotationDegrees = rotationDegreeIncrement * precomputeIndex; | |
| 1980 | + | const frameStart = Date.now(); | |
| 1981 | + | this.discArtArray[precomputeIndex] = RotateImage(combinedImg, this.discArtSize.w, this.discArtSize.h, rotationDegrees, grm.artCache.discArtImgMaxRes); | |
| 1982 | + | const frameTime = Date.now() - frameStart; | |
| 1983 | + | updatePerformanceTier(frameTime); // Update per-frame for quicker response | |
| 1984 | + | grm.debug.debugLog(`碟片 => 预计算碟片图像: ${precomputeIndex} (${this.discArtSize.w}x${this.discArtSize.h}) 旋转: ${rotationDegrees} 度`); | |
| 1985 | + | computedInBatch++; | |
| 1986 | + | precomputeIndex = (precomputeIndex + 1) % grSet.spinDiscArtImageCount; | |
| 1987 | + | } | |
| 1988 | + | ||
| 1989 | + | batchCount++; | |
| 1990 | + | ||
| 1991 | + | if (this.discArtArray.every(frame => !!frame)) { | |
| 1992 | + | grm.debug.debugLog('碟片 => 所有帧均已预先计算'); | |
| 1993 | + | return; | |
| 1994 | + | } | |
| 1995 | + | ||
| 1996 | + | if (computedInBatch > 0) { | |
| 1997 | + | precomputeTimer = setTimeout(precomputeBatch, tier.batchDelay); | |
| 1998 | + | } | |
| 1999 | + | }; | |
| 2000 | + | ||
| 2001 | + | // Start immediately but async | |
| 2002 | + | setTimeout(precomputeBatch, 0); | |
| 2003 | + | ||
| 2004 | + | // Cleanup | |
| 2005 | + | this.clearTimer = (type) => { | |
| 2006 | + | if (type === 'discArt' && precomputeTimer) { | |
| 2007 | + | clearTimeout(precomputeTimer); | |
| 2008 | + | grm.debug.debugLog('碟片 => 清除预计算计时器'); | |
| 2009 | + | } | |
| 2010 | + | Details.prototype.clearTimer.call(this, type); | |
| 2011 | + | delete this.clearTimer; // Restore to prototype chain | |
| 2012 | + | }; | |
| 2013 | + | } | |
| 2014 | + | ||
| 2015 | + | /** | |
| 2016 | + | * Sets and creates the disc art rotation animation with RotateImg(). | |
| 2017 | + | * @returns {GdiBitmap} The rotated disc art image. | |
| 2018 | + | */ | |
| 2019 | + | setDiscArtRotation() { | |
| 2020 | + | if (!grSet.displayDiscArt || grm.ui.albumArtCorrupt || !grm.ui.albumArt || !this.discArt || this.discArtSize.w < 1) { | |
| 2021 | + | return null; | |
| 2022 | + | } | |
| 2023 | + | ||
| 2024 | + | // Drawing discArt rotated is slow, so first draw it rotated into the discArtRotation image, and then draw discArtRotation image unrotated in on_paint. | |
| 2025 | + | const vinylAdjustedTrackNumFormat = `$num($if(${grTF.vinyl_tracknum},$sub($mul(${grTF.vinyl_tracknum},2),1),$if2(%tracknumber%,1)),1)`; | |
| 2026 | + | let tracknum = parseInt($(vinylAdjustedTrackNumFormat)) - 1; | |
| 2027 | + | if (!grSet.rotateDiscArt || Number.isNaN(tracknum)) tracknum = 0; | |
| 2028 | + | ||
| 2029 | + | const tracknumRotation = tracknum * grSet.rotationAmt % 360; | |
| 2030 | + | const combinedImg = this.combineDiscArtWithCover(true); | |
| 2031 | + | ||
| 2032 | + | this.discArtRotation = RotateImage(combinedImg, this.discArtSize.w, this.discArtSize.h, tracknumRotation, grm.artCache.discArtImgMaxRes); | |
| 2033 | + | this.discArtRotationIndex = Math.round(tracknumRotation / (360 / grSet.spinDiscArtImageCount)) % grSet.spinDiscArtImageCount; | |
| 2034 | + | ||
| 2035 | + | return this.discArtRotation; | |
| 2036 | + | } | |
| 2037 | + | ||
| 2038 | + | /** | |
| 2039 | + | * Sets the disc art timer with different set interval values for rotating the disc art. | |
| 2040 | + | */ | |
| 2041 | + | setDiscArtRotationTimer() { | |
| 2042 | + | this.clearTimer('discArt'); | |
| 2043 | + | ||
| 2044 | + | if (grSet.layout !== 'default' || !grm.ui.displayDetails || | |
| 2045 | + | grm.ui.albumArtCorrupt || !grm.ui.albumArt || !this.discArt || !this.discArtSize.w || | |
| 2046 | + | !fb.IsPlaying || fb.IsPaused || !grSet.displayDiscArt || !grSet.spinDiscArt) { | |
| 2047 | + | return; | |
| 2048 | + | } | |
| 2049 | + | ||
| 2050 | + | grm.debug.debugLog(`碟片 => 使用异步预计算启动延迟旋转: ${grSet.spinDiscArtImageCount} 帧, 每 ${grSet.spinDiscArtRedrawInterval} 毫秒 1 次`); | |
| 2051 | + | ||
| 2052 | + | const rotationDegreeIncrement = 360 / grSet.spinDiscArtImageCount; | |
| 2053 | + | const combinedImg = this.combineDiscArtWithCover(false); | |
| 2054 | + | const currentAlbumId = this.discArt.Path; | |
| 2055 | + | ||
| 2056 | + | // Main animation timer | |
| 2057 | + | this.discArtRotationTimer = setInterval(() => { | |
| 2058 | + | const intendedIndex = (this.discArtRotationIndex + 1) % grSet.spinDiscArtImageCount; | |
| 2059 | + | ||
| 2060 | + | let displayIndex = intendedIndex; | |
| 2061 | + | if (!this.discArtArray[intendedIndex]) { | |
| 2062 | + | // Nearest available: prioritize smallest angular distance, alternating fwd/bwd | |
| 2063 | + | const count = grSet.spinDiscArtImageCount; | |
| 2064 | + | let nearestFound = false; | |
| 2065 | + | for (let dist = 0; dist < count; dist++) { | |
| 2066 | + | const fwd = (intendedIndex + dist) % count; | |
| 2067 | + | if (this.discArtArray[fwd]) { | |
| 2068 | + | displayIndex = fwd; | |
| 2069 | + | nearestFound = true; | |
| 2070 | + | break; | |
| 2071 | + | } | |
| 2072 | + | ||
| 2073 | + | const bwd = (intendedIndex - dist + count) % count; | |
| 2074 | + | if (this.discArtArray[bwd]) { | |
| 2075 | + | displayIndex = bwd; | |
| 2076 | + | nearestFound = true; | |
| 2077 | + | break; | |
| 2078 | + | } | |
| 2079 | + | } | |
| 2080 | + | if (!nearestFound) displayIndex = 0; // Ultimate fallback | |
| 2081 | + | ||
| 2082 | + | grm.debug.debugLog(`碟片 => 帧 ${intendedIndex} 未就绪,显示最接近的 ${displayIndex}`); | |
| 2083 | + | ||
| 2084 | + | // Emergency compute intended (sync for immediacy, but only one frame) | |
| 2085 | + | const rotationDegrees = rotationDegreeIncrement * intendedIndex; | |
| 2086 | + | this.discArtArray[intendedIndex] = RotateImage(combinedImg, this.discArtSize.w, this.discArtSize.h, rotationDegrees, grm.artCache.discArtImgMaxRes); | |
| 2087 | + | grm.debug.debugLog(`碟片 => 紧急计算碟片: ${intendedIndex} (${this.discArtSize.w}x${this.discArtSize.h}) 旋转: ${rotationDegrees} 度`); | |
| 2088 | + | } | |
| 2089 | + | ||
| 2090 | + | this.discArtRotationIndex = intendedIndex; // Advance intended for next tick | |
| 2091 | + | this.repaintDiscArt(); // Repaint with displayIndex (but since we just computed if missing, often same) | |
| 2092 | + | }, grSet.spinDiscArtRedrawInterval); | |
| 2093 | + | ||
| 2094 | + | // Start precomputation | |
| 2095 | + | this.setDiscArtPrecomputation(combinedImg, currentAlbumId, rotationDegreeIncrement); | |
| 2096 | + | } | |
| 2097 | + | ||
| 2098 | + | /** | |
| 2099 | + | * Sets the drop shadow for disc art. | |
| 2100 | + | */ | |
| 2101 | + | setDiscArtShadow() { | |
| 2102 | + | const isDisabled = !grm.ui.displayDetails || !grSet.displayDiscArt || grSet.layout === 'compact'; | |
| 2103 | + | const isMissing = !this.discArt || !grm.ui.hasArtwork && !grm.ui.noAlbumArtStub; | |
| 2104 | + | const isCached = this.discArtShadowImg && this.discArtShadowImg.image && this.discArtShadowImg.size === this.discArtSize.h; | |
| 2105 | + | ||
| 2106 | + | if (isDisabled || isMissing || isCached) return; | |
| 2107 | + | ||
| 2108 | + | grm.debug.setDebugProfile(grm.debug.showDebugTiming || grCfg.settings.showDebugPerformanceOverlay, 'create', '创建碟片阴影'); | |
| 2109 | + | ||
| 2110 | + | const discArtMargin = SCALE(2); | |
| 2111 | + | ||
| 2112 | + | if (grm.ui.albumArtSize.w > 0 || this.discArtSize.w > 0) { | |
| 2113 | + | const width = this.discArt | |
| 2114 | + | ? this.discArtSize.x + this.discArtSize.w + 2 * this.discArtShadow | |
| 2115 | + | : grm.ui.albumArtSize.x + grm.ui.albumArtSize.w + 2 * this.discArtShadow; | |
| 2116 | + | ||
| 2117 | + | const height = this.discArt | |
| 2118 | + | ? this.discArtSize.h + discArtMargin + 2 * this.discArtShadow | |
| 2119 | + | : grm.ui.albumArtSize.h + 2 * this.discArtShadow; | |
| 2120 | + | ||
| 2121 | + | const newShadowImg = gdi.CreateImage(width, height); | |
| 2122 | + | ||
| 2123 | + | if (grSet.layout === 'default' && newShadowImg) { | |
| 2124 | + | const shimg = newShadowImg.GetGraphics(); | |
| 2125 | + | ||
| 2126 | + | if (this.discArt) { | |
| 2127 | + | const offset = this.discArtSize.w * 0.40; // Don't change this value | |
| 2128 | + | const xVal = this.discArtSize.x; | |
| 2129 | + | const shadowOffset = this.discArtShadow * 2; | |
| 2130 | + | ||
| 2131 | + | shimg.DrawEllipse(xVal + shadowOffset, shadowOffset + discArtMargin, this.discArtSize.w - shadowOffset, this.discArtSize.w - shadowOffset, shadowOffset, grCol.discArtShadow); // outer shadow | |
| 2132 | + | shimg.DrawEllipse(xVal + this.discArtShadow + offset, offset + this.discArtShadow + discArtMargin, this.discArtSize.w - offset * 2, this.discArtSize.h - offset * 2, 60, grCol.discArtShadow); // inner shadow | |
| 2133 | + | } | |
| 2134 | + | ||
| 2135 | + | newShadowImg.ReleaseGraphics(shimg); | |
| 2136 | + | newShadowImg.StackBlur(this.discArtShadow); | |
| 2137 | + | } | |
| 2138 | + | ||
| 2139 | + | this.discArtShadowImg.image = newShadowImg; | |
| 2140 | + | this.discArtShadowImg.size = this.discArtSize.h; | |
| 2141 | + | } | |
| 2142 | + | ||
| 2143 | + | grm.debug.setDebugProfile(false, 'print', '创建碟片阴影'); | |
| 2144 | + | } | |
| 2145 | + | ||
| 2146 | + | /** | |
| 2147 | + | * Updates the disc art by resizing artwork, creating rotation, and setting the rotation timer. | |
| 2148 | + | */ | |
| 2149 | + | updateDiscArt() { | |
| 2150 | + | grm.ui.resizeArtwork(true); | |
| 2151 | + | this.setDiscArtRotation(); | |
| 2152 | + | ||
| 2153 | + | if (!grSet.spinDiscArt) return; | |
| 2154 | + | ||
| 2155 | + | this.discArtArray = []; // Clear last image | |
| 2156 | + | this.setDiscArtRotationTimer(); | |
| 2157 | + | } | |
| 2158 | + | // #endregion | |
| 2159 | + | ||
| 2160 | + | // * PUBLIC METHODS - BAND & LABEL LOGO * // | |
| 2161 | + | // #region PUBLIC METHODS - BAND & LABEL LOGO | |
| 2162 | + | /** | |
| 2163 | + | * Gets the band logo path if it exists at various paths. | |
| 2164 | + | * @param {string} bandStr - The name of the band. | |
| 2165 | + | * @returns {string} The path of the band logo if it exists. | |
| 2166 | + | */ | |
| 2167 | + | getBandLogoPath(bandStr) { | |
| 2168 | + | if (!bandStr) return ''; | |
| 2169 | + | ||
| 2170 | + | const testBandLogoPath = (imgDir, name) => { | |
| 2171 | + | const logoPath = `${imgDir}${name}.png`; | |
| 2172 | + | if (IsFile(logoPath)) { | |
| 2173 | + | grm.debug.debugLog(`图标 => 找到艺术家标识: ${logoPath}`); | |
| 2174 | + | return logoPath; | |
| 2175 | + | } | |
| 2176 | + | return ''; | |
| 2177 | + | }; | |
| 2178 | + | ||
| 2179 | + | const bandLogoPath = | |
| 2180 | + | testBandLogoPath(grPath.artistlogos, bandStr) || // Try 800x310 white | |
| 2181 | + | testBandLogoPath(grPath.artistlogosColor, bandStr); // Try 800x310 color | |
| 2182 | + | ||
| 2183 | + | return bandLogoPath || ''; | |
| 2184 | + | } | |
| 2185 | + | ||
| 2186 | + | /** | |
| 2187 | + | * Gets the band logo and its inverted version based on the current playing album artist in Details. | |
| 2188 | + | * @param {FbMetadbHandle} metadb - The metadb of the track. | |
| 2189 | + | */ | |
| 2190 | + | getBandLogo(metadb = undefined) { | |
| 2191 | + | this.clearCache('bandLogo'); | |
| 2192 | + | let path; | |
| 2193 | + | ||
| 2194 | + | const artists = GetMetaValues('%artist%', metadb); | |
| 2195 | + | const trackArtist = ReplaceIllegalChars($('[%track artist%]', metadb)); | |
| 2196 | + | const albumArtists = GetMetaValues('%album artist%', metadb); | |
| 2197 | + | ||
| 2198 | + | const artistList = [ | |
| 2199 | + | ...artists.flatMap(artist => [ | |
| 2200 | + | ReplaceIllegalChars(artist), ReplaceIllegalChars(artist).replace(Regex.TextPrefixThe, '') | |
| 2201 | + | ]), | |
| 2202 | + | trackArtist, | |
| 2203 | + | ...albumArtists.flatMap(artist => [ | |
| 2204 | + | ReplaceIllegalChars(artist), ReplaceIllegalChars(artist).replace(Regex.TextPrefixThe, '') | |
| 2205 | + | ]) | |
| 2206 | + | ]; | |
| 2207 | + | const uniqueArtistList = [...new Set(artistList)]; | |
| 2208 | + | ||
| 2209 | + | for (const artist of uniqueArtistList) { | |
| 2210 | + | path = this.getBandLogoPath(artist); | |
| 2211 | + | if (path) break; | |
| 2212 | + | } | |
| 2213 | + | ||
| 2214 | + | if (!path) return; | |
| 2215 | + | ||
| 2216 | + | this.bandLogo = grm.artCache.getImage(path); | |
| 2217 | + | if (!this.bandLogo) { | |
| 2218 | + | const logo = gdi.Image(path); | |
| 2219 | + | if (logo) { | |
| 2220 | + | this.bandLogo = grm.artCache.encache(logo, path); | |
| 2221 | + | this.bandLogoInverted = grm.artCache.encache(logo.InvertColours(), `${path}-inv`); | |
| 2222 | + | } | |
| 2223 | + | } | |
| 2224 | + | ||
| 2225 | + | this.bandLogoInverted = grm.artCache.getImage(`${path}-inv`); | |
| 2226 | + | if (!this.bandLogoInverted && this.bandLogo) { | |
| 2227 | + | this.bandLogoInverted = grm.artCache.encache(this.bandLogo.InvertColours(), `${path}-inv`); | |
| 2228 | + | } | |
| 2229 | + | } | |
| 2230 | + | ||
| 2231 | + | /** | |
| 2232 | + | * Gets label logos based on current playing album artist in Details. | |
| 2233 | + | * @param {FbMetadbHandle} metadb - The metadb of the track. | |
| 2234 | + | */ | |
| 2235 | + | getLabelLogo(metadb) { | |
| 2236 | + | this.clearCache('labelLogo'); | |
| 2237 | + | const labelFields = ['label', 'publisher', 'discogs_label']; | |
| 2238 | + | const labels = new Set(labelFields.flatMap(label => GetMetaValues(label, metadb))); | |
| 2239 | + | ||
| 2240 | + | for (const label of labels) { | |
| 2241 | + | const addLabel = this.loadLabelLogo(label); | |
| 2242 | + | if (addLabel != null) { | |
| 2243 | + | this.labelLogo.push(addLabel); | |
| 2244 | + | try { | |
| 2245 | + | this.labelLogoInverted.push(addLabel.InvertColours()); | |
| 2246 | + | } catch (e) {} | |
| 2247 | + | } | |
| 2248 | + | } | |
| 2249 | + | } | |
| 2250 | + | ||
| 2251 | + | /** | |
| 2252 | + | * Loads the label logo image for a given record label in Details. | |
| 2253 | + | * @param {string} publisherString - The name of a record label or publisher. | |
| 2254 | + | * @returns {GdiBitmap|null} The record label logo as a gdi image object or null if not found. | |
| 2255 | + | */ | |
| 2256 | + | loadLabelLogo(publisherString) { | |
| 2257 | + | const date = new Date(); | |
| 2258 | + | const lastSearchYear = date.getFullYear(); | |
| 2259 | + | let dir = grPath.labelsBase; | |
| 2260 | + | let labelStr = ReplaceIllegalChars(publisherString); | |
| 2261 | + | let recordLabel = null; | |
| 2262 | + | ||
| 2263 | + | if (!labelStr) return recordLabel; | |
| 2264 | + | ||
| 2265 | + | // * Clean up the label string | |
| 2266 | + | const cleanLabelString = (str) => str | |
| 2267 | + | .replace(Regex.ArtImageLabelSuffix, '') | |
| 2268 | + | .replace(Regex.EdgeDotSpaceTrailing, '') | |
| 2269 | + | .replace(Regex.TextDash, '-'); | |
| 2270 | + | ||
| 2271 | + | // * Check for label folders by year | |
| 2272 | + | const checkLabelFolders = (label) => { | |
| 2273 | + | const startYear = parseInt($('$year(%date%)')); | |
| 2274 | + | const baseDir = `${dir}${label}\\`; | |
| 2275 | + | ||
| 2276 | + | for (let year = startYear; year <= lastSearchYear; year++) { | |
| 2277 | + | const yearFolder = `${baseDir}${year}`; | |
| 2278 | + | if (IsFolder(yearFolder)) { | |
| 2279 | + | grm.debug.debugLog(`图标 => 找到 ${label} 年份 ${year}文件夹.`); | |
| 2280 | + | return `${yearFolder}\\`; | |
| 2281 | + | } | |
| 2282 | + | } | |
| 2283 | + | ||
| 2284 | + | grm.debug.debugLog(`图标 => 找到 ${label} 文件夹并使用最新图标.`); | |
| 2285 | + | return baseDir; | |
| 2286 | + | }; | |
| 2287 | + | ||
| 2288 | + | // * Check if a folder exists for the initial label string | |
| 2289 | + | const folderExists = (label) => IsFolder(`${dir}${label}`); | |
| 2290 | + | if (folderExists(labelStr)) { | |
| 2291 | + | dir = checkLabelFolders(labelStr); | |
| 2292 | + | } else { | |
| 2293 | + | labelStr = cleanLabelString(labelStr); | |
| 2294 | + | if (folderExists(labelStr)) { | |
| 2295 | + | dir = checkLabelFolders(labelStr); | |
| 2296 | + | } | |
| 2297 | + | } | |
| 2298 | + | ||
| 2299 | + | // * Reinitialize to original string for file search | |
| 2300 | + | labelStr = ReplaceIllegalChars(publisherString); | |
| 2301 | + | ||
| 2302 | + | // * Get the file path for the initial label string | |
| 2303 | + | const searchFile = (label) => `${dir}${label}.png`; | |
| 2304 | + | let label = searchFile(labelStr); | |
| 2305 | + | ||
| 2306 | + | // * Load the record label image | |
| 2307 | + | if (IsFile(label)) { | |
| 2308 | + | recordLabel = gdi.Image(label); | |
| 2309 | + | grm.debug.debugLog('图标 => 找到唱片商标:', label, !recordLabel ? '<无法加载>' : ''); | |
| 2310 | + | } else { | |
| 2311 | + | labelStr = cleanLabelString(labelStr); | |
| 2312 | + | label = searchFile(labelStr); | |
| 2313 | + | if (IsFile(label)) { | |
| 2314 | + | recordLabel = gdi.Image(label); | |
| 2315 | + | } else { | |
| 2316 | + | label = searchFile(`${labelStr} Records`); | |
| 2317 | + | if (IsFile(label)) { | |
| 2318 | + | recordLabel = gdi.Image(label); | |
| 2319 | + | } | |
| 2320 | + | } | |
| 2321 | + | } | |
| 2322 | + | ||
| 2323 | + | return recordLabel; | |
| 2324 | + | } | |
| 2325 | + | // #endregion | |
| 2326 | + | ||
| 2327 | + | // * PUBLIC METHODS - CALLBACKS * // | |
| 2328 | + | // #region PUBLIC METHODS - CALLBACKS | |
| 2329 | + | /** | |
| 2330 | + | * Checks if the mouse is within the boundaries of the metadata grid in Details. | |
| 2331 | + | * @global | |
| 2332 | + | * @param {number} x - The x-coordinate. | |
| 2333 | + | * @param {number} y - The y-coordinate. | |
| 2334 | + | * @param {string} boundary - The boundary to check ('artist', 'title', 'album', 'tagKey', 'tagValue', 'timeline', 'grid'). | |
| 2335 | + | * @returns {boolean} True or false. | |
| 2336 | + | */ | |
| 2337 | + | mouseInMetadataGrid(x, y, boundary) { | |
| 2338 | + | return this.gridSectionBounds[boundary] ? this.gridSectionBounds[boundary](x, y) : false; | |
| 2339 | + | } | |
| 2340 | + | ||
| 2341 | + | /** | |
| 2342 | + | * Handles the tooltip when the mouse is in the metadata grid tooltip area. | |
| 2343 | + | * @param {number} x - The x-coordinate. | |
| 2344 | + | * @param {number} y - The y-coordinate. | |
| 2345 | + | * @param {number} m - The mouse mask. | |
| 2346 | + | */ | |
| 2347 | + | on_mouse_move(x, y, m) { | |
| 2348 | + | if (grSet.showTooltipMain || grSet.showTooltipTruncated) { | |
| 2349 | + | this.handleGridTooltip(x, y); | |
| 2350 | + | } | |
| 2351 | + | if (grSet.showTooltipTimeline) { | |
| 2352 | + | this.handleGridTimelineTooltip(x, y); | |
| 2353 | + | } | |
| 2354 | + | } | |
| 2355 | + | // #endregion | |
| 2356 | + | } | |
gr-main-components.js(archivo creado)
| @@ -0,0 +1,4459 @@ | |||
| 1 | + | ///////////////////////////////////////////////////////////////////////////////// | |
| 2 | + | // * Georgia-ReBORN: A Clean - Full Dynamic Color Reborn - Foobar2000 Player * // | |
| 3 | + | // * Description: Georgia-ReBORN Main Components * // | |
| 4 | + | // * Author: TT * // | |
| 5 | + | // * Website: https://github.com/TT-ReBORN/Georgia-ReBORN * // | |
| 6 | + | // * Version: 3.0-x64-DEV * // | |
| 7 | + | // * Dev. started: 22-12-2017 * // | |
| 8 | + | // * Last change: 17-05-2026 * // | |
| 9 | + | ///////////////////////////////////////////////////////////////////////////////// | |
| 10 | + | ||
| 11 | + | ||
| 12 | + | 'use strict'; | |
| 13 | + | ||
| 14 | + | ||
| 15 | + | /////////////////////// | |
| 16 | + | // * IMAGE CACHING * // | |
| 17 | + | /////////////////////// | |
| 18 | + | /** | |
| 19 | + | * A class that creates album art and playlist thumbnails cache. | |
| 20 | + | */ | |
| 21 | + | class ArtCache { | |
| 22 | + | /** | |
| 23 | + | * Creates the `ArtCache` instance. | |
| 24 | + | * The ArtCache is a Least-Recently Used cache meaning that each cache hit will bump | |
| 25 | + | * that image to be the last image to be removed from the cache (if maxCacheSize is exceeded). | |
| 26 | + | * @param {number} maxCacheSize - The maximum number of images to keep in the cache. | |
| 27 | + | */ | |
| 28 | + | constructor(maxCacheSize = 15) { | |
| 29 | + | /** | |
| 30 | + | * @typedef {object} ArtCacheObj | |
| 31 | + | * @property {GdiBitmap} image - The GDI+ bitmap image object cached. | |
| 32 | + | * @property {number} filesize - The size of the image file in bytes. | |
| 33 | + | */ | |
| 34 | + | ||
| 35 | + | /** @private @type {object.<string, ArtCacheObj>} The primary cache storing image objects. */ | |
| 36 | + | this.cache = {}; | |
| 37 | + | /** @private @type {object.<string, ArtCacheObj>} The secondary cache used mainly for disc art covers to prevent overwriting album art with masked images. */ | |
| 38 | + | this.cache2 = {}; | |
| 39 | + | /** @private @type {string[]} The array of cache keys in the order of their usage. */ | |
| 40 | + | this.cacheIndexes = []; | |
| 41 | + | /** @private @type {string[]} The array of secondary cache keys in the order of their usage. */ | |
| 42 | + | this.cacheIndexes2 = []; | |
| 43 | + | /** @private @type {number} The maximum number of images that can be stored in the primary cache. */ | |
| 44 | + | this.cacheMaxSize = maxCacheSize; | |
| 45 | + | /** @private @type {number} The maximum number of images that can be stored in the secondary cache. */ | |
| 46 | + | this.cacheMaxSize2 = maxCacheSize; | |
| 47 | + | ||
| 48 | + | /** @private @type {number} The maximum width an image can be displayed. */ | |
| 49 | + | this.imgMaxWidth = SCALE(1440); | |
| 50 | + | /** @private @type {number} The maximum height an image can be displayed. */ | |
| 51 | + | this.imgMaxHeight = SCALE(872); | |
| 52 | + | ||
| 53 | + | /** | |
| 54 | + | * Because foobar x86 can allocate only 4 gigs memory, we must limit disc art res for 4K when using | |
| 55 | + | * high grSet.spinDiscArtImageCount, i.e 90 (4 degrees), 120 (3 degrees), 180 (2 degrees) to prevent crash. | |
| 56 | + | * When SMP has x64 support, we could try to increase this limit w (1836px max possible res for 4K). | |
| 57 | + | * @public @type {number} | |
| 58 | + | */ | |
| 59 | + | this.discArtImgMaxRes = this.setDiscArtMaxResolution(grSet.spinDiscArtImageCount); | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // * PUBLIC METHODS * // | |
| 63 | + | // #region PUBLIC METHODS | |
| 64 | + | /** | |
| 65 | + | * Gets cached image if it exists under the location string. If image is found, move it's index to the end of the cacheIndexes. | |
| 66 | + | * @param {string} location - The string value to check if image is cached under. | |
| 67 | + | * @param {number} cacheIndex - The first or second index of the cache to check. | |
| 68 | + | * @returns {GdiBitmap|null} The cached image, or null if not found or the file does not exist. | |
| 69 | + | */ | |
| 70 | + | getImage(location, cacheIndex = 1) { | |
| 71 | + | const cache = cacheIndex === 1 ? this.cache : this.cache2; | |
| 72 | + | const cacheIndexes = cacheIndex === 1 ? this.cacheIndexes : this.cacheIndexes2; | |
| 73 | + | ||
| 74 | + | if (!cache[location] || !fso.FileExists(location)) { | |
| 75 | + | // If image is not in cache or location does not exist, return to prevent crash. | |
| 76 | + | return null; | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | const file = fso.GetFile(location); | |
| 80 | + | const pathIndex = cacheIndexes.indexOf(location); | |
| 81 | + | cacheIndexes.splice(pathIndex, 1); | |
| 82 | + | ||
| 83 | + | if (file && file.Size === cache[location].filesize) { | |
| 84 | + | cacheIndexes.push(location); | |
| 85 | + | grm.debug.debugLog('Art cache => Cache hit:', location); | |
| 86 | + | return cache[location].image; | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | // Size of file on disk has changed | |
| 90 | + | grm.debug.debugLog(`Art cache => Cache entry was stale: ${location} [old size: ${cache[location].filesize}, new size: ${file.Size}]`); | |
| 91 | + | delete cache[location]; // Was removed from cacheIndexes already | |
| 92 | + | ||
| 93 | + | return null; | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | /** | |
| 97 | + | * Gets and optionally logs the size of an image or all images in both caches if cacheIndex is 0. | |
| 98 | + | * @param {string|null} location - The location string of the image to check. If null, process all images in the specified cache or both if cacheIndex is 0. | |
| 99 | + | * @param {number} cacheIndex - The index of the cache to check. 0 for both, 1 for the first, 2 for the second. Defaults to 1. | |
| 100 | + | * @param {boolean} logSize - Whether to log the size(s) to the console. Defaults to false. | |
| 101 | + | * @returns {number|null|object} The size of the image in bytes, all images sizes if location is null and cacheIndex is specified, or null if the image is not found. | |
| 102 | + | * @example | |
| 103 | + | * - Get size of a specific image in cache 1: getImageSize('path/to/img.jpg', 1); | |
| 104 | + | * - Get sizes of all images in cache 1: getImageSize(null, 1, true); | |
| 105 | + | * - Get sizes of all images in both caches: getImageSize(null, 0, true); | |
| 106 | + | */ | |
| 107 | + | getImageSize(location, cacheIndex = 1, logSize = false) { | |
| 108 | + | const processCache = (cache, prefix = '') => { | |
| 109 | + | const sizes = {}; | |
| 110 | + | for (const [loc, cacheObj] of Object.entries(cache)) { | |
| 111 | + | const formattedSize = FormatSize(cacheObj.filesize); | |
| 112 | + | sizes[loc] = formattedSize; | |
| 113 | + | if (logSize) { | |
| 114 | + | console.log(`Art cache => ${prefix}Image at '${loc}' size: ${formattedSize}`); | |
| 115 | + | } | |
| 116 | + | } | |
| 117 | + | return sizes; | |
| 118 | + | }; | |
| 119 | + | ||
| 120 | + | if (cacheIndex === 0) { // If location is 0, process both caches | |
| 121 | + | const sizes1 = processCache(this.cache, 'Cache 1 '); | |
| 122 | + | const sizes2 = processCache(this.cache2, 'Cache 2 '); | |
| 123 | + | return { ...sizes1, ...sizes2 }; // Merge results from both caches | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | const cache = cacheIndex === 1 ? this.cache : this.cache2; | |
| 127 | + | ||
| 128 | + | if (location === null) { // If location is null, process all images in the specified cache. | |
| 129 | + | return processCache(cache); | |
| 130 | + | } | |
| 131 | + | else if (cache[location]) { // Process a specific image in the specified cache. | |
| 132 | + | const formattedSize = FormatSize(cache[location].filesize); | |
| 133 | + | if (logSize) { | |
| 134 | + | console.log(`Art cache => Image at '${location}' size: ${formattedSize}`); | |
| 135 | + | } | |
| 136 | + | return formattedSize; | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | return null; | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | /** | |
| 143 | + | * Gets and optionally logs the total size of the cached images. | |
| 144 | + | * If cacheIndex is 0, calculates for both caches combined. | |
| 145 | + | * @param {number} cacheIndex - The index of the cache to calculate size for. If 0, calculates for both caches. | |
| 146 | + | * @param {boolean} logSizes - Whether to log individual image sizes to the console. | |
| 147 | + | * @returns {number} The total size of the cache or caches in bytes. | |
| 148 | + | * @example | |
| 149 | + | * - Get total size of cache 1: getTotalCacheSize(1, true); | |
| 150 | + | * - Get total size of both caches combined: getTotalCacheSize(0, true); | |
| 151 | + | */ | |
| 152 | + | getTotalCacheSize(cacheIndex, logSizes = false) { | |
| 153 | + | let totalSize = 0; | |
| 154 | + | ||
| 155 | + | const calculateAndLogSize = (cache, cacheName = '') => { | |
| 156 | + | for (const [location, cacheObj] of Object.entries(cache)) { | |
| 157 | + | totalSize += cacheObj.filesize; | |
| 158 | + | if (logSizes) { | |
| 159 | + | const formattedSize = FormatSize(cacheObj.filesize); | |
| 160 | + | console.log(`Art cache => ${cacheName} Image at '${location}' size: ${formattedSize}`); | |
| 161 | + | } | |
| 162 | + | } | |
| 163 | + | }; | |
| 164 | + | ||
| 165 | + | if (cacheIndex === 0) { // If cacheIndex is 0, process both caches | |
| 166 | + | calculateAndLogSize(this.cache, 'Cache 1'); | |
| 167 | + | calculateAndLogSize(this.cache2, 'Cache 2'); | |
| 168 | + | } else { | |
| 169 | + | const cache = cacheIndex === 1 ? this.cache : this.cache2; | |
| 170 | + | calculateAndLogSize(cache, `Cache ${cacheIndex}`); | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | const cacheLabel = cacheIndex === 0 ? 'Total size for both caches' : `Total size for Cache ${cacheIndex}`; | |
| 174 | + | const totalFormattedSize = FormatSize(totalSize); | |
| 175 | + | if (logSizes) console.log(`Art cache => ${cacheLabel}: ${totalFormattedSize}`); | |
| 176 | + | ||
| 177 | + | return totalFormattedSize; | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | /** | |
| 181 | + | * Sets the maximum resolution for disc art based on the spinDiscArtImageCount. | |
| 182 | + | * @param {number} spinDiscArtImageCount - The count for spinning disc art images. | |
| 183 | + | * @returns {number} The maximum resolution for the disc art image. | |
| 184 | + | */ | |
| 185 | + | setDiscArtMaxResolution(spinDiscArtImageCount = 72) { | |
| 186 | + | const maxResByImgCount = { | |
| 187 | + | 36: 1500, | |
| 188 | + | 45: 1500, | |
| 189 | + | 60: 1400, | |
| 190 | + | 72: 1400, | |
| 191 | + | 90: 1300, | |
| 192 | + | 120: 1200, | |
| 193 | + | 180: 1000 | |
| 194 | + | }; | |
| 195 | + | ||
| 196 | + | return maxResByImgCount[spinDiscArtImageCount]; | |
| 197 | + | } | |
| 198 | + | ||
| 199 | + | /** | |
| 200 | + | * Adds a rescaled image to the cache under string `location` and returns the cached image. | |
| 201 | + | * @param {GdiBitmap} img - The image object to cache. | |
| 202 | + | * @param {string} location - The string value to cache image under. Does not need to be a path. | |
| 203 | + | * @param {number} cacheIndex - The first or second index of the cache to check. | |
| 204 | + | * @returns {GdiBitmap} The image stored in the cache at the specified location. | |
| 205 | + | * If there is no image in the cache at that location, it returns the original image passed as a parameter. | |
| 206 | + | */ | |
| 207 | + | encache(img, location, cacheIndex = 1) { | |
| 208 | + | const cache = cacheIndex === 1 ? this.cache : this.cache2; | |
| 209 | + | const cacheIndexes = cacheIndex === 1 ? this.cacheIndexes : this.cacheIndexes2; | |
| 210 | + | const cacheMaxSize = cacheIndex === 1 ? this.cacheMaxSize : this.cacheMaxSize2; | |
| 211 | + | ||
| 212 | + | try { | |
| 213 | + | let { Width: w, Height: h } = img; | |
| 214 | + | ||
| 215 | + | // Scale image | |
| 216 | + | if (w > this.imgMaxWidth || h > this.imgMaxHeight) { | |
| 217 | + | const scaleFactor = Math.max(w / this.imgMaxWidth, h / this.imgMaxHeight); | |
| 218 | + | w /= scaleFactor; | |
| 219 | + | h /= scaleFactor; | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | const file = fso.GetFile(location); | |
| 223 | + | cache[location] = { image: img.Resize(w, h), filesize: file.Size }; | |
| 224 | + | img = null; | |
| 225 | + | ||
| 226 | + | // Update cache order | |
| 227 | + | const pathIndex = cacheIndexes.indexOf(location); | |
| 228 | + | if (pathIndex !== -1) { | |
| 229 | + | cacheIndexes.splice(pathIndex, 1); // Remove from middle of cache and put on end | |
| 230 | + | } | |
| 231 | + | cacheIndexes.push(location); | |
| 232 | + | ||
| 233 | + | // Maintain cache size | |
| 234 | + | if (cacheIndexes.length > cacheMaxSize) { | |
| 235 | + | const remove = cacheIndexes.shift(); | |
| 236 | + | grm.debug.debugLog('Art cache => Removing img from cache:', remove); | |
| 237 | + | delete cache[remove]; | |
| 238 | + | } | |
| 239 | + | } catch (e) { | |
| 240 | + | // Do not console.log inverted band logo and label images in the process of being created | |
| 241 | + | grm.ui.bandLogoInverted && console.log(`\nArt cache => <Error: Image could not be properly parsed: ${location}>\n`); | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | return cache[location] ? cache[location].image : img; | |
| 245 | + | } | |
| 246 | + | ||
| 247 | + | /** | |
| 248 | + | * Completely clears all cached entries and releases memory held by scaled bitmaps. | |
| 249 | + | */ | |
| 250 | + | clear() { | |
| 251 | + | if (grCfg.settings.showDebugLog) { | |
| 252 | + | grm.debug.debugLog(`Art cache => Total cache size for Cache 1: ${this.getTotalCacheSize(1, false)}`); | |
| 253 | + | grm.debug.debugLog(`Art cache => Total cache size for Cache 2: ${this.getTotalCacheSize(2, false)}`); | |
| 254 | + | grm.debug.debugLog(`Art cache => Total cache size cleared: ${this.getTotalCacheSize(0, false)}`); | |
| 255 | + | } | |
| 256 | + | ||
| 257 | + | const clearCache = (cacheIndexes, cache) => { | |
| 258 | + | for (const index of cacheIndexes) { | |
| 259 | + | delete cache[index]; | |
| 260 | + | } | |
| 261 | + | cacheIndexes.length = 0; | |
| 262 | + | }; | |
| 263 | + | ||
| 264 | + | clearCache(this.cacheIndexes, this.cache); | |
| 265 | + | clearCache(this.cacheIndexes2, this.cache2); | |
| 266 | + | } | |
| 267 | + | // #endregion | |
| 268 | + | } | |
| 269 | + | ||
| 270 | + | ||
| 271 | + | /** | |
| 272 | + | * A class that creates background images for the Playlist or Library. | |
| 273 | + | */ | |
| 274 | + | class BackgroundImage { | |
| 275 | + | /** | |
| 276 | + | * Creates the `BackgroundImage` instance. | |
| 277 | + | */ | |
| 278 | + | constructor() { | |
| 279 | + | // * BACKGROUND PANEL IMAGES * // | |
| 280 | + | // #region BACKGROUND PANEL IMAGES | |
| 281 | + | /** @public @type {GdiBitmap|null} The background image used for the Playlist. */ | |
| 282 | + | this.playlistBgImg = null; | |
| 283 | + | /** @public @type {GdiBitmap|null} The background image used for the Library. */ | |
| 284 | + | this.libraryBgImg = null; | |
| 285 | + | /** @public @type {GdiBitmap|null} The background image used for the Lyrics. */ | |
| 286 | + | this.lyricsBgImg = null; | |
| 287 | + | // #endregion | |
| 288 | + | ||
| 289 | + | // * ARTIST IMAGES * // | |
| 290 | + | // #region ARTIST IMAGES | |
| 291 | + | /** @public @type {GdiBitmap|null} The artist background image of the biography. */ | |
| 292 | + | this.artistBgImg = null; | |
| 293 | + | /** @public @type {GdiBitmap[]} The artist list of background images. */ | |
| 294 | + | this.artistImgList = []; | |
| 295 | + | /** @public @type {number} The artist index of the currently displayed background image for the Playlist. */ | |
| 296 | + | this.artistIdxPlaylist = 0; | |
| 297 | + | /** @public @type {number} The artist index of the currently displayed background image for the Library. */ | |
| 298 | + | this.artistIdxLibrary = 0; | |
| 299 | + | /** @public @type {number} The artist index of the currently displayed background image for the Lyrics. */ | |
| 300 | + | this.artistIdxLyrics = 0; | |
| 301 | + | /** @public @type {number} The artist index of the cached biography artist image for the Playlist. */ | |
| 302 | + | this.artistIdxCachedPlaylist = -1; | |
| 303 | + | /** @public @type {number} The artist index of the cached biography artist image for the Library. */ | |
| 304 | + | this.artistIdxCachedLibrary = -1; | |
| 305 | + | /** @public @type {number} The artist index of the cached biography artist image for the Lyrics. */ | |
| 306 | + | this.artistIdxCachedLyrics = -1; | |
| 307 | + | // #endregion | |
| 308 | + | ||
| 309 | + | // * ALBUM IMAGES * // | |
| 310 | + | // #region ALBUM IMAGES | |
| 311 | + | /** @public @type {GdiBitmap|null} The album background image. */ | |
| 312 | + | this.albumBgImg = null; | |
| 313 | + | /** @public @type {GdiBitmap[]} The album list of background images. */ | |
| 314 | + | this.albumImgList = []; | |
| 315 | + | /** @public @type {number[]} The album art image index: 0 for Front, 1 for Back, and 4 for Artist. */ | |
| 316 | + | this.albumArtIdx = [0, 1, 4]; | |
| 317 | + | /** @public @type {number} The album index of the currently displayed background image for the Playlist. */ | |
| 318 | + | this.albumIdxPlaylist = 0; | |
| 319 | + | /** @public @type {number} The album index of the currently displayed background image for the Library. */ | |
| 320 | + | this.albumIdxLibrary = 0; | |
| 321 | + | /** @public @type {number} The album index of the currently displayed background image for the Lyrics. */ | |
| 322 | + | this.albumIdxLyrics = 0; | |
| 323 | + | /** @public @type {number} The album index of the cached album background image for the Playlist. */ | |
| 324 | + | this.albumIdxCachedPlaylist = -1; | |
| 325 | + | /** @public @type {number} The album index of the cached album background image for the Library. */ | |
| 326 | + | this.albumIdxCachedLibrary = -1; | |
| 327 | + | /** @public @type {number} The album index of the cached album background image for the Lyrics. */ | |
| 328 | + | this.albumIdxCachedLyrics = -1; | |
| 329 | + | // #endregion | |
| 330 | + | ||
| 331 | + | // * CUSTOM IMAGES * // | |
| 332 | + | // #region CUSTOM IMAGES | |
| 333 | + | /** @public @type {GdiBitmap|null} The custom background image. */ | |
| 334 | + | this.customBgImg = null; | |
| 335 | + | /** @public @type {GdiBitmap[]} The custom list of custom background images. */ | |
| 336 | + | this.customImgList = []; | |
| 337 | + | /** @public @type {number} The custom index of the currently displayed custom background image for the Playlist. */ | |
| 338 | + | this.customIdxPlaylist = 0; | |
| 339 | + | /** @public @type {number} The custom index of the currently displayed custom background image for the Library. */ | |
| 340 | + | this.customIdxLibrary = 0; | |
| 341 | + | /** @public @type {number} The custom index of the currently displayed custom background image for the Lyrics. */ | |
| 342 | + | this.customIdxLyrics = 0; | |
| 343 | + | /** @public @type {number} The custom index of the cached custom background image for the Playlist. */ | |
| 344 | + | this.customIdxCachedPlaylist = -1; | |
| 345 | + | /** @public @type {number} The custom index of the cached custom background image for the Library. */ | |
| 346 | + | this.customIdxCachedLibrary = -1; | |
| 347 | + | /** @public @type {number} The custom index of the cached custom background image for the Lyrics. */ | |
| 348 | + | this.customIdxCachedLyrics = -1; | |
| 349 | + | // #endregion | |
| 350 | + | ||
| 351 | + | // * STATE * // | |
| 352 | + | // #region STATE | |
| 353 | + | /** @public @type {Object} The background image fetching state for the Playlist, Library, and Lyrics. */ | |
| 354 | + | this.imgFetching = {}; | |
| 355 | + | /** @public @type {Object} The background image cycle intervals for the Playlist, Library, and Lyrics. */ | |
| 356 | + | this.imgCycleIntervals = {}; | |
| 357 | + | // #endregion | |
| 358 | + | ||
| 359 | + | // * INITIALIZATION * // | |
| 360 | + | // #region INITIALIZATION | |
| 361 | + | this.initBgImageCycle(); | |
| 362 | + | // #endregion | |
| 363 | + | } | |
| 364 | + | ||
| 365 | + | // * PUBLIC METHODS * // | |
| 366 | + | // #region PUBLIC METHODS | |
| 367 | + | /** | |
| 368 | + | * Draws an artist, album or custom image on the Playlist or Library's background. | |
| 369 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 370 | + | * @param {object} img - The image object containing the image and other properties. | |
| 371 | + | * @param {string} scale - The scale mode ("default", "filled" or "stretched") to apply to the image. | |
| 372 | + | * @param {number} x - The x-coordinate where the image should be drawn. | |
| 373 | + | * @param {number} y - The y-coordinate where the image should be drawn. | |
| 374 | + | * @param {number} w - The width of the area to draw the image. | |
| 375 | + | * @param {number} h - The height of the area to draw the image. | |
| 376 | + | * @param {number} opacity - The opacity level to apply to the image. | |
| 377 | + | * @param {boolean} mask - Whether to apply a mask to the image. | |
| 378 | + | * @param {number} maskOffsetY - The y-offset for the mask. | |
| 379 | + | * @param {number} maskHeight - The height of the mask. | |
| 380 | + | */ | |
| 381 | + | drawBgImage(gr, img, scale, x, y, w, h, opacity, mask, maskOffsetY, maskHeight) { | |
| 382 | + | if (!img || !img.image) return; | |
| 383 | + | ||
| 384 | + | if (!img.scaled || img.changed) { | |
| 385 | + | img.scaled = ScaleImage(img.image, scale, x, y, w, h, 0, 0, img.image.Width, img.image.Height); | |
| 386 | + | } | |
| 387 | + | ||
| 388 | + | if (mask && (!img.masked || img.changed)) { | |
| 389 | + | img.masked = MaskImage(img.scaled, 0, maskOffsetY, img.scaled.Width, img.scaled.Height - maskHeight); | |
| 390 | + | } | |
| 391 | + | ||
| 392 | + | const finalImage = mask ? img.masked : img.scaled; | |
| 393 | + | gr.DrawImage(finalImage, x, y, w, h, 0, 0, finalImage.Width, finalImage.Height, 0, opacity); | |
| 394 | + | ||
| 395 | + | img.changed = false; | |
| 396 | + | } | |
| 397 | + | ||
| 398 | + | /** | |
| 399 | + | * Initializes the current background image by clearing the relevant caches and fetching a new image based on the source setting. | |
| 400 | + | * @param {string} panel - The panel ('playlist', 'library', 'lyrics', or false for all) in which the image is being requested. | |
| 401 | + | * @param {boolean} [clearCache] - Whether to clear the background image cache. | |
| 402 | + | * | |
| 403 | + | * The background image cache should be cleared when: | |
| 404 | + | * - The background image source is updated, clearing the previous source. | |
| 405 | + | * - The playback starts a new album, clearing the previous images. | |
| 406 | + | * - The player size changes, requiring a new scaling for the images. | |
| 407 | + | */ | |
| 408 | + | initBgImage(panel, clearCache) { | |
| 409 | + | if (!grSet.playlistBgImg && !grSet.libraryBgImg && !grSet.lyricsBgImg) return; | |
| 410 | + | ||
| 411 | + | if (clearCache) this.clearBgImageCache(); | |
| 412 | + | ||
| 413 | + | this.handleBgImageIndex(panel, 'getIndexes'); | |
| 414 | + | ||
| 415 | + | const displayPanel = { | |
| 416 | + | playlist: grm.ui.displayPlaylist, | |
| 417 | + | library: grm.ui.displayLibrary, | |
| 418 | + | lyrics: grm.ui.displayLyrics | |
| 419 | + | }; | |
| 420 | + | ||
| 421 | + | const panelSize = { | |
| 422 | + | playlist: [pl.playlist.x - SCALE(1), pl.playlist.y - pl.plman.h, pl.playlist.w + SCALE(2), pl.playlist.h + pl.plman.h * 2], | |
| 423 | + | library: [lib.ui.x, lib.ui.y, lib.ui.w, lib.ui.h], | |
| 424 | + | lyrics: [0, grm.ui.topMenuHeight, grm.ui.ww, grm.ui.wh - grm.ui.topMenuHeight - grm.ui.lowerBarHeight] | |
| 425 | + | }; | |
| 426 | + | ||
| 427 | + | const panelToProcess = panel ? [panel] : ['playlist', 'library', 'lyrics'].filter(p => grSet[`${p}BgImg`]); | |
| 428 | + | ||
| 429 | + | for (const p of panelToProcess) { | |
| 430 | + | this.getBgImage(p).then(img => { | |
| 431 | + | this[`${p}BgImg`] = img; | |
| 432 | + | this.handleBgImageIndex(p, 'setIndexes'); | |
| 433 | + | if (displayPanel[p]) window.RepaintRect(...panelSize[p]); | |
| 434 | + | }); | |
| 435 | + | } | |
| 436 | + | } | |
| 437 | + | ||
| 438 | + | /** | |
| 439 | + | * Initializes or clears the cycling of background images. | |
| 440 | + | */ | |
| 441 | + | initBgImageCycle() { | |
| 442 | + | for (const panel of ['playlist', 'library', 'lyrics']) { | |
| 443 | + | clearInterval(this.imgCycleIntervals[panel]); | |
| 444 | + | this.imgCycleIntervals[panel] = null; | |
| 445 | + | ||
| 446 | + | const enabled = grSet[`${panel}BgImg`]; | |
| 447 | + | const cycle = grSet[`${panel}BgImgCycle`]; | |
| 448 | + | const cycleTime = grSet[`${panel}BgImgCycleTime`]; | |
| 449 | + | ||
| 450 | + | if (!enabled || !cycle) continue; | |
| 451 | + | ||
| 452 | + | grm.debug.debugLog(`\n>>> initImage => initImgCycle => Panel: ${CapitalizeString(panel)} - Cycle time: ${cycleTime} seconds <<<\n`); | |
| 453 | + | this.imgCycleIntervals[panel] = setInterval(() => { | |
| 454 | + | this.cycleBgImage(panel, 1); | |
| 455 | + | }, cycleTime * 1000); | |
| 456 | + | } | |
| 457 | + | } | |
| 458 | + | ||
| 459 | + | /** | |
| 460 | + | * Cycles the background image for the specified panel. | |
| 461 | + | * @param {string} panel - The panel ('playlist', 'library', 'lyrics') in which the image should be cycled. | |
| 462 | + | * @param {number} direction - The direction to cycle the images (1 for next, -1 for previous). | |
| 463 | + | */ | |
| 464 | + | cycleBgImage(panel, direction) { | |
| 465 | + | const imgKey = this.getBgImageSourceKeys(panel); | |
| 466 | + | const imgList = Array.isArray(imgKey.imgList) ? imgKey.imgList : this[imgKey.imgList]; | |
| 467 | + | ||
| 468 | + | if (!imgList.length) { | |
| 469 | + | this[imgKey.imgIdx] = (this[imgKey.imgIdx] + direction + this.albumArtIdx.length) % this.albumArtIdx.length; | |
| 470 | + | const imgIdx = this.albumArtIdx[this[imgKey.imgIdx]]; | |
| 471 | + | this.handleBgImageIndex(panel, 'setIndexes'); | |
| 472 | + | ||
| 473 | + | this.fetchBgImageEmbedded(panel, imgIdx, imgKey.bgImg, imgKey.bgImgIdx).then(() => { | |
| 474 | + | this.initBgImage(panel); | |
| 475 | + | }); | |
| 476 | + | ||
| 477 | + | return; | |
| 478 | + | } | |
| 479 | + | ||
| 480 | + | if (imgList.length <= 1) return; | |
| 481 | + | ||
| 482 | + | this[imgKey.imgIdx] = (this[imgKey.imgIdx] + direction + imgList.length) % imgList.length; | |
| 483 | + | this.handleBgImageIndex(panel, 'setIndexes'); | |
| 484 | + | ||
| 485 | + | this.fetchBgImage(panel, imgList, imgKey.imgIdx, imgKey.bgImg, imgKey.bgImgIdx).then(() => { | |
| 486 | + | this.initBgImage(panel); | |
| 487 | + | }); | |
| 488 | + | } | |
| 489 | + | ||
| 490 | + | /** | |
| 491 | + | * Checks if the background image is cached, and updates the relevant properties if it is. | |
| 492 | + | * @param {string} panel - The panel ('playlist', 'library', 'lyrics') in which the image is being requested. | |
| 493 | + | * @param {string|string[]} imgList - The name of the property in `this` that contains the image list, or the image list array itself. | |
| 494 | + | * @param {string} imgIdx - The name of the property in `this` that contains the current image index. | |
| 495 | + | * @param {string} bgImg - The name of the property in `this` that contains the cached image. | |
| 496 | + | * @param {string} bgImgIdx - The name of the property in `this` that contains the cached image index. | |
| 497 | + | * @returns {{image: GdiBitmap|null, changed: boolean}} The background image and a flag indicating if it has changed. | |
| 498 | + | */ | |
| 499 | + | checkBgImageCache(panel, imgList, imgIdx, bgImg, bgImgIdx) { | |
| 500 | + | const imgArray = Array.isArray(imgList) ? imgList : this[imgList]; | |
| 501 | + | const imgPath = imgArray[this[imgIdx]]; | |
| 502 | + | const imgCached = grm.artCache.getImage(imgPath); | |
| 503 | + | ||
| 504 | + | if (imgCached) { | |
| 505 | + | this[bgImg] = imgCached; | |
| 506 | + | this[bgImgIdx] = this[imgIdx]; | |
| 507 | + | return { image: imgCached, changed: false }; | |
| 508 | + | } | |
| 509 | + | return { image: null, changed: true }; | |
| 510 | + | } | |
| 511 | + | ||
| 512 | + | /** | |
| 513 | + | * Clears the background image cache. | |
| 514 | + | */ | |
| 515 | + | clearBgImageCache() { | |
| 516 | + | this.playlistBgImg = null; | |
| 517 | + | this.libraryBgImg = null; | |
| 518 | + | this.lyricsBgImg = null; | |
| 519 | + | this.artistBgImg = null; | |
| 520 | + | this.artistImgList = []; | |
| 521 | + | this.albumBgImg = null; | |
| 522 | + | this.albumImgList = []; | |
| 523 | + | this.customBgImg = null; | |
| 524 | + | this.customImgList = []; | |
| 525 | + | grm.debug.debugLog('Main cache => Background image cache cleared'); | |
| 526 | + | } | |
| 527 | + | ||
| 528 | + | /** | |
| 529 | + | * Fetches a background image, either from cache or asynchronously, and updates the relevant properties. | |
| 530 | + | * @param {string} panel - The panel ('playlist', 'library', 'lyrics') in which the image is being requested. | |
| 531 | + | * @param {string|string[]} imgList - The name of the property in `this` that contains the image list, or the image list array itself. | |
| 532 | + | * @param {string} imgIdx - The name of the property in `this` that contains the current image index. | |
| 533 | + | * @param {string} bgImg - The name of the property in `this` that contains the cached image. | |
| 534 | + | * @param {string} bgImgIdx - The name of the property in `this` that contains the cached image index. | |
| 535 | + | * @returns {Promise<{image: GdiBitmap|null, changed: boolean}>} The background image and a flag indicating if it has changed. | |
| 536 | + | */ | |
| 537 | + | fetchBgImage(panel, imgList, imgIdx, bgImg, bgImgIdx) { | |
| 538 | + | if (this.imgFetching[panel]) { | |
| 539 | + | return Promise.resolve({ image: null, changed: false }); | |
| 540 | + | } | |
| 541 | + | ||
| 542 | + | this.imgFetching[panel] = true; | |
| 543 | + | ||
| 544 | + | const imgArray = Array.isArray(imgList) ? imgList : this[imgList]; | |
| 545 | + | const imgIdxLocal = (this[imgIdx] >= imgArray.length) ? 0 : this[imgIdx]; | |
| 546 | + | const imgPathIdx = imgArray[imgIdxLocal]; | |
| 547 | + | ||
| 548 | + | return gdi.LoadImageAsyncV2(window.ID, imgPathIdx) | |
| 549 | + | .then(img => { | |
| 550 | + | this[bgImg] = grm.artCache.encache(img, imgPathIdx); | |
| 551 | + | this[bgImgIdx] = imgIdxLocal; | |
| 552 | + | return { image: img, changed: true }; | |
| 553 | + | }) | |
| 554 | + | .catch(error => { | |
| 555 | + | console.log(`\n>>> Background Image => fetchBgImage => <Error: Image could not be properly parsed: ${panel}:>\n`, error); | |
| 556 | + | return { image: null, changed: false }; | |
| 557 | + | }) | |
| 558 | + | .finally(() => { | |
| 559 | + | this.imgFetching[panel] = false; | |
| 560 | + | }); | |
| 561 | + | } | |
| 562 | + | ||
| 563 | + | /** | |
| 564 | + | * Fetches and caches embedded album art if available. | |
| 565 | + | * @param {string} panel - The panel ('playlist', 'library', 'lyrics') in which the image is being requested. | |
| 566 | + | * @param {string} imgIdx - The name of the property in `this` that contains the current image index. | |
| 567 | + | * @param {string} bgImg - The name of the property in `this` that contains the cached image. | |
| 568 | + | * @param {string} bgImgIdx - The name of the property in `this` that contains the cached image index. | |
| 569 | + | * @returns {Promise<{image: GdiBitmap|null, changed: boolean}>} The embedded album art image and a flag indicating if it has changed. | |
| 570 | + | */ | |
| 571 | + | fetchBgImageEmbedded(panel, imgIdx, bgImg, bgImgIdx) { | |
| 572 | + | if (this.imgFetching[panel]) { | |
| 573 | + | return Promise.resolve({ image: null, changed: false }); | |
| 574 | + | } | |
| 575 | + | ||
| 576 | + | this.imgFetching[panel] = true; | |
| 577 | + | ||
| 578 | + | try { | |
| 579 | + | const metadb = grm.ui.initMetadb(); | |
| 580 | + | ||
| 581 | + | if (!metadb) { | |
| 582 | + | return Promise.resolve({ image: null, changed: false }); | |
| 583 | + | } | |
| 584 | + | ||
| 585 | + | const imgIdxLocal = this.albumArtIdx.includes(imgIdx) ? imgIdx : 0; | |
| 586 | + | const albumArt = utils.GetAlbumArtV2(metadb, imgIdxLocal); | |
| 587 | + | ||
| 588 | + | if (albumArt) { | |
| 589 | + | this[bgImg] = grm.artCache.encache(albumArt, imgIdxLocal); | |
| 590 | + | this[bgImgIdx] = imgIdxLocal; | |
| 591 | + | return Promise.resolve({ image: albumArt, changed: true }); | |
| 592 | + | } | |
| 593 | + | ||
| 594 | + | return Promise.resolve({ image: null, changed: false }); | |
| 595 | + | } | |
| 596 | + | catch (error) { | |
| 597 | + | console.log(`\n>>> Background Image => fetchBgImageEmbedded => <Error: Image could not be properly parsed: ${panel}:>\n`, error); | |
| 598 | + | return Promise.resolve({ image: null, changed: false }); | |
| 599 | + | } | |
| 600 | + | finally { | |
| 601 | + | this.imgFetching[panel] = false; | |
| 602 | + | } | |
| 603 | + | } | |
| 604 | + | ||
| 605 | + | /** | |
| 606 | + | * Gets the background image based on the source setting. | |
| 607 | + | * @param {string} panel - The panel ('playlist', 'library', 'lyrics') in which the image is being requested. | |
| 608 | + | * @returns {{image: GdiBitmap|null, changed: boolean}} The background image and a flag indicating if it has changed. | |
| 609 | + | */ | |
| 610 | + | getBgImage(panel) { | |
| 611 | + | const { imgType, imgList, imgIdx, bgImg, bgImgIdx } = this.getBgImageSourceKeys(panel); | |
| 612 | + | const bgImageFormats = ParseStringToRegExp(grCfg.artworkImageFormats.bgImage); | |
| 613 | + | const bgImagePattern = this.getBgImagePatterns(panel); | |
| 614 | + | ||
| 615 | + | this[imgList] = grm.ui.getImagePathList(imgType, grm.ui.initMetadb(), bgImagePattern).filter(path => bgImageFormats.test(path)); | |
| 616 | + | ||
| 617 | + | if (!this[imgList].length) { | |
| 618 | + | const embeddedIdx = this.albumArtIdx[this[imgIdx]]; | |
| 619 | + | return this.fetchBgImageEmbedded(panel, embeddedIdx, bgImg, bgImgIdx); | |
| 620 | + | } | |
| 621 | + | ||
| 622 | + | const { image, changed } = this.checkBgImageCache(panel, imgList, imgIdx, bgImg, bgImgIdx); | |
| 623 | + | if (image) { | |
| 624 | + | return Promise.resolve({ image, changed }); | |
| 625 | + | } | |
| 626 | + | ||
| 627 | + | return this.fetchBgImage(panel, imgList, imgIdx, bgImg, bgImgIdx); | |
| 628 | + | } | |
| 629 | + | ||
| 630 | + | /** | |
| 631 | + | * Gets the background image pattern for a given panel type. | |
| 632 | + | * @param {string} panel - The panel type, which can be 'playlist', 'library', or 'lyrics'. | |
| 633 | + | * @returns {RegExp} The pattern for the specified panel type. | |
| 634 | + | */ | |
| 635 | + | getBgImagePatterns(panel) { | |
| 636 | + | const bgImagePatterns = { | |
| 637 | + | playlist: grSet.playlistBgImgAlbumArtFilter && ParseStringToRegExp(grCfg.artworkPatterns.playlistBgAlbumArt), | |
| 638 | + | library: grSet.libraryBgImgAlbumArtFilter && ParseStringToRegExp(grCfg.artworkPatterns.libraryBgAlbumArt), | |
| 639 | + | lyrics: grSet.lyricsBgImgAlbumArtFilter && ParseStringToRegExp(grCfg.artworkPatterns.lyricsBgAlbumArt) | |
| 640 | + | }; | |
| 641 | + | ||
| 642 | + | return bgImagePatterns[panel]; | |
| 643 | + | } | |
| 644 | + | ||
| 645 | + | /** | |
| 646 | + | * Retrieves the background image source keys based on the specified panel. | |
| 647 | + | * @param {string} panel - The panel ('playlist', 'library', 'lyrics') whose background image source keys are to be retrieved. | |
| 648 | + | * @returns {object} An object containing the image type, list, index, and background image keys. | |
| 649 | + | */ | |
| 650 | + | getBgImageSourceKeys(panel) { | |
| 651 | + | const Panel = CapitalizeString(panel); | |
| 652 | + | ||
| 653 | + | const imgSrcKeys = { | |
| 654 | + | artist: { imgType: 'artistArt', imgList: 'artistImgList', imgIdx: `artistIdx${Panel}`, bgImg: 'artistBgImg', bgImgIdx: `artistIdxCached${Panel}` }, | |
| 655 | + | album: { imgType: 'albumArt', imgList: 'albumImgList', imgIdx: `albumIdx${Panel}`, bgImg: 'albumBgImg', bgImgIdx: `albumIdxCached${Panel}` }, | |
| 656 | + | custom: { imgType: 'customArt', imgList: 'customImgList', imgIdx: `customIdx${Panel}`, bgImg: 'customBgImg', bgImgIdx: `customIdxCached${Panel}` } | |
| 657 | + | }; | |
| 658 | + | ||
| 659 | + | return imgSrcKeys[grSet[`${panel}BgImgSource`]]; | |
| 660 | + | } | |
| 661 | + | ||
| 662 | + | /** | |
| 663 | + | * Handles background image indexes for the specified panel. | |
| 664 | + | * @param {string} panel - The panel ('playlist', 'library', 'lyrics', or false for all) whose background image indexes are to be retrieved. | |
| 665 | + | * @param {string} action - The action to perform: 'getIndexes', 'setIndexes', or 'clearIndexes'. | |
| 666 | + | */ | |
| 667 | + | handleBgImageIndex(panel, action) { | |
| 668 | + | const panels = panel ? [panel] : ['playlist', 'library', 'lyrics']; | |
| 669 | + | ||
| 670 | + | const indexFields = { | |
| 671 | + | playlist: ['bgImgArtistIdxPlaylist', 'bgImgAlbumIdxPlaylist', 'bgImgCustomIdxPlaylist'], | |
| 672 | + | library: ['bgImgArtistIdxLibrary', 'bgImgAlbumIdxLibrary', 'bgImgCustomIdxLibrary'], | |
| 673 | + | lyrics: ['bgImgArtistIdxLyrics', 'bgImgAlbumIdxLyrics', 'bgImgCustomIdxLyrics'] | |
| 674 | + | }; | |
| 675 | + | ||
| 676 | + | const actions = { | |
| 677 | + | getIndexes: (Panel, artistIdx, albumIdx, customIdx) => { | |
| 678 | + | this[`artistIdx${Panel}`] = grSet[artistIdx]; | |
| 679 | + | this[`albumIdx${Panel}`] = grSet[albumIdx]; | |
| 680 | + | this[`customIdx${Panel}`] = grSet[customIdx]; | |
| 681 | + | }, | |
| 682 | + | setIndexes: (Panel, artistIdx, albumIdx, customIdx) => { | |
| 683 | + | grSet[artistIdx] = this[`artistIdx${Panel}`]; | |
| 684 | + | grSet[albumIdx] = this[`albumIdx${Panel}`]; | |
| 685 | + | grSet[customIdx] = this[`customIdx${Panel}`]; | |
| 686 | + | }, | |
| 687 | + | clearIndexes: (Panel) => { | |
| 688 | + | this[`artistIdx${Panel}`] = 0; | |
| 689 | + | this[`artistIdxCached${Panel}`] = -1; | |
| 690 | + | this[`albumIdx${Panel}`] = 0; | |
| 691 | + | this[`albumIdxCached${Panel}`] = -1; | |
| 692 | + | this[`customIdx${Panel}`] = 0; | |
| 693 | + | this[`customIdxCached${Panel}`] = -1; | |
| 694 | + | } | |
| 695 | + | }; | |
| 696 | + | ||
| 697 | + | for (const panel of panels) { | |
| 698 | + | if (actions[action]) { | |
| 699 | + | actions[action](CapitalizeString(panel), ...indexFields[panel]); | |
| 700 | + | } | |
| 701 | + | } | |
| 702 | + | } | |
| 703 | + | // #endregion | |
| 704 | + | } | |
| 705 | + | ||
| 706 | + | ||
| 707 | + | /////////////////////////// | |
| 708 | + | // * CPU USAGE TRACKER * // | |
| 709 | + | /////////////////////////// | |
| 710 | + | /** | |
| 711 | + | * A class that tracks and monitors CPU usage. | |
| 712 | + | */ | |
| 713 | + | class CPUTracker { | |
| 714 | + | /** | |
| 715 | + | * Create the CPUTracker instance. | |
| 716 | + | * @param {Function} onChangeCallback - A callback function to call when CPU usage changes. | |
| 717 | + | */ | |
| 718 | + | constructor(onChangeCallback) { | |
| 719 | + | /** @private @type {number} */ | |
| 720 | + | this.cpuUsage = 0; | |
| 721 | + | /** @private @type {number} */ | |
| 722 | + | this.guiCpuUsage = 0; | |
| 723 | + | /** @private @type {?number} */ | |
| 724 | + | this.cpuTrackerTimer = null; | |
| 725 | + | /** @private @type {Function} */ | |
| 726 | + | this.onChangeCallback = onChangeCallback; | |
| 727 | + | /** @private @type {{[key: string]: {sampleCount: number, currentSampleCount: number, resetSampleCount: number, acumUsage: number, averageUsage: number}}} */ | |
| 728 | + | this.usage = { | |
| 729 | + | idle: { | |
| 730 | + | sampleCount: 30, | |
| 731 | + | currentSampleCount: 0, | |
| 732 | + | resetSampleCount: 0, | |
| 733 | + | acumUsage: 0, | |
| 734 | + | averageUsage: 0 | |
| 735 | + | }, | |
| 736 | + | playing: { | |
| 737 | + | sampleCount: 30, | |
| 738 | + | currentSampleCount: 0, | |
| 739 | + | resetSampleCount: 0, | |
| 740 | + | acumUsage: 0, | |
| 741 | + | averageUsage: 0 | |
| 742 | + | } | |
| 743 | + | }; | |
| 744 | + | } | |
| 745 | + | ||
| 746 | + | /** | |
| 747 | + | * Gets the current CPU usage. | |
| 748 | + | * @returns {number} The current CPU usage. | |
| 749 | + | */ | |
| 750 | + | getCpuUsage() { | |
| 751 | + | return this.cpuUsage; | |
| 752 | + | } | |
| 753 | + | ||
| 754 | + | /** | |
| 755 | + | * Gets the current GUI CPU usage. | |
| 756 | + | * @returns {number} The current GUI CPU usage. | |
| 757 | + | */ | |
| 758 | + | getGuiCpuUsage() { | |
| 759 | + | return this.guiCpuUsage; | |
| 760 | + | } | |
| 761 | + | ||
| 762 | + | /** | |
| 763 | + | * Starts the CPU usage monitoring process. | |
| 764 | + | */ | |
| 765 | + | start() { | |
| 766 | + | if (this.cpuTrackerTimer) return; | |
| 767 | + | ||
| 768 | + | this.cpuTrackerTimer = setInterval(() => { | |
| 769 | + | const floatUsage = Math.random() * 100; // Simulated CPU usage | |
| 770 | + | const isPlaying = Math.random() > 0.5; // Simulated playback status | |
| 771 | + | const isPaused = Math.random() > 0.8; | |
| 772 | + | const usageType = isPlaying && !isPaused ? 'playing' : 'idle'; | |
| 773 | + | ||
| 774 | + | this.updateUsage(usageType, floatUsage); | |
| 775 | + | ||
| 776 | + | const baseLine = this.usage[usageType].averageUsage; | |
| 777 | + | this.cpuUsage = floatUsage.toFixed(1); | |
| 778 | + | let usageDiff = Math.max((floatUsage - baseLine), 0); | |
| 779 | + | usageDiff = (usageDiff <= 0.5 ? 0 : usageDiff); // Suppress low spikes | |
| 780 | + | this.guiCpuUsage = usageDiff.toFixed(1); | |
| 781 | + | ||
| 782 | + | if (this.onChangeCallback) { | |
| 783 | + | this.onChangeCallback(); | |
| 784 | + | } | |
| 785 | + | }, 1000); | |
| 786 | + | } | |
| 787 | + | ||
| 788 | + | /** | |
| 789 | + | * Stops the CPU usage monitoring and resets usage statistics. | |
| 790 | + | */ | |
| 791 | + | stop() { | |
| 792 | + | if (this.cpuTrackerTimer) { | |
| 793 | + | clearInterval(this.cpuTrackerTimer); | |
| 794 | + | this.cpuTrackerTimer = undefined; | |
| 795 | + | } | |
| 796 | + | ||
| 797 | + | this.resetUsage('idle'); | |
| 798 | + | this.resetUsage('playing'); | |
| 799 | + | } | |
| 800 | + | ||
| 801 | + | /** | |
| 802 | + | * Recalculates the average CPU usage based on a new sample. | |
| 803 | + | * @param {string} type - The type of CPU usage to recalculate ('idle' or 'playing'). | |
| 804 | + | * @param {number} currentUsage - The new CPU usage sample. | |
| 805 | + | */ | |
| 806 | + | recalcAvg(type, currentUsage) { | |
| 807 | + | const usageState = this.usage[type]; | |
| 808 | + | ||
| 809 | + | if (usageState.currentSampleCount < usageState.sampleCount) { | |
| 810 | + | usageState.acumUsage += currentUsage; | |
| 811 | + | usageState.currentSampleCount++; | |
| 812 | + | usageState.averageUsage = usageState.acumUsage / usageState.currentSampleCount; | |
| 813 | + | return; | |
| 814 | + | } | |
| 815 | + | ||
| 816 | + | usageState.averageUsage -= usageState.averageUsage / usageState.sampleCount; | |
| 817 | + | usageState.averageUsage += currentUsage / usageState.sampleCount; | |
| 818 | + | } | |
| 819 | + | ||
| 820 | + | /** | |
| 821 | + | * Resets the CPU usage data for a specified type. | |
| 822 | + | * @param {string} type - The type of CPU usage to reset ('idle' or 'playing'). | |
| 823 | + | */ | |
| 824 | + | resetUsage(type) { | |
| 825 | + | const usageState = this.usage[type]; | |
| 826 | + | usageState.currentSampleCount = 0; | |
| 827 | + | usageState.resetSampleCount = 0; | |
| 828 | + | usageState.acumUsage = 0; | |
| 829 | + | usageState.averageUsage = 0; | |
| 830 | + | } | |
| 831 | + | ||
| 832 | + | /** | |
| 833 | + | * Updates the CPU usage data based on new sample. | |
| 834 | + | * @param {string} type - The type of CPU usage to update ('idle' or 'playing'). | |
| 835 | + | * @param {number} currentUsage - The current CPU usage to update. | |
| 836 | + | */ | |
| 837 | + | updateUsage(type, currentUsage) { | |
| 838 | + | const usageState = this.usage[type]; | |
| 839 | + | ||
| 840 | + | if (usageState.currentSampleCount) { | |
| 841 | + | if (usageState.averageUsage - currentUsage > 2) { | |
| 842 | + | if (usageState.resetSampleCount < 3) { | |
| 843 | + | usageState.resetSampleCount++; | |
| 844 | + | } else { | |
| 845 | + | this.resetUsage(type); | |
| 846 | + | } | |
| 847 | + | } else if (Math.abs(currentUsage - usageState.averageUsage) < 2) { | |
| 848 | + | this.recalcAvg(type, currentUsage); | |
| 849 | + | } | |
| 850 | + | } else { | |
| 851 | + | this.recalcAvg(type, currentUsage); | |
| 852 | + | } | |
| 853 | + | } | |
| 854 | + | } | |
| 855 | + | ||
| 856 | + | ||
| 857 | + | ///////////////// | |
| 858 | + | // * TOOLTIP * // | |
| 859 | + | ///////////////// | |
| 860 | + | /** | |
| 861 | + | * A class that creates or stops the tooltip timer. | |
| 862 | + | */ | |
| 863 | + | class TooltipTimer { | |
| 864 | + | /** | |
| 865 | + | * Creates the `TooltipTimer` instance. | |
| 866 | + | */ | |
| 867 | + | constructor() { | |
| 868 | + | /** @private @type {number|undefined} The timer ID for the tooltip display timeout. */ | |
| 869 | + | this.tooltipTimer = undefined; | |
| 870 | + | /** @private @type {number|undefined} The identifier of the current tooltip caller. */ | |
| 871 | + | this.tooltipCaller = undefined; | |
| 872 | + | } | |
| 873 | + | ||
| 874 | + | // * PUBLIC METHODS * // | |
| 875 | + | // #region PUBLIC METHODS | |
| 876 | + | /** | |
| 877 | + | * Displays the tooltip. | |
| 878 | + | * @param {string} text - The text to show in the tooltip. | |
| 879 | + | * @param {boolean} [force] - Activates the tooltip whether or not text has changed. | |
| 880 | + | */ | |
| 881 | + | displayTooltip(text, force) { | |
| 882 | + | if (grm.ui.ttip && (grm.ui.ttip.Text !== text.toString() || force)) { | |
| 883 | + | grm.ui.ttip.Text = text; | |
| 884 | + | grm.ui.ttip.Activate(); | |
| 885 | + | } | |
| 886 | + | } | |
| 887 | + | ||
| 888 | + | /** | |
| 889 | + | * Starts a tooltip. | |
| 890 | + | * @param {number} id - The id of the caller. | |
| 891 | + | * @param {string} text - The text to show in the tooltip. | |
| 892 | + | */ | |
| 893 | + | start(id, text) { | |
| 894 | + | const oldCaller = this.tooltipCaller; | |
| 895 | + | this.tooltipCaller = id; | |
| 896 | + | ||
| 897 | + | if (!this.tooltipTimer && grm.ui.ttip.Text) { | |
| 898 | + | this.displayTooltip(text, oldCaller !== this.tooltipCaller); | |
| 899 | + | } | |
| 900 | + | else { // * There can be only one tooltip present at all times, so we can kill the timer w/o any worries | |
| 901 | + | if (this.tooltipTimer) { | |
| 902 | + | this.forceStop(); | |
| 903 | + | } | |
| 904 | + | ||
| 905 | + | if (!this.tooltipTimer) { | |
| 906 | + | this.tooltipTimer = setTimeout(() => { | |
| 907 | + | this.displayTooltip(text); | |
| 908 | + | this.tooltipTimer = null; | |
| 909 | + | }, 300); | |
| 910 | + | } | |
| 911 | + | } | |
| 912 | + | } | |
| 913 | + | ||
| 914 | + | /** | |
| 915 | + | * Stops a tooltip. | |
| 916 | + | * @param {number} id - The id of the caller. | |
| 917 | + | */ | |
| 918 | + | stop(id) { | |
| 919 | + | if (this.tooltipCaller === id) { // Do not stop other callers | |
| 920 | + | this.forceStop(); | |
| 921 | + | } | |
| 922 | + | } | |
| 923 | + | ||
| 924 | + | /** | |
| 925 | + | * Forces the tooltip to stop. | |
| 926 | + | */ | |
| 927 | + | forceStop() { | |
| 928 | + | this.displayTooltip(''); | |
| 929 | + | if (!this.tooltipTimer) return; | |
| 930 | + | clearTimeout(this.tooltipTimer); | |
| 931 | + | this.tooltipTimer = null; | |
| 932 | + | this.tooltipCaller = null; | |
| 933 | + | } | |
| 934 | + | // #endregion | |
| 935 | + | } | |
| 936 | + | ||
| 937 | + | ||
| 938 | + | /** | |
| 939 | + | * A class that creates or clears the tooltip text for normal and styled tooltips. | |
| 940 | + | */ | |
| 941 | + | class TooltipHandler { | |
| 942 | + | /** | |
| 943 | + | * Creates the `TooltipHandler` instance. | |
| 944 | + | * Constructs a unique ID and a reference to the TooltipTimer instance. | |
| 945 | + | */ | |
| 946 | + | constructor() { | |
| 947 | + | /** @private @type {number} The unique identifier for this TooltipHandler instance. */ | |
| 948 | + | this.id = Math.ceil(Math.random() * 10000); | |
| 949 | + | /** @private @type {TooltipTimer} A reference to the TooltipTimer instance used to manage tooltip timing. */ | |
| 950 | + | this.timer = new TooltipTimer(); | |
| 951 | + | } | |
| 952 | + | ||
| 953 | + | // * PUBLIC METHODS * // | |
| 954 | + | // #region PUBLIC METHODS | |
| 955 | + | /** | |
| 956 | + | * Shows tooltip after delay (300ms). | |
| 957 | + | * @param {string} text - The text to show in the tooltip. | |
| 958 | + | */ | |
| 959 | + | showDelayed(text) { | |
| 960 | + | grm.ui.styledTooltipText = text; | |
| 961 | + | if (!grSet.showStyledTooltips) { | |
| 962 | + | this.timer.start(this.id, text); | |
| 963 | + | } | |
| 964 | + | } | |
| 965 | + | ||
| 966 | + | /** | |
| 967 | + | * Shows the tooltip immediately. | |
| 968 | + | * @param {string} text - The text to show in the tooltip. | |
| 969 | + | */ | |
| 970 | + | showImmediate(text) { | |
| 971 | + | grm.ui.styledTooltipText = text; | |
| 972 | + | if (!grSet.showStyledTooltips) { | |
| 973 | + | this.timer.stop(this.id); | |
| 974 | + | this.timer.displayTooltip(text); | |
| 975 | + | } | |
| 976 | + | } | |
| 977 | + | ||
| 978 | + | /** | |
| 979 | + | * Clears this tooltip if this handler created it. | |
| 980 | + | */ | |
| 981 | + | clear() { | |
| 982 | + | this.timer.stop(this.id); | |
| 983 | + | } | |
| 984 | + | ||
| 985 | + | /** | |
| 986 | + | * Clears the tooltip regardless of which handler created it. | |
| 987 | + | */ | |
| 988 | + | stop() { | |
| 989 | + | this.timer.forceStop(); | |
| 990 | + | } | |
| 991 | + | // #endregion | |
| 992 | + | } | |
| 993 | + | ||
| 994 | + | ||
| 995 | + | ////////////////////////////// | |
| 996 | + | // * INTERFACE HYPERLINKS * // | |
| 997 | + | ////////////////////////////// | |
| 998 | + | /** | |
| 999 | + | * A class that creates clickable hyperlinks in the Playlist header and in the lower bar. | |
| 1000 | + | */ | |
| 1001 | + | class Hyperlink { | |
| 1002 | + | /** | |
| 1003 | + | * Creates the `Hyperlink` instance. | |
| 1004 | + | * Initializes properties for the text element in the playlist. | |
| 1005 | + | * @param {string} text - The text that will be displayed in the hyperlink. | |
| 1006 | + | * @param {GdiFont} font - The font to use. | |
| 1007 | + | * @param {string} type - The field name which will be searched when clicking on the hyperlink. | |
| 1008 | + | * @param {number} xOffset - The x-offset of the hyperlink. Negative values will be subtracted from the containerWidth to right justify. | |
| 1009 | + | * @param {number} yOffset - The y-offset of the hyperlink. | |
| 1010 | + | * @param {number} containerWidth - The width of the container the hyperlink will be in. Used for right justification purposes. | |
| 1011 | + | * @param {boolean} [inPlaylist] - If the hyperlink is drawing in a scrolling container like a playlist, then it is drawn differently. | |
| 1012 | + | */ | |
| 1013 | + | constructor(text, font, type, xOffset, yOffset, containerWidth, inPlaylist = false) { | |
| 1014 | + | /** @private @type {string} */ | |
| 1015 | + | this.text = text; | |
| 1016 | + | /** @private @type {string} */ | |
| 1017 | + | this.type = type; | |
| 1018 | + | /** @private @type {number} */ | |
| 1019 | + | this.x_offset = xOffset; | |
| 1020 | + | /** @private @type {number} */ | |
| 1021 | + | this.x = xOffset < 0 ? containerWidth + xOffset : xOffset; | |
| 1022 | + | /** @private @type {number} */ | |
| 1023 | + | this.y_offset = yOffset; | |
| 1024 | + | /** @private @type {number} */ | |
| 1025 | + | this.y = yOffset; | |
| 1026 | + | /** @private @type {number} */ | |
| 1027 | + | this.container_w = containerWidth; | |
| 1028 | + | /** @private @type {boolean} */ | |
| 1029 | + | this.state = HyperlinkStates.Normal; | |
| 1030 | + | /** @private @type {boolean} */ | |
| 1031 | + | this.inPlaylist = inPlaylist; | |
| 1032 | + | ||
| 1033 | + | this.setFont(font); | |
| 1034 | + | } | |
| 1035 | + | ||
| 1036 | + | // * PUBLIC METHODS * // | |
| 1037 | + | // #region PUBLIC METHODS | |
| 1038 | + | /** | |
| 1039 | + | * Draws the hyperlink. When drawing in a playlist, we draw from the y-offset instead of y, because the playlist scrolls. | |
| 1040 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 1041 | + | * @param {number} color - The color of the hyperlink. | |
| 1042 | + | */ | |
| 1043 | + | draw(gr, color) { | |
| 1044 | + | const font = this.state === HyperlinkStates.Hovered ? this.hoverFont : this.font; | |
| 1045 | + | DrawString(gr, this.text, font, color, this.x, this.inPlaylist ? this.y_offset : this.y, this.w + SCALE(1), this.h, Stringformat.Trim_Ellipsis_Char); | |
| 1046 | + | } | |
| 1047 | + | ||
| 1048 | + | /** | |
| 1049 | + | * Sets the xOffset of the hyperlink after it has been created. | |
| 1050 | + | * @param {number} xOffset - The x-offset of the hyperlink. Negative values will be subtracted from the containerWidth to right justify. | |
| 1051 | + | */ | |
| 1052 | + | setXOffset(xOffset) { | |
| 1053 | + | this.x = xOffset < 0 ? this.container_w + xOffset : xOffset; | |
| 1054 | + | } | |
| 1055 | + | ||
| 1056 | + | /** | |
| 1057 | + | * Sets the vertical position of the hyperlink. | |
| 1058 | + | * The playlist requires subtracting 2 additional pixels from y for some reason. | |
| 1059 | + | * @param {number} y - The y-coordinate. | |
| 1060 | + | */ | |
| 1061 | + | setY(y) { | |
| 1062 | + | this.y = y + this.y_offset + (-2); | |
| 1063 | + | } | |
| 1064 | + | ||
| 1065 | + | /** | |
| 1066 | + | * Sets the font for the hyperlink. | |
| 1067 | + | * @param {GdiFont} font - The font that will be used. | |
| 1068 | + | */ | |
| 1069 | + | setFont(font) { | |
| 1070 | + | this.font = font; | |
| 1071 | + | this.hoverFont = gdi.Font(font.Name, font.Size, font.Style | FontStyle.Underline); | |
| 1072 | + | this.link_dimensions = this.updateDimensions(); | |
| 1073 | + | } | |
| 1074 | + | ||
| 1075 | + | /** | |
| 1076 | + | * Sets the width of the container the hyperlink will be placed in. | |
| 1077 | + | * If hyperlink width is smaller than the container, it will be truncated. | |
| 1078 | + | * If the the xOffset is negative, the position will be adjusted as the container width changes. | |
| 1079 | + | * @param {number} w - The width. | |
| 1080 | + | */ | |
| 1081 | + | setContainerWidth(w) { | |
| 1082 | + | if (this.x_offset < 0) { | |
| 1083 | + | this.x = w + this.x_offset; // Add because offset is negative | |
| 1084 | + | } | |
| 1085 | + | this.container_w = w; | |
| 1086 | + | this.link_dimensions = this.updateDimensions(); | |
| 1087 | + | this.w = Math.ceil(Math.min(this.container_w, this.link_dimensions.Width + 1)); | |
| 1088 | + | } | |
| 1089 | + | ||
| 1090 | + | /** | |
| 1091 | + | * Gets the width of the hyperlink. | |
| 1092 | + | * @returns {number} The width of the link in pixels. | |
| 1093 | + | */ | |
| 1094 | + | getWidth() { | |
| 1095 | + | try { | |
| 1096 | + | return Math.ceil(this.link_dimensions.Width); | |
| 1097 | + | } catch (e) { | |
| 1098 | + | return null; | |
| 1099 | + | } | |
| 1100 | + | } | |
| 1101 | + | ||
| 1102 | + | /** | |
| 1103 | + | * Updates the width and height of the hyperlinks. | |
| 1104 | + | * @returns {number} The dimensions of the text. | |
| 1105 | + | */ | |
| 1106 | + | updateDimensions() { | |
| 1107 | + | try { | |
| 1108 | + | const measureStringScratchImg = gdi.CreateImage(1000, 200); | |
| 1109 | + | const gr = measureStringScratchImg.GetGraphics(); | |
| 1110 | + | const dimensions = gr.MeasureString(this.text, this.font, 0, 0, 0, 0); | |
| 1111 | + | this.h = Math.ceil(dimensions.Height) + 1; | |
| 1112 | + | this.w = Math.min(Math.ceil(dimensions.Width) + 1, this.container_w); | |
| 1113 | + | measureStringScratchImg.ReleaseGraphics(gr); | |
| 1114 | + | return dimensions; | |
| 1115 | + | } catch (e) { | |
| 1116 | + | return null; // Probably some invalid parameters on init | |
| 1117 | + | } | |
| 1118 | + | } | |
| 1119 | + | ||
| 1120 | + | /** | |
| 1121 | + | * Populates the result of artist, album, date or label in the "Search" playlist when a hyperlink was clicked. | |
| 1122 | + | */ | |
| 1123 | + | click() { | |
| 1124 | + | const populatePlaylist = (query) => { | |
| 1125 | + | grm.debug.debugLog(query); | |
| 1126 | + | try { | |
| 1127 | + | const handle_list = fb.GetQueryItems(fb.GetLibraryItems(), query); | |
| 1128 | + | if (handle_list.Count) { | |
| 1129 | + | pl.history.ignorePlaylistMutations = true; | |
| 1130 | + | const plist = plman.FindOrCreatePlaylist('Search', true); | |
| 1131 | + | plman.UndoBackup(plist); | |
| 1132 | + | handle_list.Sort(); | |
| 1133 | + | const index = fb.IsPlaying ? handle_list.BSearch(fb.GetNowPlaying()) : -1; | |
| 1134 | + | ||
| 1135 | + | if (plist === plman.PlayingPlaylist && plman.GetPlayingItemLocation().PlaylistIndex === pl && index !== -1) { | |
| 1136 | + | // Remove everything in playlist except currently playing song | |
| 1137 | + | plman.ClearPlaylistSelection(plist); | |
| 1138 | + | plman.SetPlaylistSelection(plist, [plman.GetPlayingItemLocation().PlaylistItemIndex], true); | |
| 1139 | + | plman.RemovePlaylistSelection(plist, true); | |
| 1140 | + | plman.ClearPlaylistSelection(plist); | |
| 1141 | + | ||
| 1142 | + | handle_list.RemoveById(index); | |
| 1143 | + | } | |
| 1144 | + | else { | |
| 1145 | + | // Nothing playing or Search playlist is not active | |
| 1146 | + | plman.ClearPlaylist(plist); | |
| 1147 | + | } | |
| 1148 | + | ||
| 1149 | + | plman.InsertPlaylistItems(plist, 0, handle_list); | |
| 1150 | + | plman.SortByFormat(plist, grCfg.settings.playlistSortDefault); | |
| 1151 | + | plman.ActivePlaylist = plist; | |
| 1152 | + | pl.history.ignorePlaylistMutations = false; | |
| 1153 | + | ||
| 1154 | + | return true; | |
| 1155 | + | } | |
| 1156 | + | return false; | |
| 1157 | + | } | |
| 1158 | + | catch (e) { | |
| 1159 | + | pl.history.ignorePlaylistMutations = false; | |
| 1160 | + | console.log(`Could not successfully execute: ${query}`); | |
| 1161 | + | } | |
| 1162 | + | }; | |
| 1163 | + | ||
| 1164 | + | /** @type {string} */ | |
| 1165 | + | let query; | |
| 1166 | + | switch (this.type) { | |
| 1167 | + | case 'update': RunCmd('https://github.com/TT-ReBORN/Georgia-ReBORN/releases'); break; | |
| 1168 | + | case 'date': query = grSet.showPlaylistFullDate ? `"${grTF.date}" IS ${this.text}` : `"$year(%date%)" IS ${this.text}`; break; | |
| 1169 | + | case 'artist': query = grSet.headerFlipRows ? `Album HAS "${this.text.replace(Regex.PunctQuoteDouble, '')}"` : `Artist HAS "${this.text.replace(Regex.PunctQuoteDouble, '')}" OR Album Artist HAS "${this.text.replace(Regex.PunctQuoteDouble, '')}" OR ARTISTFILTER HAS "${this.text.replace(Regex.PunctQuoteDouble, '')}"`; break; | |
| 1170 | + | case 'album': query = grSet.headerFlipRows ? `Artist HAS "${this.text.replace(Regex.PunctQuoteDouble, '')}" OR Album Artist HAS "${this.text.replace(Regex.PunctQuoteDouble, '')}" OR ARTISTFILTER HAS "${this.text.replace(Regex.PunctQuoteDouble, '')}"` : `Album HAS "${this.text.replace(Regex.PunctQuoteDouble, '')}"`; break; | |
| 1171 | + | case 'label': query = `Label HAS "${this.text.replace(Regex.PunctQuoteDouble, '')}" OR Publisher HAS "${this.text.replace(Regex.PunctQuoteDouble, '')}"`; break; | |
| 1172 | + | default: query = `${this.type} IS "${this.text}"`; break; | |
| 1173 | + | } | |
| 1174 | + | ||
| 1175 | + | if (!populatePlaylist(query)) { | |
| 1176 | + | const start = this.text.indexOf('['); | |
| 1177 | + | if (start > 0) { | |
| 1178 | + | query = `${this.type} IS ${this.text.slice(0, start - 3)}`; // Remove ' - [...]' from end of string in case we're showing "Album - [Deluxe Edition]", etc. | |
| 1179 | + | populatePlaylist(query); | |
| 1180 | + | } | |
| 1181 | + | } | |
| 1182 | + | } | |
| 1183 | + | ||
| 1184 | + | /** | |
| 1185 | + | * Updates the hyperlink state. | |
| 1186 | + | */ | |
| 1187 | + | repaint() { | |
| 1188 | + | try { | |
| 1189 | + | window.RepaintRect(this.x, this.y, this.w, this.h); | |
| 1190 | + | } catch (e) { | |
| 1191 | + | // Probably already redrawing | |
| 1192 | + | } | |
| 1193 | + | } | |
| 1194 | + | // #endregion | |
| 1195 | + | ||
| 1196 | + | // * CALLBACKS * // | |
| 1197 | + | // #region CALLBACKS | |
| 1198 | + | /** | |
| 1199 | + | * Sets mouse hover state for every hyperlink not created in Playlist. | |
| 1200 | + | * @param {object} hyperlink - The hyperlink object. | |
| 1201 | + | * @param {number} x - The x-coordinate. | |
| 1202 | + | * @param {number} y - The y-coordinate. | |
| 1203 | + | * @returns {boolean} True or false. | |
| 1204 | + | */ | |
| 1205 | + | on_mouse_move(hyperlink, x, y) { | |
| 1206 | + | if (hyperlink.trace(x, y)) { | |
| 1207 | + | if (hyperlink.state !== HyperlinkStates.Hovered) { | |
| 1208 | + | hyperlink.state = HyperlinkStates.Hovered; | |
| 1209 | + | window.RepaintRect(hyperlink.x, hyperlink.y, hyperlink.w, hyperlink.h); | |
| 1210 | + | } | |
| 1211 | + | return true; | |
| 1212 | + | } | |
| 1213 | + | if (hyperlink.state !== HyperlinkStates.Normal) { | |
| 1214 | + | hyperlink.state = HyperlinkStates.Normal; | |
| 1215 | + | window.RepaintRect(hyperlink.x, hyperlink.y, hyperlink.w, hyperlink.h); | |
| 1216 | + | } | |
| 1217 | + | return false; | |
| 1218 | + | } | |
| 1219 | + | ||
| 1220 | + | /** | |
| 1221 | + | * Checks if the mouse is within the boundaries of a hyperlink. | |
| 1222 | + | * @param {number} x - The x-coordinate. | |
| 1223 | + | * @param {number} y - The y-coordinate. | |
| 1224 | + | * @returns {boolean} True or false. | |
| 1225 | + | */ | |
| 1226 | + | trace(x, y) { | |
| 1227 | + | return (this.x <= x) && (x <= this.x + this.w) && (this.y <= y) && (y <= this.y + this.h); | |
| 1228 | + | } | |
| 1229 | + | // #endregion | |
| 1230 | + | } | |
| 1231 | + | ||
| 1232 | + | ||
| 1233 | + | ///////////////////// | |
| 1234 | + | // * JUMP SEARCH * // | |
| 1235 | + | ///////////////////// | |
| 1236 | + | /** | |
| 1237 | + | * A class that creates the jump search when using keystrokes. | |
| 1238 | + | * Searches in the active Playlist first and when nothing found, it tries in the Library. | |
| 1239 | + | */ | |
| 1240 | + | class JumpSearch { | |
| 1241 | + | /** | |
| 1242 | + | * Creates the `JumpSearch` instance. | |
| 1243 | + | */ | |
| 1244 | + | constructor() { | |
| 1245 | + | /** @private @type {number} */ | |
| 1246 | + | this.arc1 = 5; | |
| 1247 | + | /** @private @type {number} */ | |
| 1248 | + | this.arc2 = 4; | |
| 1249 | + | /** @private @type {object} */ | |
| 1250 | + | this.j = { | |
| 1251 | + | x: 0, | |
| 1252 | + | y: 0, | |
| 1253 | + | w: grSet.notificationFontSize_layout * 2, | |
| 1254 | + | h: grSet.notificationFontSize_layout * 2 | |
| 1255 | + | }; | |
| 1256 | + | /** @private @type {string} */ | |
| 1257 | + | this.jSearch = ''; | |
| 1258 | + | /** @private @type {boolean} */ | |
| 1259 | + | this.jump_search = true; | |
| 1260 | + | /** @type {{ [key: string]: number[] }} */ | |
| 1261 | + | this.initials = null; | |
| 1262 | + | } | |
| 1263 | + | ||
| 1264 | + | // * PUBLIC METHODS * // | |
| 1265 | + | // #region PUBLIC METHODS | |
| 1266 | + | /** | |
| 1267 | + | * Draws the jump search on the playlist panel. | |
| 1268 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 1269 | + | */ | |
| 1270 | + | draw(gr) { | |
| 1271 | + | if (!this.jSearch) return; | |
| 1272 | + | gr.SetSmoothingMode(4); | |
| 1273 | + | this.j.w = gr.CalcTextWidth(this.jSearch, grFont.notification) + 25; | |
| 1274 | + | gr.FillRoundRect(this.j.x - this.j.w / 2, this.j.y, this.j.w, this.j.h, this.arc1, this.arc1, RGBtoRGBA(grCol.popupBg, 220)); | |
| 1275 | + | gr.DrawRoundRect(this.j.x - this.j.w / 2, this.j.y, this.j.w, this.j.h, this.arc1, this.arc1, 1, 0x64000000); | |
| 1276 | + | gr.DrawRoundRect(this.j.x - this.j.w / 2 + 1, this.j.y + 1, this.j.w - 2, this.j.h - 2, this.arc2, this.arc2, 1, 0x28ffffff); | |
| 1277 | + | // gr.GdiDrawText(this.jSearch, grFont.notification, RGB(0, 0, 0), this.j.x - this.j.w / 2 + 1, this.j.y + 1, this.j.w, this.j.h, panel.cc); // Drop shadow not needed | |
| 1278 | + | gr.GdiDrawText(this.jSearch, grFont.notification, this.jump_search ? grCol.popupText : 0xffff4646, this.j.x - this.j.w / 2, this.j.y, this.j.w, this.j.h, lib.panel.cc); | |
| 1279 | + | gr.SetSmoothingMode(0); | |
| 1280 | + | } | |
| 1281 | + | ||
| 1282 | + | /** | |
| 1283 | + | * Sets the vertical position of the jump search. | |
| 1284 | + | * @param {number} y - The y-coordinate. | |
| 1285 | + | */ | |
| 1286 | + | setY(y) { | |
| 1287 | + | this.y = y; | |
| 1288 | + | } | |
| 1289 | + | // #endregion | |
| 1290 | + | ||
| 1291 | + | // * CALLBACKS * // | |
| 1292 | + | // #region CALLBACKS | |
| 1293 | + | /** | |
| 1294 | + | * Handles key pressed events and activates the jump search. | |
| 1295 | + | * @param {number} code - The character code. | |
| 1296 | + | */ | |
| 1297 | + | on_char(code) { | |
| 1298 | + | const text = String.fromCharCode(code); | |
| 1299 | + | ||
| 1300 | + | if (grSet.jumpSearchDisabled || lib.panel.search.active || | |
| 1301 | + | utils.IsKeyPressed(VKey.CONTROL) || utils.IsKeyPressed(VKey.ESCAPE) || | |
| 1302 | + | this.jSearch === '' && text === ' ') { | |
| 1303 | + | return; | |
| 1304 | + | } | |
| 1305 | + | ||
| 1306 | + | const playlistItems = plman.GetPlaylistItems(plman.ActivePlaylist); | |
| 1307 | + | const search = fb.TitleFormat(grSet.jumpSearchComposerOnly ? '%composer%' : '$if2(%album artist%, %artist%)').EvalWithMetadbs(playlistItems); | |
| 1308 | + | let focusIndex = plman.GetPlaylistFocusItemIndex(plman.ActivePlaylist); | |
| 1309 | + | let advance = false; | |
| 1310 | + | let foundInPlaylist = false; | |
| 1311 | + | let foundInLibrary = false; | |
| 1312 | + | ||
| 1313 | + | switch (code) { | |
| 1314 | + | case lib.vk.back: | |
| 1315 | + | this.jSearch = this.jSearch.slice(0, -1); | |
| 1316 | + | break; | |
| 1317 | + | case lib.vk.enter: | |
| 1318 | + | this.jSearch = ''; | |
| 1319 | + | return; | |
| 1320 | + | default: | |
| 1321 | + | this.jSearch += text; | |
| 1322 | + | break; | |
| 1323 | + | } | |
| 1324 | + | ||
| 1325 | + | // * Playlist advance | |
| 1326 | + | if (focusIndex >= 0 && focusIndex < search.length && (grm.ui.displayPlaylist || grm.ui.displayLibrarySplit(true))) { | |
| 1327 | + | const char = search[focusIndex].replace(Regex.LibMarkerColor, '').charAt(0).toLowerCase(); | |
| 1328 | + | if (char === text && AllEqual(this.jSearch)) { | |
| 1329 | + | this.jSearch = this.jSearch.slice(0, 1); | |
| 1330 | + | advance = true; | |
| 1331 | + | } | |
| 1332 | + | } | |
| 1333 | + | // * Library advance | |
| 1334 | + | else if (lib.panel.pos >= 0 && lib.panel.pos < lib.pop.tree.length && !grm.ui.displayLibrarySplit(true)) { | |
| 1335 | + | const char = lib.pop.tree[lib.panel.pos].name.replace(Regex.LibMarkerColor, '').charAt(0).toLowerCase(); | |
| 1336 | + | if (lib.pop.tree[lib.panel.pos].sel && char === text && AllEqual(this.jSearch)) { | |
| 1337 | + | this.jSearch = this.jSearch.slice(0, 1); | |
| 1338 | + | advance = true; | |
| 1339 | + | } | |
| 1340 | + | } | |
| 1341 | + | ||
| 1342 | + | switch (true) { | |
| 1343 | + | case advance: { | |
| 1344 | + | if (utils.IsKeyPressed(0x0A) || utils.IsKeyPressed(VKey.BACK) || utils.IsKeyPressed(VKey.TAB) || utils.IsKeyPressed(VKey.CONTROL) || utils.IsKeyPressed(VKey.ESCAPE) || utils.IsKeyPressed(VKey.MULTIPLY) || utils.IsKeyPressed(VKey.SUBTRACT)) return; | |
| 1345 | + | let init = ''; | |
| 1346 | + | let cur = 'currentArr'; | |
| 1347 | + | if (!this.initials) { // reset in buildTree | |
| 1348 | + | this.initials = {}; | |
| 1349 | + | // * Playlist advance | |
| 1350 | + | if (grm.ui.displayPlaylist || grm.ui.displayLibrarySplit(true)) { | |
| 1351 | + | for (const [i] of playlistItems.Convert().entries()) { | |
| 1352 | + | const name = search[i].replace(Regex.LibMarkerColor, ''); | |
| 1353 | + | init = name.charAt().toLowerCase(); | |
| 1354 | + | if (cur !== init && !this.initials[init]) { | |
| 1355 | + | this.initials[init] = [i]; | |
| 1356 | + | cur = init; | |
| 1357 | + | } else { | |
| 1358 | + | this.initials[init].push(i); | |
| 1359 | + | } | |
| 1360 | + | } | |
| 1361 | + | } | |
| 1362 | + | // * Library advance | |
| 1363 | + | else { | |
| 1364 | + | for (const [i, v] of lib.pop.tree.entries()) { | |
| 1365 | + | if (!v.root) { | |
| 1366 | + | const nm = v.name.replace(Regex.LibMarkerColor, ''); | |
| 1367 | + | init = nm.charAt().toLowerCase(); | |
| 1368 | + | if (cur !== init && !this.initials[init]) { | |
| 1369 | + | this.initials[init] = [i]; | |
| 1370 | + | cur = init; | |
| 1371 | + | } else { | |
| 1372 | + | this.initials[init].push(i); | |
| 1373 | + | } | |
| 1374 | + | } | |
| 1375 | + | } | |
| 1376 | + | } | |
| 1377 | + | } | |
| 1378 | + | ||
| 1379 | + | this.jump_search = false; | |
| 1380 | + | ||
| 1381 | + | // * Playlist advance | |
| 1382 | + | if (focusIndex >= 0 && focusIndex < search.length && (grm.ui.displayPlaylist || grm.ui.displayLibrarySplit(true))) { | |
| 1383 | + | this.matches = this.initials[text]; | |
| 1384 | + | grm.debug.debugLog('Playlist advance results', this.matches); // Debug | |
| 1385 | + | this.ix = this.matches.indexOf(focusIndex); | |
| 1386 | + | this.ix++; | |
| 1387 | + | if (this.ix >= this.matches.length) this.ix = 0; | |
| 1388 | + | focusIndex = this.matches[this.ix]; | |
| 1389 | + | this.jump_search = true; | |
| 1390 | + | } | |
| 1391 | + | // * Library advance | |
| 1392 | + | else if (lib.panel.pos >= 0 && lib.panel.pos < lib.pop.tree.length && !grm.ui.displayLibrarySplit(true)) { | |
| 1393 | + | this.matches = this.initials[text]; | |
| 1394 | + | grm.debug.debugLog('Library advance results', this.matches); // Debug, can remove this soon | |
| 1395 | + | this.ix = this.matches.indexOf(lib.panel.pos); | |
| 1396 | + | this.ix++; | |
| 1397 | + | if (this.ix >= this.matches.length) this.ix = 0; | |
| 1398 | + | lib.panel.pos = this.matches[this.ix]; | |
| 1399 | + | this.jump_search = true; | |
| 1400 | + | } | |
| 1401 | + | ||
| 1402 | + | // * Playlist advance | |
| 1403 | + | if (this.jump_search && (grm.ui.displayPlaylist || grm.ui.displayLibrarySplit(true))) { | |
| 1404 | + | plman.ClearPlaylistSelection(plman.ActivePlaylist); | |
| 1405 | + | plman.SetPlaylistFocusItem(plman.ActivePlaylist, focusIndex); | |
| 1406 | + | plman.SetPlaylistSelectionSingle(plman.ActivePlaylist, focusIndex, true); | |
| 1407 | + | window.Repaint(); | |
| 1408 | + | } | |
| 1409 | + | // * Library advance | |
| 1410 | + | else if (this.jump_search && !grm.ui.displayLibrarySplit(true)) { | |
| 1411 | + | lib.pop.clearSelected(); | |
| 1412 | + | lib.pop.sel_items = []; | |
| 1413 | + | lib.pop.tree[lib.panel.pos].sel = true; | |
| 1414 | + | lib.pop.setPos(lib.panel.pos); | |
| 1415 | + | lib.pop.getTreeSel(); | |
| 1416 | + | lib.lib.treeState(false, libSet.rememberTree); | |
| 1417 | + | window.Repaint(); | |
| 1418 | + | if (lib.panel.imgView) lib.pop.showItem(lib.panel.pos, 'focus'); | |
| 1419 | + | else { | |
| 1420 | + | const row = (lib.panel.pos * lib.ui.row.h - lib.sbar.scroll) / lib.ui.row.h; | |
| 1421 | + | if (lib.sbar.rows_drawn - row < 3 || row < 0) lib.sbar.checkScroll((lib.panel.pos + 3) * lib.ui.row.h - lib.sbar.rows_drawn * lib.ui.row.h); | |
| 1422 | + | } | |
| 1423 | + | if (libSet.libSource) { | |
| 1424 | + | if (lib.pop.autoFill.key) lib.pop.load(lib.pop.sel_items, true, false, false, !libSet.sendToCur, false); | |
| 1425 | + | lib.pop.track(lib.pop.autoFill.key); | |
| 1426 | + | } else if (lib.panel.pos >= 0 && lib.panel.pos < lib.pop.tree.length) lib.pop.setPlaylistSelection(lib.panel.pos, lib.pop.tree[lib.panel.pos]); | |
| 1427 | + | } | |
| 1428 | + | else { | |
| 1429 | + | window.Repaint(); | |
| 1430 | + | } | |
| 1431 | + | lib.timer.clear(lib.timer.jsearch2); | |
| 1432 | + | lib.timer.jsearch2.id = setTimeout(() => { | |
| 1433 | + | this.jSearch = ''; | |
| 1434 | + | window.Repaint(); | |
| 1435 | + | lib.timer.jsearch2.id = null; | |
| 1436 | + | }, 2200); | |
| 1437 | + | } | |
| 1438 | + | break; | |
| 1439 | + | ||
| 1440 | + | case !advance: | |
| 1441 | + | if (utils.IsKeyPressed(VKey.TAB) || utils.IsKeyPressed(VKey.CONTROL) || utils.IsKeyPressed(VKey.ESCAPE) || utils.IsKeyPressed(VKey.MULTIPLY) || utils.IsKeyPressed(VKey.SUBTRACT)) return; | |
| 1442 | + | if (!lib.panel.search.active) { | |
| 1443 | + | let pos = -1; | |
| 1444 | + | lib.pop.clearSelected(); | |
| 1445 | + | if (!this.jSearch) return; | |
| 1446 | + | lib.pop.sel_items = []; | |
| 1447 | + | this.jump_search = true; | |
| 1448 | + | window.Repaint(); | |
| 1449 | + | lib.timer.clear(lib.timer.jsearch1); | |
| 1450 | + | ||
| 1451 | + | lib.timer.jsearch1.id = setTimeout(() => { | |
| 1452 | + | // * First search in the Playlist | |
| 1453 | + | playlistItems.Convert().some((v, i) => { | |
| 1454 | + | const name = search[i].replace(Regex.LibMarkerColor, ''); | |
| 1455 | + | if (name.substring(0, this.jSearch.length).toLowerCase() === this.jSearch.toLowerCase()) { | |
| 1456 | + | foundInPlaylist = true; | |
| 1457 | + | pos = i; | |
| 1458 | + | plman.ClearPlaylistSelection(plman.ActivePlaylist); | |
| 1459 | + | plman.SetPlaylistFocusItem(plman.ActivePlaylist, pos); | |
| 1460 | + | plman.SetPlaylistSelectionSingle(plman.ActivePlaylist, pos, true); | |
| 1461 | + | grm.debug.debugLog(`Jumpsearch: "${name}" found in Playlist`); // Debug, can remove this soon | |
| 1462 | + | return true; | |
| 1463 | + | } | |
| 1464 | + | return false; | |
| 1465 | + | }); | |
| 1466 | + | // * If no Playlist results found, try search query in the Library | |
| 1467 | + | if (!foundInPlaylist && grSet.jumpSearchIncludeLibrary && grSet.layout !== 'compact') { | |
| 1468 | + | lib.pop.tree.some((v, i) => { | |
| 1469 | + | const name = v.name.replace(Regex.LibMarkerColor, ''); | |
| 1470 | + | if (name !== lib.panel.rootName && name.substring(0, this.jSearch.length).toLowerCase() === this.jSearch.toLowerCase()) { | |
| 1471 | + | foundInPlaylist = false; | |
| 1472 | + | foundInLibrary = true; | |
| 1473 | + | pos = i; | |
| 1474 | + | v.sel = true; | |
| 1475 | + | lib.pop.setPos(pos); | |
| 1476 | + | if (lib.pop.autoFill.key) lib.pop.getTreeSel(); | |
| 1477 | + | lib.lib.treeState(false, libSet.rememberTree); | |
| 1478 | + | grm.debug.debugLog(`Jumpsearch: "${name}" found in Library`); // Debug, can remove this soon | |
| 1479 | + | return true; | |
| 1480 | + | } | |
| 1481 | + | return false; | |
| 1482 | + | }); | |
| 1483 | + | } | |
| 1484 | + | ||
| 1485 | + | if (!foundInPlaylist && !foundInLibrary) { | |
| 1486 | + | this.jump_search = false; | |
| 1487 | + | grm.debug.debugLog('Jumpsearch: No results were found'); // Debug, can remove this soon | |
| 1488 | + | } | |
| 1489 | + | ||
| 1490 | + | window.Repaint(); | |
| 1491 | + | ||
| 1492 | + | if (foundInPlaylist) { | |
| 1493 | + | grm.ui.displayPlaylist = true; | |
| 1494 | + | grm.ui.displayLibrary = grSet.libraryLayout === 'split' && grm.ui.displayPlaylist; | |
| 1495 | + | grm.ui.displayBiography = false; | |
| 1496 | + | grm.ui.displayLyrics = false; | |
| 1497 | + | grm.button.initButtonState(); | |
| 1498 | + | } | |
| 1499 | + | else if (foundInLibrary && grSet.jumpSearchIncludeLibrary) { | |
| 1500 | + | grm.ui.displayPlaylist = grSet.libraryLayout === 'split' && grm.ui.displayPlaylist; | |
| 1501 | + | grm.ui.displayLibrary = true; | |
| 1502 | + | grm.ui.displayBiography = false; | |
| 1503 | + | grm.ui.displayLyrics = false; | |
| 1504 | + | lib.pop.showItem(pos, 'focus'); | |
| 1505 | + | this.jSearch = ''; // Reset to avoid conflict with other query | |
| 1506 | + | grm.button.initButtonState(); | |
| 1507 | + | } | |
| 1508 | + | ||
| 1509 | + | lib.timer.jsearch1.id = null; | |
| 1510 | + | }, 500); | |
| 1511 | + | ||
| 1512 | + | lib.timer.clear(lib.timer.jsearch2); | |
| 1513 | + | ||
| 1514 | + | lib.timer.jsearch2.id = setTimeout(() => { | |
| 1515 | + | this.jSearch = ''; | |
| 1516 | + | window.Repaint(); | |
| 1517 | + | lib.timer.jsearch2.id = null; | |
| 1518 | + | }, 1200); | |
| 1519 | + | } | |
| 1520 | + | } | |
| 1521 | + | } | |
| 1522 | + | ||
| 1523 | + | /** | |
| 1524 | + | * Sets the size and position of the jump search and updates them on window resizing. | |
| 1525 | + | */ | |
| 1526 | + | on_size() { | |
| 1527 | + | this.j.h = grSet.notificationFontSize_layout * 2; | |
| 1528 | + | this.j.x = Math.round(grSet.playlistLayout === 'full' || grSet.layout !== 'default' ? grm.ui.ww * 0.5 : grm.ui.ww * 0.5 + grm.ui.ww * 0.25); | |
| 1529 | + | this.j.y = Math.round((grm.ui.wh + grm.ui.topMenuHeight - grm.ui.lowerBarHeight - this.j.h) / 2); | |
| 1530 | + | this.arc1 = Math.min(5, this.j.h / 2); | |
| 1531 | + | this.arc2 = Math.min(4, (this.j.h - 2) / 2); | |
| 1532 | + | } | |
| 1533 | + | // #endregion | |
| 1534 | + | } | |
| 1535 | + | ||
| 1536 | + | ||
| 1537 | + | ////////////////////// | |
| 1538 | + | // * PROGRESS BAR * // | |
| 1539 | + | ////////////////////// | |
| 1540 | + | /** | |
| 1541 | + | * A class that creates the progress bar in the lower bar when enabled. | |
| 1542 | + | * Quick access via right click context menu on lower bar. | |
| 1543 | + | */ | |
| 1544 | + | class ProgressBar { | |
| 1545 | + | /** | |
| 1546 | + | * Creates the `ProgressBar` instance. | |
| 1547 | + | */ | |
| 1548 | + | constructor() { | |
| 1549 | + | /** @public @type {number} The x-coordinate of the progress bar. */ | |
| 1550 | + | this.x = grm.ui.edgeMargin; | |
| 1551 | + | /** @public @type {number} The y-coordinate of the progress bar. */ | |
| 1552 | + | this.y = 0; | |
| 1553 | + | /** @public @type {number} The width of the progress bar. */ | |
| 1554 | + | this.w = grm.ui.ww - grm.ui.edgeMarginBoth; | |
| 1555 | + | /** @public @type {number} The height of the progress bar. */ | |
| 1556 | + | this.h = grm.ui.seekbarHeight; | |
| 1557 | + | /** @public @type {number} The arc radius for rounded corners of the progress bar. */ | |
| 1558 | + | this.arc = Math.min(this.w, this.h) / 2; | |
| 1559 | + | /** @public @type {number} The length of the progress bar fill. */ | |
| 1560 | + | this.progressLength = 0; | |
| 1561 | + | /** @private @type {boolean} The state that indicates if the progress bar is being dragged. */ | |
| 1562 | + | this.drag = false; | |
| 1563 | + | } | |
| 1564 | + | ||
| 1565 | + | // * PUBLIC METHODS * // | |
| 1566 | + | // #region PUBLIC METHODS | |
| 1567 | + | /** | |
| 1568 | + | * Draws the progress bar with various progress bar styles. | |
| 1569 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 1570 | + | */ | |
| 1571 | + | draw(gr) { | |
| 1572 | + | if (grm.debug.showDrawExtendedTiming) grm.ui.seekbarProfiler.Reset(); | |
| 1573 | + | ||
| 1574 | + | const styleRounded = grSet.styleProgressBarDesign === 'rounded'; | |
| 1575 | + | if (styleRounded) this.arc = Math.min(this.w, this.h) / 2; | |
| 1576 | + | ||
| 1577 | + | gr.SetSmoothingMode(styleRounded ? SmoothingMode.AntiAlias : SmoothingMode.None); | |
| 1578 | + | ||
| 1579 | + | this.drawProgressBarBg(gr); | |
| 1580 | + | this.drawProgressBarFill(gr); | |
| 1581 | + | } | |
| 1582 | + | ||
| 1583 | + | /** | |
| 1584 | + | * Draws the progress bar background. | |
| 1585 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 1586 | + | */ | |
| 1587 | + | drawProgressBarBg(gr) { | |
| 1588 | + | const barDesignNoDotsThin = !['dots', 'thin'].includes(grSet.styleProgressBarDesign); | |
| 1589 | + | const styleRounded = grSet.styleProgressBarDesign === 'rounded'; | |
| 1590 | + | const styleDefault = grSet.styleDefault && (['blue', 'darkblue', 'red', 'cream'].includes(grSet.theme) || grSet.theme.startsWith('custom')); | |
| 1591 | + | const styleCream = grSet.theme === 'cream' && (grSet.styleAlternative || grSet.styleAlternative2) && (!grSet.styleBevel && !grSet.styleBlend && !grSet.styleBlend2 && grSet.styleProgressBarDesign !== 'rounded') && !grSet.systemFirstLaunch; | |
| 1592 | + | const styleBevelOrInner = barDesignNoDotsThin && ['bevel', 'inner'].includes(grSet.styleProgressBar); | |
| 1593 | + | const progressBarColor = grm.ui.isStreaming && fb.IsPlaying ? grCol.progressBarStreaming : grCol.progressBar; | |
| 1594 | + | ||
| 1595 | + | if (styleRounded) { | |
| 1596 | + | FillRoundRect(gr, this.x, this.y, this.w, this.h, this.arc, this.arc, progressBarColor); | |
| 1597 | + | } else if (barDesignNoDotsThin) { | |
| 1598 | + | gr.FillSolidRect(this.x, this.y, this.w, this.h, progressBarColor); | |
| 1599 | + | } | |
| 1600 | + | ||
| 1601 | + | if (styleDefault || styleCream) { | |
| 1602 | + | gr.DrawRect(this.x - 2, this.y - 2, this.w + 3, this.h + 3, 1, grCol.progressBarFrame); | |
| 1603 | + | } | |
| 1604 | + | ||
| 1605 | + | if (styleBevelOrInner) { | |
| 1606 | + | const styleBlackReborn = grSet.styleBlackReborn && fb.IsPlaying; | |
| 1607 | + | const angle = grSet.styleProgressBar === 'inner' ? (styleBlackReborn ? 90 : -90) : (styleBlackReborn ? -90 : 90); | |
| 1608 | + | ||
| 1609 | + | if (styleRounded) { | |
| 1610 | + | FillGradRoundRect(gr, this.x, this.y, this.w + SCALE(2), this.h + SCALE(2), this.arc, this.arc, angle, 0, grCol.styleProgressBar, 1); | |
| 1611 | + | ||
| 1612 | + | const xLeft = this.x + SCALE(3); | |
| 1613 | + | const xRight = this.w + this.x - SCALE(12); | |
| 1614 | + | const yTop = this.y - 0.5; | |
| 1615 | + | const yBottom = this.y + this.h - 0.5; | |
| 1616 | + | FillGradRect(gr, xLeft, yTop, SCALE(9), 1, 179, grCol.styleProgressBarLineTop, 0); // Top left | |
| 1617 | + | FillGradRect(gr, xLeft, yBottom, SCALE(9), 1, 179, grCol.styleProgressBarLineBottom, 0); // Bottom left | |
| 1618 | + | FillGradRect(gr, xRight, yTop, SCALE(9), 1, 179, 0, grCol.styleProgressBarLineTop); // Top right | |
| 1619 | + | FillGradRect(gr, xRight, yBottom, SCALE(9), 1, 179, 0, grCol.styleProgressBarLineBottom); // Bottom right | |
| 1620 | + | } | |
| 1621 | + | else { | |
| 1622 | + | FillGradRect(gr, this.x, this.y, this.w, this.h, angle, 0, grCol.styleProgressBar); | |
| 1623 | + | } | |
| 1624 | + | ||
| 1625 | + | const lineX1 = this.x + (styleRounded ? SCALE(12) : 0); | |
| 1626 | + | const lineX2 = this.x + this.w - (styleRounded ? SCALE(12) : 1); | |
| 1627 | + | gr.DrawLine(lineX1, this.y, lineX2, this.y, 1, grCol.styleProgressBarLineTop); | |
| 1628 | + | gr.DrawLine(lineX1, this.y + this.h, lineX2, this.y + this.h, 1, grCol.styleProgressBarLineBottom); | |
| 1629 | + | } | |
| 1630 | + | } | |
| 1631 | + | ||
| 1632 | + | /** | |
| 1633 | + | * Draws the progress bar fill. | |
| 1634 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 1635 | + | */ | |
| 1636 | + | drawProgressBarFill(gr) { | |
| 1637 | + | if (!fb.IsPlaying || fb.PlaybackLength <= 0) return; | |
| 1638 | + | ||
| 1639 | + | const playbackRatio = fb.PlaybackTime / fb.PlaybackLength; | |
| 1640 | + | this.progressLength = Math.floor(this.w * playbackRatio); | |
| 1641 | + | ||
| 1642 | + | const drawBarDesign = { | |
| 1643 | + | default: () => gr.FillSolidRect(this.x, this.y, this.progressLength, this.h, grCol.progressBarFill), | |
| 1644 | + | rounded: () => FillRoundRect(gr, this.x, this.y, this.progressLength, this.h, this.arc, this.arc, grCol.progressBarFill), | |
| 1645 | + | lines: () => this.drawBarDesignLines(gr), | |
| 1646 | + | blocks: () => this.drawBarDesignBlocks(gr), | |
| 1647 | + | dots: () => this.drawBarDesignDots(gr), | |
| 1648 | + | thin: () => this.drawBarDesignThin(gr) | |
| 1649 | + | }; | |
| 1650 | + | ||
| 1651 | + | drawBarDesign[grSet.styleProgressBarDesign](); | |
| 1652 | + | ||
| 1653 | + | if (!['dots', 'thin'].includes(grSet.styleProgressBarDesign) && ['bevel', 'inner'].includes(grSet.styleProgressBarFill)) { | |
| 1654 | + | if (grSet.styleProgressBarDesign === 'rounded') { | |
| 1655 | + | FillGradRoundRect(gr, this.x, this.y, this.progressLength + SCALE(2), this.h + SCALE(2), this.arc, this.arc, grSet.styleProgressBarFill === 'inner' ? -88 : 88, 0, grCol.styleProgressBarFill); | |
| 1656 | + | } else { | |
| 1657 | + | FillGradRect(gr, this.x, this.y, this.progressLength, this.h, grSet.styleProgressBarFill === 'inner' ? -90 : 89, 0, grCol.styleProgressBarFill); | |
| 1658 | + | } | |
| 1659 | + | } | |
| 1660 | + | else if (grSet.styleProgressBarFill === 'blend' && grm.ui.albumArt && grCol.imgBlended) { | |
| 1661 | + | if (grSet.styleProgressBarDesign === 'rounded') { | |
| 1662 | + | FillBlendedRoundRect(gr, this.x, this.y, this.progressLength + SCALE(2), this.h + SCALE(2), this.arc, this.arc, 88, grCol.imgBlended, 0); | |
| 1663 | + | } else { | |
| 1664 | + | gr.DrawImage(grCol.imgBlended, this.x, this.y, this.progressLength, this.h, 0, this.h, grCol.imgBlended.Width, grCol.imgBlended.Height); | |
| 1665 | + | } | |
| 1666 | + | } | |
| 1667 | + | } | |
| 1668 | + | ||
| 1669 | + | /** | |
| 1670 | + | * Draws the progress bar fill in lines design. | |
| 1671 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 1672 | + | */ | |
| 1673 | + | drawBarDesignLines(gr) { | |
| 1674 | + | gr.FillSolidRect(this.x + this.progressLength, this.y, SCALE(2), grm.ui.seekbarHeight, grCol.progressBarFill); | |
| 1675 | + | ||
| 1676 | + | for (let progressLine = 0; progressLine < this.progressLength; progressLine += SCALE(4)) { | |
| 1677 | + | gr.DrawLine(this.x + progressLine + SCALE(2), this.y, this.x + progressLine + SCALE(2), this.y + this.h, SCALE(2), grCol.progressBarFill); | |
| 1678 | + | } | |
| 1679 | + | } | |
| 1680 | + | ||
| 1681 | + | /** | |
| 1682 | + | * Draws the progress bar fill in blocks design. | |
| 1683 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 1684 | + | */ | |
| 1685 | + | drawBarDesignBlocks(gr) { | |
| 1686 | + | for (let progressLine = 0; progressLine < this.progressLength; progressLine += grm.ui.seekbarHeight + SCALE(2)) { | |
| 1687 | + | gr.FillSolidRect(this.x + progressLine, this.y + SCALE(2), grm.ui.seekbarHeight, grm.ui.seekbarHeight - SCALE(4), grCol.progressBarFill); | |
| 1688 | + | } | |
| 1689 | + | ||
| 1690 | + | gr.FillSolidRect(this.x + this.progressLength, this.y + 1, grm.ui.seekbarHeight, grm.ui.seekbarHeight - 1, grCol.progressBar); | |
| 1691 | + | FillGradRect(gr, this.x + this.progressLength, this.y + 1, grm.ui.seekbarHeight, grm.ui.seekbarHeight - 1, grSet.styleProgressBar === 'inner' ? grSet.styleBlackReborn && fb.IsPlaying ? 88 : -88 : grSet.styleBlackReborn && fb.IsPlaying ? -88 : 88, 0, grCol.styleProgressBar); | |
| 1692 | + | } | |
| 1693 | + | ||
| 1694 | + | /** | |
| 1695 | + | * Draws the progress bar fill in dots design. | |
| 1696 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 1697 | + | */ | |
| 1698 | + | drawBarDesignDots(gr) { | |
| 1699 | + | for (let progressLine = 0; progressLine < this.progressLength; progressLine += SCALE(8)) { | |
| 1700 | + | gr.DrawLine(this.x + this.progressLength + SCALE(10), this.y + this.h * 0.5, this.x + this.w, this.y + this.h * 0.5, SCALE(1), grCol.progressBar); | |
| 1701 | + | gr.SetSmoothingMode(SmoothingMode.AntiAlias); | |
| 1702 | + | gr.DrawEllipse(this.x + progressLine, this.y + this.h * 0.5 - SCALE(1), SCALE(2), SCALE(2), SCALE(2), grCol.progressBarFill); | |
| 1703 | + | } | |
| 1704 | + | ||
| 1705 | + | const posFix = HD_4K(3, grSet.layout !== 'default' ? 6 : 7); | |
| 1706 | + | gr.DrawEllipse(this.x + this.progressLength, this.y + this.h * 0.5 - grm.ui.seekbarHeight * 0.5 + SCALE(2), grm.ui.seekbarHeight - SCALE(4), grm.ui.seekbarHeight - SCALE(4), SCALE(2), grCol.progressBarFill); // Knob outline | |
| 1707 | + | gr.DrawEllipse(this.x + this.progressLength + posFix, this.y + this.h * 0.5 - SCALE(1), SCALE(2), SCALE(2), SCALE(2), grCol.transportIconHovered); // Knob inner | |
| 1708 | + | } | |
| 1709 | + | ||
| 1710 | + | /** | |
| 1711 | + | * Draws the progress bar fill in thin design. | |
| 1712 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 1713 | + | */ | |
| 1714 | + | drawBarDesignThin(gr) { | |
| 1715 | + | gr.DrawLine(this.x, this.y + this.h * 0.5, this.x + this.w, this.y + this.h * 0.5, SCALE(1), grCol.progressBar); | |
| 1716 | + | gr.SetSmoothingMode(SmoothingMode.AntiAlias); | |
| 1717 | + | gr.FillSolidRect(this.x, this.y + this.h * 0.5 - SCALE(2), this.progressLength, SCALE(4), grCol.progressBarFill); | |
| 1718 | + | gr.FillSolidRect(this.x + this.progressLength, this.y + this.h * 0.5 - SCALE(3), SCALE(6), SCALE(6), grCol.progressBarFill); | |
| 1719 | + | } | |
| 1720 | + | ||
| 1721 | + | /** | |
| 1722 | + | * Sets the vertical progress bar position. | |
| 1723 | + | * @param {number} y - The y-coordinate. | |
| 1724 | + | */ | |
| 1725 | + | setY(y) { | |
| 1726 | + | this.y = y; | |
| 1727 | + | } | |
| 1728 | + | ||
| 1729 | + | /** | |
| 1730 | + | * Sets the playback time of the progress bar. | |
| 1731 | + | * @param {number} x - The x-coordinate. | |
| 1732 | + | * @private | |
| 1733 | + | */ | |
| 1734 | + | setPlaybackTime(x) { | |
| 1735 | + | const clampedPosition = Clamp((x - this.x) / this.w, 0, 1); | |
| 1736 | + | const newPlaybackTime = clampedPosition * fb.PlaybackLength; | |
| 1737 | + | if (fb.PlaybackTime !== newPlaybackTime) { | |
| 1738 | + | fb.PlaybackTime = newPlaybackTime; | |
| 1739 | + | } | |
| 1740 | + | } | |
| 1741 | + | ||
| 1742 | + | /** | |
| 1743 | + | * Sets the refresh rate for the progress bar. | |
| 1744 | + | */ | |
| 1745 | + | setRefreshRate() { | |
| 1746 | + | if (grm.ui.isStreaming) { | |
| 1747 | + | grm.ui.seekbarTimerInterval = FPS._1; | |
| 1748 | + | } | |
| 1749 | + | else if (grSet.progressBarRefreshRate !== 'variable') { | |
| 1750 | + | grm.ui.seekbarTimerInterval = grSet.progressBarRefreshRate; | |
| 1751 | + | } | |
| 1752 | + | else { | |
| 1753 | + | const pixelsPerMillisecond = (grm.ui.ww - grm.ui.edgeMarginBoth) / fb.PlaybackLength; | |
| 1754 | + | const FPS_VARIABLE = Math.ceil(1000 / pixelsPerMillisecond); | |
| 1755 | + | grm.ui.seekbarTimerInterval = Clamp(FPS_VARIABLE, FPS._15, FPS._2); | |
| 1756 | + | } | |
| 1757 | + | } | |
| 1758 | + | ||
| 1759 | + | /** | |
| 1760 | + | * Updates the progress bar state. | |
| 1761 | + | */ | |
| 1762 | + | repaint() { | |
| 1763 | + | window.RepaintRect(this.x, this.y, this.w, this.h); | |
| 1764 | + | } | |
| 1765 | + | // #endregion | |
| 1766 | + | ||
| 1767 | + | // * CALLBACKS * // | |
| 1768 | + | // #region CALLBACKS | |
| 1769 | + | /** | |
| 1770 | + | * Checks if the mouse is within the boundaries of the progress bar. | |
| 1771 | + | * @param {number} x - The x-coordinate. | |
| 1772 | + | * @param {number} y - The y-coordinate. | |
| 1773 | + | * @returns {boolean} True or false. | |
| 1774 | + | */ | |
| 1775 | + | mouseInThis(x, y) { | |
| 1776 | + | return (x >= this.x && y >= this.y && x < this.x + this.w && y <= this.y + this.h); | |
| 1777 | + | } | |
| 1778 | + | ||
| 1779 | + | /** | |
| 1780 | + | * Handles left mouse button down click events and enables dragging. | |
| 1781 | + | * @param {number} x - The x-coordinate. | |
| 1782 | + | * @param {number} y - The y-coordinate. | |
| 1783 | + | */ | |
| 1784 | + | on_mouse_lbtn_down(x, y) { | |
| 1785 | + | this.drag = true; | |
| 1786 | + | } | |
| 1787 | + | ||
| 1788 | + | /** | |
| 1789 | + | * Handles left mouse button up click events and disables dragging and updates the playback time. | |
| 1790 | + | * @param {number} x - The x-coordinate. | |
| 1791 | + | * @param {number} y - The y-coordinate. | |
| 1792 | + | */ | |
| 1793 | + | on_mouse_lbtn_up(x, y) { | |
| 1794 | + | this.drag = false; | |
| 1795 | + | if (this.mouseInThis(x, y)) { | |
| 1796 | + | this.setPlaybackTime(x); | |
| 1797 | + | } | |
| 1798 | + | } | |
| 1799 | + | ||
| 1800 | + | /** | |
| 1801 | + | * Handles mouse movement events and updates the playback time based on the mouse movement if a drag event is occurring. | |
| 1802 | + | * @param {number} x - The x-coordinate. | |
| 1803 | + | * @param {number} y - The y-coordinate. | |
| 1804 | + | */ | |
| 1805 | + | on_mouse_move(x, y) { | |
| 1806 | + | if (this.drag) { | |
| 1807 | + | this.setPlaybackTime(x); | |
| 1808 | + | } | |
| 1809 | + | } | |
| 1810 | + | ||
| 1811 | + | /** | |
| 1812 | + | * Updates progress bar length when playing a new track. | |
| 1813 | + | * @param {FbMetadbHandle} metadb - The metadb of the track. | |
| 1814 | + | */ | |
| 1815 | + | on_playback_new_track(metadb) { | |
| 1816 | + | if (!metadb) return; | |
| 1817 | + | this.progressLength = 0; | |
| 1818 | + | } | |
| 1819 | + | ||
| 1820 | + | /** | |
| 1821 | + | * Sets the size and position of the progress bar and updates them on window resizing. | |
| 1822 | + | * @param {number} w - The width of the window or element. | |
| 1823 | + | * @param {number} h - The height of the window or element. | |
| 1824 | + | */ | |
| 1825 | + | on_size(w, h) { | |
| 1826 | + | this.x = grm.ui.edgeMargin; | |
| 1827 | + | this.y = 0; | |
| 1828 | + | this.w = w - grm.ui.edgeMarginBoth; | |
| 1829 | + | this.h = grm.ui.seekbarHeight; | |
| 1830 | + | } | |
| 1831 | + | // #endregion | |
| 1832 | + | } | |
| 1833 | + | ||
| 1834 | + | ||
| 1835 | + | /////////////////////// | |
| 1836 | + | // * PEAKMETER BAR * // | |
| 1837 | + | /////////////////////// | |
| 1838 | + | /** | |
| 1839 | + | * A class that creates the peakmeter bar in the lower bar when enabled. | |
| 1840 | + | * Quick access via right click context menu on lower bar. | |
| 1841 | + | */ | |
| 1842 | + | class PeakmeterBar { | |
| 1843 | + | /** | |
| 1844 | + | * Creates the `PeakmeterBar` instance. | |
| 1845 | + | */ | |
| 1846 | + | constructor() { | |
| 1847 | + | // * GEOMETRY - STYLE HORIZONTAL * // | |
| 1848 | + | // #region GEOMETRY - STYLE HORIZONTAL | |
| 1849 | + | /** @public @type {number} The x-position of the peakmeter bar. */ | |
| 1850 | + | this.x = grm.ui.edgeMargin; | |
| 1851 | + | /** @public @type {number} The y-position of the peakmeter bar. */ | |
| 1852 | + | this.y = 0; | |
| 1853 | + | /** @public @type {number} The width of the peakmeter bar. */ | |
| 1854 | + | this.w = grm.ui.ww - grm.ui.edgeMarginBoth; | |
| 1855 | + | /** @public @type {number} The secondary width of the peakmeter bar. */ | |
| 1856 | + | this.w2 = 0; | |
| 1857 | + | /** @public @type {number} The height of the peakmeter bar. */ | |
| 1858 | + | this.h = grm.ui.seekbarHeight; | |
| 1859 | + | /** @private @type {number} The height of the bar for the peakmeter. */ | |
| 1860 | + | this.bar_h = grSet.layout !== 'default' ? SCALE(2) : SCALE(4); | |
| 1861 | + | /** @private @type {number} The half height of the bar for the peakmeter. */ | |
| 1862 | + | this.bar2_h = this.bar_h * 0.5; | |
| 1863 | + | /** @private @type {number} The offset for the peakmeter bar. */ | |
| 1864 | + | this.offset = 0; | |
| 1865 | + | /** @private @type {number} The middle offset for the peakmeter bar. */ | |
| 1866 | + | this.middleOffset = 0; | |
| 1867 | + | /** @private @type {number} The middle width for the peakmeter bar. */ | |
| 1868 | + | this.middle_w = 0; | |
| 1869 | + | ||
| 1870 | + | // * Top | |
| 1871 | + | /** @private @type {number} The width of the outer left bar. */ | |
| 1872 | + | this.outerLeft_w = 0; | |
| 1873 | + | /** @private @type {number} The old width of the outer left bar. */ | |
| 1874 | + | this.outerLeft_w_old = 0; | |
| 1875 | + | /** @private @type {number} The animated width of the outer left bar. */ | |
| 1876 | + | this.outerLeftAnim_w = 0; | |
| 1877 | + | /** @private @type {number} The x-position of the animated outer left bar. */ | |
| 1878 | + | this.outerLeftAnim_x = 0; | |
| 1879 | + | /** @private @type {number} The offset for the outer left bar. */ | |
| 1880 | + | this.outerLeft_k = 0; | |
| 1881 | + | ||
| 1882 | + | /** @private @type {number} The x-position of the main left bar. */ | |
| 1883 | + | this.mainLeft_x = 0; | |
| 1884 | + | /** @private @type {number} The x-position of the animated main left bar. */ | |
| 1885 | + | this.mainLeftAnim_x = 0; | |
| 1886 | + | /** @private @type {number} The secondary x-position of the animated main left bar. */ | |
| 1887 | + | this.mainLeftAnim2_x = 0; | |
| 1888 | + | /** @private @type {number} The offset for the main left bar. */ | |
| 1889 | + | this.mainLeft_k = 0; | |
| 1890 | + | /** @private @type {number} The secondary offset for the main left bar. */ | |
| 1891 | + | this.mainLeft2_k = 0; | |
| 1892 | + | ||
| 1893 | + | // * Bottom | |
| 1894 | + | /** @private @type {number} The width of the outer right bar. */ | |
| 1895 | + | this.outerRight_w = 0; | |
| 1896 | + | /** @private @type {number} The old width of the outer right bar. */ | |
| 1897 | + | this.outerRight_w_old = 0; | |
| 1898 | + | /** @private @type {number} The animated width of the outer right bar. */ | |
| 1899 | + | this.outerRightAnim_w = 0; | |
| 1900 | + | /** @private @type {number} The x-position of the animated outer right bar. */ | |
| 1901 | + | this.outerRightAnim_x = 0; | |
| 1902 | + | /** @private @type {number} The offset for the outer right bar. */ | |
| 1903 | + | this.outerRight_k = 0; | |
| 1904 | + | ||
| 1905 | + | /** @private @type {number} The x-position of the main right bar. */ | |
| 1906 | + | this.mainRight_x = 0; | |
| 1907 | + | /** @private @type {number} The x-position of the animated main right bar. */ | |
| 1908 | + | this.mainRightAnim_x = 0; | |
| 1909 | + | /** @private @type {number} The secondary x-position of the animated main right bar. */ | |
| 1910 | + | this.mainRightAnim2_x = 0; | |
| 1911 | + | /** @private @type {number} The offset for the main right bar. */ | |
| 1912 | + | this.mainRight_k = 0; | |
| 1913 | + | /** @private @type {number} The secondary offset for the main right bar. */ | |
| 1914 | + | this.mainRight2_k = 0; | |
| 1915 | + | // #endregion | |
| 1916 | + | ||
| 1917 | + | // * GEOMETRY - STYLE VERTICAL * // | |
| 1918 | + | // #region GEOMETRY - STYLE VERTICAL | |
| 1919 | + | /** @private @type {number} The vertical offset for the bar. */ | |
| 1920 | + | this.vertBar_offset = 0; | |
| 1921 | + | /** @private @type {number} The vertical width of the bar. */ | |
| 1922 | + | this.vertBar_w = 0; | |
| 1923 | + | /** @private @type {number} The vertical height of the bar. */ | |
| 1924 | + | this.vertBar_h = 0; | |
| 1925 | + | /** @private @type {number} The x-coordinate for the left vertical bar. */ | |
| 1926 | + | this.vertLeft_x = 0; | |
| 1927 | + | /** @private @type {number} The x-coordinate for the right vertical bar. */ | |
| 1928 | + | this.vertRight_x = 0; | |
| 1929 | + | // #endregion | |
| 1930 | + | ||
| 1931 | + | // * PROGRESS BAR * // | |
| 1932 | + | // #region PROGRESS BAR | |
| 1933 | + | /** @public @type {number} The length of the progress bar. */ | |
| 1934 | + | this.progressLength = 0; | |
| 1935 | + | /** @private @type {boolean} The state indicating whether the progress bar is being dragged. */ | |
| 1936 | + | this.drag = false; | |
| 1937 | + | // #endregion | |
| 1938 | + | ||
| 1939 | + | // * MOUSE EVENTS * // | |
| 1940 | + | // #region MOUSE EVENTS | |
| 1941 | + | /** @private @type {number} The x-coordinate position of the mouse. */ | |
| 1942 | + | this.pos_x = 0; | |
| 1943 | + | /** @private @type {number} The y-coordinate position of the mouse. */ | |
| 1944 | + | this.pos_y = 0; | |
| 1945 | + | /** @private @type {boolean} The state indicating whether the mouse is over the peakmeter bar. */ | |
| 1946 | + | this.on_mouse = false; | |
| 1947 | + | /** @private @type {boolean} The state indicating whether the mouse wheel is being used. */ | |
| 1948 | + | this.wheel = false; | |
| 1949 | + | // #endregion | |
| 1950 | + | ||
| 1951 | + | // * TEXT * // | |
| 1952 | + | // #region TEXT | |
| 1953 | + | /** @private @type {GdiFont} The font used for text rendering. */ | |
| 1954 | + | this.textFont = gdi.Font('Segoe UI', HD_4K(9, 16), 1); | |
| 1955 | + | /** @private @type {number} The width of the text. */ | |
| 1956 | + | this.textWidth = 0; | |
| 1957 | + | /** @private @type {number} The height of the text. */ | |
| 1958 | + | this.textHeight = 0; | |
| 1959 | + | /** @private @type {string} The text for the tooltip. */ | |
| 1960 | + | this.tooltipText = ''; | |
| 1961 | + | // #endregion | |
| 1962 | + | ||
| 1963 | + | // * VOLUME * // | |
| 1964 | + | // #region VOLUME | |
| 1965 | + | /** @private @type {number[]} The middle decibel values. */ | |
| 1966 | + | this.db_middle = []; | |
| 1967 | + | /** @private @type {number[]} The current decibel values. */ | |
| 1968 | + | this.db = []; | |
| 1969 | + | /** @private @type {object[]} The vertical decibel values. */ | |
| 1970 | + | this.db_vert = {}; | |
| 1971 | + | ||
| 1972 | + | /** @private @type {number} The middle points for the peakmeter. */ | |
| 1973 | + | this.points_middle = 0; | |
| 1974 | + | /** @private @type {number} The points for the peakmeter. */ | |
| 1975 | + | this.points = 0; | |
| 1976 | + | /** @private @type {number} The vertical points for the peakmeter. */ | |
| 1977 | + | this.points_vert = 0; | |
| 1978 | + | /** @private @type {number[]} The left peaks for the peakmeter. */ | |
| 1979 | + | this.leftPeaks_s = []; | |
| 1980 | + | /** @private @type {number[]} The right peaks for the peakmeter. */ | |
| 1981 | + | this.rightPeaks_s = []; | |
| 1982 | + | // #endregion | |
| 1983 | + | ||
| 1984 | + | // * COLORS * // | |
| 1985 | + | // #region COLORS | |
| 1986 | + | /** @private @type {number} The separator index for the peakmeter. */ | |
| 1987 | + | this.separator = 0; | |
| 1988 | + | /** @private @type {number} The first separator value for the peakmeter. */ | |
| 1989 | + | this.sep1 = 0; | |
| 1990 | + | /** @private @type {number} The second separator value for the peakmeter. */ | |
| 1991 | + | this.sep2 = 0; | |
| 1992 | + | // #endregion | |
| 1993 | + | ||
| 1994 | + | // * INITIALIZATION * // | |
| 1995 | + | // #region INITIALIZATION | |
| 1996 | + | grm.ui.seekbarTimerInterval = grSet.peakmeterBarRefreshRate === 'variable' ? FPS._10 : grSet.peakmeterBarRefreshRate; | |
| 1997 | + | ||
| 1998 | + | this.initDecibel(); | |
| 1999 | + | this.initPeaks(); | |
| 2000 | + | this.initPoints(); | |
| 2001 | + | this.initSeparator(); | |
| 2002 | + | this.setColors(); | |
| 2003 | + | // #endregion | |
| 2004 | + | } | |
| 2005 | + | ||
| 2006 | + | // * PUBLIC METHODS - DRAW * // | |
| 2007 | + | // #region PUBLIC METHODS - DRAW | |
| 2008 | + | /** | |
| 2009 | + | * Draws the peakmeter bar in various peakmeter bar designs. | |
| 2010 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 2011 | + | */ | |
| 2012 | + | draw(gr) { | |
| 2013 | + | if (!fb.IsPlaying || !AudioWizard) { | |
| 2014 | + | gr.FillSolidRect(this.x, this.y, this.w, this.h, grCol.bg); | |
| 2015 | + | gr.FillSolidRect(this.x, grm.ui.seekbarY, this.w, SCALE(grSet.layout !== 'default' ? 10 : 12), grCol.progressBar); | |
| 2016 | + | return; | |
| 2017 | + | } | |
| 2018 | + | ||
| 2019 | + | if (grSet.peakmeterBarRefreshRate === 'variable' || grm.debug.showDrawExtendedTiming) { | |
| 2020 | + | grm.ui.seekbarProfiler.Reset(); | |
| 2021 | + | } | |
| 2022 | + | ||
| 2023 | + | this.drawPeakmeterBar(gr); | |
| 2024 | + | this.setAnimation(); | |
| 2025 | + | this.setMonitoring(); | |
| 2026 | + | this.setRefreshRate(); | |
| 2027 | + | } | |
| 2028 | + | ||
| 2029 | + | /** | |
| 2030 | + | * Draws the peakmeter bar design based on design setting. | |
| 2031 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 2032 | + | */ | |
| 2033 | + | drawPeakmeterBar(gr) { | |
| 2034 | + | const drawBarDesign = { | |
| 2035 | + | horizontal: () => this.drawBarDesignHorizontal(gr), | |
| 2036 | + | horizontal_center: () => this.drawBarDesignCenter(gr), | |
| 2037 | + | vertical: () => this.drawBarDesignVertical(gr) | |
| 2038 | + | }; | |
| 2039 | + | ||
| 2040 | + | drawBarDesign[grSet.peakmeterBarDesign](); | |
| 2041 | + | } | |
| 2042 | + | ||
| 2043 | + | /** | |
| 2044 | + | * Draws the horizontal peakmeter bar design. | |
| 2045 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 2046 | + | */ | |
| 2047 | + | drawBarDesignHorizontal(gr) { | |
| 2048 | + | this.drawBarGrid(gr); | |
| 2049 | + | ||
| 2050 | + | for (let i = 0; i <= this.points; i++) { | |
| 2051 | + | const color = this.color[i]; | |
| 2052 | + | const db = this.db[i]; | |
| 2053 | + | const dbNext = this.db[i + 1]; | |
| 2054 | + | const offset = i * this.offset; | |
| 2055 | + | ||
| 2056 | + | this.drawHorizontalMainBars(gr, db, dbNext, offset, this.leftPeak, this.mainLeftAnim_x, this.mainLeftAnim2_x, 'mainLeft_x', this.mainLeft_y, color); | |
| 2057 | + | this.drawHorizontalMainBars(gr, db, dbNext, offset, this.rightPeak, this.mainRightAnim_x, this.mainRightAnim2_x, 'mainRight_x', this.mainRight_y, color); | |
| 2058 | + | ||
| 2059 | + | this.drawHorizontalOuterBars(gr, db, dbNext, offset, this.leftLevel, this.outerLeftAnim_x, this.outerLeftAnim_w, this.outerLeft_y, 'outerLeft_w'); | |
| 2060 | + | this.drawHorizontalOuterBars(gr, db, dbNext, offset, this.rightLevel, this.outerRightAnim_x, this.outerRightAnim_w, this.outerRight_y, 'outerRight_w'); | |
| 2061 | + | ||
| 2062 | + | this.drawOverBars(gr); | |
| 2063 | + | } | |
| 2064 | + | ||
| 2065 | + | this.drawMiddleBars(gr); | |
| 2066 | + | this.drawProgressBar(gr); | |
| 2067 | + | this.drawBarInfo(gr); | |
| 2068 | + | } | |
| 2069 | + | ||
| 2070 | + | /** | |
| 2071 | + | * Draws the main bars in the horizontal peakmeter bar design. | |
| 2072 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 2073 | + | * @param {number} db - The current decibel level. | |
| 2074 | + | * @param {number} dbNext - The next decibel level. | |
| 2075 | + | * @param {number} offset - The offset for drawing. | |
| 2076 | + | * @param {number} peak - The peak value. | |
| 2077 | + | * @param {number} anim_x - The animation x-coordinate. | |
| 2078 | + | * @param {number} anim2_x - The second animation x-coordinate. | |
| 2079 | + | * @param {string} main_x - The main x-coordinate property key name. | |
| 2080 | + | * @param {number} main_y - The main y-coordinate. | |
| 2081 | + | * @param {number} color - The color of the bar. | |
| 2082 | + | * @private | |
| 2083 | + | */ | |
| 2084 | + | drawHorizontalMainBars(gr, db, dbNext, offset, peak, anim_x, anim2_x, main_x, main_y, color) { | |
| 2085 | + | if (peak <= db) return; | |
| 2086 | + | ||
| 2087 | + | if (peak < dbNext) this[main_x] = offset; | |
| 2088 | + | ||
| 2089 | + | if (grSet.peakmeterBarMainBars) { | |
| 2090 | + | gr.FillSolidRect(this.x + offset, main_y, this.w2, this.bar_h, color); | |
| 2091 | + | } | |
| 2092 | + | ||
| 2093 | + | if (grSet.peakmeterBarMainPeaks) { | |
| 2094 | + | const color = this.color[Math.round(this.mainLeftAnim_x / this.offset)]; | |
| 2095 | + | gr.FillSolidRect(this.x + anim_x + this.offset, main_y, this.w2 * 0.66, this.bar_h, color); | |
| 2096 | + | ||
| 2097 | + | const x = Clamp(this.x + anim2_x + this.offset + this.w2 * 0.66, this.x, this.x + this.w - this.w2 * 0.33); | |
| 2098 | + | const w = x > this.w + this.w2 * 0.5 ? 0 : this.w2 * 0.33; | |
| 2099 | + | gr.FillSolidRect(x, main_y, w, this.bar_h, color); | |
| 2100 | + | } | |
| 2101 | + | } | |
| 2102 | + | ||
| 2103 | + | /** | |
| 2104 | + | * Draws the outer bars in the horizontal peakmeter bar design. | |
| 2105 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 2106 | + | * @param {number} db - The current decibel level. | |
| 2107 | + | * @param {number} dbNext - The next decibel level. | |
| 2108 | + | * @param {number} offset - The offset for drawing. | |
| 2109 | + | * @param {number} level - The level value. | |
| 2110 | + | * @param {number} anim_x - The animation x-coordinate. | |
| 2111 | + | * @param {number} anim_w - The animation width. | |
| 2112 | + | * @param {number} outer_y - The outer y-coordinate. | |
| 2113 | + | * @param {string} outer_w - The outer width property key name. | |
| 2114 | + | * @private | |
| 2115 | + | */ | |
| 2116 | + | drawHorizontalOuterBars(gr, db, dbNext, offset, level, anim_x, anim_w, outer_y, outer_w) { | |
| 2117 | + | if (level <= db) return; | |
| 2118 | + | ||
| 2119 | + | if (level < dbNext) { | |
| 2120 | + | this[outer_w] = offset + this.offset / Math.abs(dbNext - db) * Math.abs(level - db) - this.x; | |
| 2121 | + | } | |
| 2122 | + | ||
| 2123 | + | if (grSet.peakmeterBarOuterBars) { | |
| 2124 | + | gr.FillSolidRect(this.x, outer_y, this[outer_w], this.bar_h, this.color[1]); | |
| 2125 | + | } | |
| 2126 | + | ||
| 2127 | + | if (grSet.peakmeterBarOuterPeaks) { | |
| 2128 | + | const x = Clamp(this.x + anim_x, this.x, this.x + this.w - anim_w); | |
| 2129 | + | gr.FillSolidRect(x, outer_y, anim_w <= 0 ? 2 : anim_w, this.bar_h, this.color[1]); | |
| 2130 | + | } | |
| 2131 | + | } | |
| 2132 | + | ||
| 2133 | + | /** | |
| 2134 | + | * Draws the horizontal center peakmeter bar design. | |
| 2135 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 2136 | + | */ | |
| 2137 | + | drawBarDesignCenter(gr) { | |
| 2138 | + | this.drawBarGrid(gr); | |
| 2139 | + | ||
| 2140 | + | for (let i = 0; i <= this.points; i++) { | |
| 2141 | + | const color = this.color[i]; | |
| 2142 | + | const db = this.db[i]; | |
| 2143 | + | const dbNext = this.db[i + 1]; | |
| 2144 | + | const offset = i * this.offset; | |
| 2145 | + | const mainLeft_x = this.x * 0.5 + this.w * 0.5 - i * this.offset + this.w2; | |
| 2146 | + | const mainRight_x = this.x + this.w * 0.5 + i * this.offset - this.w2; | |
| 2147 | + | ||
| 2148 | + | this.drawCenterMainBars(gr, db, dbNext, offset, this.leftPeak, this.mainLeftAnim_x, this.mainLeftAnim2_x, mainLeft_x, mainRight_x, 'mainLeft_x', this.mainLeft_y, color); | |
| 2149 | + | this.drawCenterMainBars(gr, db, dbNext, offset, this.rightPeak, this.mainRightAnim_x, this.mainRightAnim2_x, mainLeft_x, mainRight_x, 'mainRight_x', this.mainRight_y, color); | |
| 2150 | + | ||
| 2151 | + | this.drawCenterOuterBars(gr, db, dbNext, offset, this.leftLevel, this.outerLeftAnim_x, this.outerLeftAnim_w, this.outerLeft_y, 'outerLeft_w'); | |
| 2152 | + | this.drawCenterOuterBars(gr, db, dbNext, offset, this.rightLevel, this.outerRightAnim_x, this.outerRightAnim_w, this.outerRight_y, 'outerRight_w'); | |
| 2153 | + | ||
| 2154 | + | this.drawOverBars(gr); | |
| 2155 | + | } | |
| 2156 | + | ||
| 2157 | + | this.drawMiddleBars(gr); | |
| 2158 | + | this.drawProgressBar(gr); | |
| 2159 | + | this.drawBarInfo(gr); | |
| 2160 | + | } | |
| 2161 | + | ||
| 2162 | + | /** | |
| 2163 | + | * Draws the main bars in the horizontal center peakmeter bar design. | |
| 2164 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 2165 | + | * @param {number} db - The current decibel level. | |
| 2166 | + | * @param {number} dbNext - The next decibel level. | |
| 2167 | + | * @param {number} offset - The offset for drawing. | |
| 2168 | + | * @param {number} peak - The peak value. | |
| 2169 | + | * @param {number} anim_x - The animation x-coordinate. | |
| 2170 | + | * @param {number} anim2_x - The second animation x-coordinate. | |
| 2171 | + | * @param {number} mainLeft_x - The main left x-coordinate. | |
| 2172 | + | * @param {number} mainRight_x - The main right x-coordinate. | |
| 2173 | + | * @param {string} main_x - The main x-coordinate property key name. | |
| 2174 | + | * @param {number} main_y - The main y-coordinate. | |
| 2175 | + | * @param {string} color - The color of the bar. | |
| 2176 | + | * @private | |
| 2177 | + | */ | |
| 2178 | + | drawCenterMainBars(gr, db, dbNext, offset, peak, anim_x, anim2_x, mainLeft_x, mainRight_x, main_x, main_y, color) { | |
| 2179 | + | if (peak <= db) return; | |
| 2180 | + | ||
| 2181 | + | if (peak < dbNext) this[main_x] = offset; | |
| 2182 | + | ||
| 2183 | + | if (grSet.peakmeterBarMainBars) { | |
| 2184 | + | gr.FillSolidRect(mainLeft_x, main_y, this.w2, this.bar_h, color); | |
| 2185 | + | gr.FillSolidRect(mainRight_x, main_y, this.w2, this.bar_h, color); | |
| 2186 | + | } | |
| 2187 | + | ||
| 2188 | + | if (grSet.peakmeterBarMainPeaks) { | |
| 2189 | + | const color = this.color[Math.round(anim_x / this.offset)]; | |
| 2190 | + | const xLeft = this.x * 0.5 + this.w * 0.5 - (anim_x + this.offset) + this.w2 * 0.33; | |
| 2191 | + | const xRight = this.x + anim_x + this.offset + this.w * 0.5; | |
| 2192 | + | gr.FillSolidRect(xLeft, main_y, this.w2 * 0.66, this.bar_h, color); | |
| 2193 | + | gr.FillSolidRect(xRight, main_y, this.w2 * 0.66, this.bar_h, color); | |
| 2194 | + | ||
| 2195 | + | const xLeftPeaks = this.x + this.w * 0.5 - anim2_x - this.offset - this.w2 * 0.66; | |
| 2196 | + | const wLeftPeaks = xLeftPeaks < this.x + this.w2 * 0.5 ? 0 : this.w2 * 0.33; | |
| 2197 | + | const xRightPeaks = this.x + this.w * 0.5 + anim2_x + this.offset + this.w2 * 0.66; | |
| 2198 | + | const wRightPeaks = xRightPeaks > this.w + this.w2 * 0.5 ? 0 : this.w2 * 0.33; | |
| 2199 | + | gr.FillSolidRect(xLeftPeaks, main_y, wLeftPeaks, this.bar_h, color); | |
| 2200 | + | gr.FillSolidRect(xRightPeaks, main_y, wRightPeaks, this.bar_h, color); | |
| 2201 | + | } | |
| 2202 | + | } | |
| 2203 | + | ||
| 2204 | + | /** | |
| 2205 | + | * Draws the outer bars in the horizontal center peakmeter bar design. | |
| 2206 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 2207 | + | * @param {number} db - The current decibel level. | |
| 2208 | + | * @param {number} dbNext - The next decibel level. | |
| 2209 | + | * @param {number} offset - The offset for drawing. | |
| 2210 | + | * @param {number} level - The level value. | |
| 2211 | + | * @param {number} anim_x - The animation x-coordinate. | |
| 2212 | + | * @param {number} anim_w - The animation width. | |
| 2213 | + | * @param {number} outer_y - The outer y-coordinate. | |
| 2214 | + | * @param {string} outer_w - The outer width property name. | |
| 2215 | + | * @private | |
| 2216 | + | */ | |
| 2217 | + | drawCenterOuterBars(gr, db, dbNext, offset, level, anim_x, anim_w, outer_y, outer_w) { | |
| 2218 | + | if (level <= db) return; | |
| 2219 | + | ||
| 2220 | + | if (level < dbNext) { | |
| 2221 | + | this[outer_w] = offset + this.offset / Math.abs(dbNext - db) * Math.abs(level - db) - this.x; | |
| 2222 | + | } | |
| 2223 | + | ||
| 2224 | + | if (grSet.peakmeterBarOuterBars) { | |
| 2225 | + | const xLeft = Clamp(this.x + this.w * 0.5 - this[outer_w], this.x, this.w * 0.5); | |
| 2226 | + | const xRight = this.x + this.w * 0.5; | |
| 2227 | + | const w = Clamp(this[outer_w], 0, this.w * 0.5); | |
| 2228 | + | gr.FillSolidRect(xLeft, outer_y, w, this.bar_h, this.color[1]); | |
| 2229 | + | gr.FillSolidRect(xRight, outer_y, w, this.bar_h, this.color[1]); | |
| 2230 | + | } | |
| 2231 | + | ||
| 2232 | + | if (grSet.peakmeterBarOuterPeaks) { | |
| 2233 | + | const x = Clamp(this.x + anim_x, this.x, this.x + this.w * 0.5 - anim_w); | |
| 2234 | + | const w = anim_w <= 0 ? 2 : anim_w; | |
| 2235 | + | const xLeftPeaks = this.w * 0.5 + this.x * 2 - x - w; | |
| 2236 | + | const xRightPeaks = this.w * 0.5 + x; | |
| 2237 | + | gr.FillSolidRect(xLeftPeaks, outer_y, w, this.bar_h, this.color[1]); | |
| 2238 | + | gr.FillSolidRect(xRightPeaks, outer_y, w, this.bar_h, this.color[1]); | |
| 2239 | + | } | |
| 2240 | + | } | |
| 2241 | + | ||
| 2242 | + | /** | |
| 2243 | + | * Draws the vertical peakmeter bar design. | |
| 2244 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 2245 | + | */ | |
| 2246 | + | drawBarDesignVertical(gr) { | |
| 2247 | + | const peakL = Math.round(this.leftPeak); | |
| 2248 | + | const peakR = Math.round(this.rightPeak); | |
| 2249 | + | const vertBarH = this.vertBar_h * 1.5; | |
| 2250 | + | ||
| 2251 | + | const toleranceBase = 0.05; | |
| 2252 | + | const toleranceMin = 0.1; | |
| 2253 | + | const toleranceMax = 1.0; | |
| 2254 | + | const toleranceL = Clamp(toleranceBase * Math.abs(peakL), toleranceMin, toleranceMax); | |
| 2255 | + | const toleranceR = Clamp(toleranceBase * Math.abs(peakR), toleranceMin, toleranceMax); | |
| 2256 | + | ||
| 2257 | + | for (let i = 0; i < this.points_vert; i++) { | |
| 2258 | + | const dbL = this.db_vert[i]; | |
| 2259 | + | const dbR = this.db_vert[this.points_vert - 1 - i]; | |
| 2260 | + | const offset = this.vertBar_offset * i; | |
| 2261 | + | ||
| 2262 | + | if (Math.abs(peakL - dbL) <= toleranceL) this.leftPeaks_s[i] = vertBarH; | |
| 2263 | + | if (Math.abs(peakR - dbR) <= toleranceR) this.rightPeaks_s[i] = vertBarH; | |
| 2264 | + | ||
| 2265 | + | this.drawVerticalPeaks(gr, this.vertLeft_x, offset, this.leftPeaks_s[i]); | |
| 2266 | + | this.drawVerticalPeaks(gr, this.vertRight_x, offset, this.rightPeaks_s[i]); | |
| 2267 | + | } | |
| 2268 | + | ||
| 2269 | + | if (grSet.peakmeterBarVertBaseline) { | |
| 2270 | + | gr.FillSolidRect(this.x, this.y + this.h - this.vertBar_h, this.w, this.vertBar_h, grCol.peakmeterBarProg); | |
| 2271 | + | } | |
| 2272 | + | ||
| 2273 | + | this.drawProgressBar(gr); | |
| 2274 | + | this.drawBarInfo(gr); | |
| 2275 | + | } | |
| 2276 | + | ||
| 2277 | + | /** | |
| 2278 | + | * Draws the peaks in the vertical peakmeter bar design. | |
| 2279 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 2280 | + | * @param {number} xBase - The base x-coordinate. | |
| 2281 | + | * @param {number} offset - The offset for drawing. | |
| 2282 | + | * @param {number} peak_s - The peak value. | |
| 2283 | + | * @private | |
| 2284 | + | */ | |
| 2285 | + | drawVerticalPeaks(gr, xBase, offset, peak_s) { | |
| 2286 | + | const x = xBase + offset; | |
| 2287 | + | const y = this.y + peak_s - this.vertBar_h; | |
| 2288 | + | ||
| 2289 | + | if (peak_s <= this.h) { | |
| 2290 | + | const h = Math.min(this.h - peak_s, this.h); | |
| 2291 | + | gr.FillSolidRect(x, y, this.vertBar_w, h, grCol.peakmeterBarVertFill); | |
| 2292 | + | } | |
| 2293 | + | ||
| 2294 | + | if (grSet.peakmeterBarVertPeaks && peak_s >= 0) { | |
| 2295 | + | gr.FillSolidRect(x, y, this.vertBar_w, this.vertBar_h, grCol.peakmeterBarVertFillPeaks); | |
| 2296 | + | } | |
| 2297 | + | } | |
| 2298 | + | ||
| 2299 | + | /** | |
| 2300 | + | * Draws the over bars in the peakmeter bar designs. | |
| 2301 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 2302 | + | * @private | |
| 2303 | + | */ | |
| 2304 | + | drawOverBars(gr) { | |
| 2305 | + | if (!grSet.peakmeterBarOverBars) return; | |
| 2306 | + | ||
| 2307 | + | const widthSize = grSet.peakmeterBarDesign === 'horizontal' ? 1 : 0.5; | |
| 2308 | + | const overLeft = this.outerLeftAnim_x + this.outerLeftAnim_w - (this.w * widthSize); | |
| 2309 | + | const overRight = this.outerRightAnim_x + this.outerRightAnim_w - (this.w * widthSize); | |
| 2310 | + | ||
| 2311 | + | const outerAnim = this.outerLeftAnim_x - this.outerLeftAnim_w; | |
| 2312 | + | const outerAnimHalf = outerAnim * 0.5; | |
| 2313 | + | ||
| 2314 | + | const xLeft = this.w - overLeft - this.x; | |
| 2315 | + | const xRight = this.w - overRight - this.x; | |
| 2316 | + | const xLeft2 = Clamp(this.w * 0.5 - overLeft - outerAnim, this.x, this.w * 0.5); | |
| 2317 | + | const xRight2 = Clamp(this.w * 0.5 - overRight - outerAnim, this.x, this.w * 0.5); | |
| 2318 | + | ||
| 2319 | + | const wLeft = this.w - xLeft + this.x; | |
| 2320 | + | const wRight = this.w - xRight + this.x; | |
| 2321 | + | const wLeft2 = this.w - xLeft + outerAnimHalf; | |
| 2322 | + | const wRight2 = this.w - xRight + outerAnimHalf; | |
| 2323 | + | ||
| 2324 | + | if (overLeft > 0) { // Top | |
| 2325 | + | gr.FillSolidRect(xLeft, this.overLeft_y, wLeft, this.bar2_h, this.color[10]); | |
| 2326 | + | grSet.peakmeterBarDesign === 'horizontal_center' && gr.FillSolidRect(xLeft2, this.overLeft_y, wLeft2, this.bar2_h, this.color[10]); | |
| 2327 | + | } | |
| 2328 | + | if (overRight > 0) { // Bottom | |
| 2329 | + | gr.FillSolidRect(xRight, this.overRight_y, wRight, this.bar2_h, this.color[10]); | |
| 2330 | + | grSet.peakmeterBarDesign === 'horizontal_center' && gr.FillSolidRect(xRight2, this.overRight_y, wRight2, this.bar2_h, this.color[10]); | |
| 2331 | + | } | |
| 2332 | + | } | |
| 2333 | + | ||
| 2334 | + | /** | |
| 2335 | + | * Draws the middle bars in the peakmeter bar designs. | |
| 2336 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 2337 | + | * @private | |
| 2338 | + | */ | |
| 2339 | + | drawMiddleBars(gr) { | |
| 2340 | + | if (!grSet.peakmeterBarMiddleBars) return; | |
| 2341 | + | ||
| 2342 | + | if (grSet.peakmeterBarDesign === 'horizontal') { | |
| 2343 | + | for (let i = 0; i <= this.points_middle; i++) { | |
| 2344 | + | const dbMiddle = this.db_middle[i]; | |
| 2345 | + | const x = this.x + i * this.middleOffset; | |
| 2346 | + | ||
| 2347 | + | if (this.leftPeak > dbMiddle) { | |
| 2348 | + | gr.FillSolidRect(x, this.middleLeft_y, this.middle_w, this.bar2_h, grCol.peakmeterBarProgFill); | |
| 2349 | + | } | |
| 2350 | + | if (this.rightPeak > dbMiddle) { | |
| 2351 | + | gr.FillSolidRect(x, this.middleRight_y, this.middle_w, this.bar2_h, grCol.peakmeterBarProgFill); | |
| 2352 | + | } | |
| 2353 | + | } | |
| 2354 | + | } | |
| 2355 | + | else if (grSet.peakmeterBarDesign === 'horizontal_center') { | |
| 2356 | + | for (let i = 0; i <= this.points_middle; i++) { | |
| 2357 | + | const dbMiddle = this.db_middle[i]; | |
| 2358 | + | const x1 = this.x * 0.5 + this.w * 0.5 - i * this.middleOffset + 1; | |
| 2359 | + | const x2 = this.x + this.w * 0.5 + i * this.middleOffset - 1; | |
| 2360 | + | ||
| 2361 | + | if (this.leftPeak > dbMiddle) { | |
| 2362 | + | gr.FillSolidRect(x1, this.middleLeft_y, this.middle_w, this.bar2_h, grCol.peakmeterBarProgFill); | |
| 2363 | + | gr.FillSolidRect(x2, this.middleLeft_y, this.middle_w, this.bar2_h, grCol.peakmeterBarProgFill); | |
| 2364 | + | } | |
| 2365 | + | if (this.rightPeak > dbMiddle) { | |
| 2366 | + | gr.FillSolidRect(x1, this.middleRight_y, this.middle_w, this.bar2_h, grCol.peakmeterBarProgFill); | |
| 2367 | + | gr.FillSolidRect(x2, this.middleRight_y, this.middle_w, this.bar2_h, grCol.peakmeterBarProgFill); | |
| 2368 | + | } | |
| 2369 | + | } | |
| 2370 | + | } | |
| 2371 | + | } | |
| 2372 | + | ||
| 2373 | + | /** | |
| 2374 | + | * Draws the progress bar in the peakmeter bar designs. | |
| 2375 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 2376 | + | * @private | |
| 2377 | + | */ | |
| 2378 | + | drawProgressBar(gr) { | |
| 2379 | + | if (!fb.IsPlaying || fb.PlaybackLength <= 0 || !grSet.peakmeterBarProgBar) return; | |
| 2380 | + | ||
| 2381 | + | const playbackRatio = fb.PlaybackTime / fb.PlaybackLength; | |
| 2382 | + | this.progressLength = Math.floor(this.w * (grSet.peakmeterBarDesign === 'horizontal_center' ? 0.5 : 1) * playbackRatio); | |
| 2383 | + | ||
| 2384 | + | if (grSet.peakmeterBarDesign === 'horizontal') { | |
| 2385 | + | gr.FillSolidRect(this.x, this.middleLeft_y, this.w, this.bar_h, grCol.peakmeterBarProg); | |
| 2386 | + | gr.FillSolidRect(this.x, this.middleLeft_y, this.progressLength, this.bar_h, grCol.peakmeterBarProgFill); | |
| 2387 | + | } | |
| 2388 | + | else if (grSet.peakmeterBarDesign === 'horizontal_center') { | |
| 2389 | + | gr.FillSolidRect(this.x, this.middleLeft_y, this.w, this.bar_h, grCol.peakmeterBarProg); | |
| 2390 | + | gr.FillSolidRect(this.x + this.w * 0.5 - this.progressLength, this.middleLeft_y, this.progressLength, this.bar_h, grCol.peakmeterBarProgFill); | |
| 2391 | + | gr.FillSolidRect(this.x + this.w * 0.5, this.middleLeft_y, this.progressLength, this.bar_h, grCol.peakmeterBarProgFill); | |
| 2392 | + | } | |
| 2393 | + | else if (grSet.peakmeterBarDesign === 'vertical') { | |
| 2394 | + | gr.FillSolidRect(this.x, this.y + this.h - this.vertBar_h, this.w, this.bar_h, grCol.peakmeterBarProg); | |
| 2395 | + | gr.FillSolidRect(this.x, this.y + this.h - this.vertBar_h, this.progressLength, this.bar_h, grCol.peakmeterBarVertProgFill); | |
| 2396 | + | } | |
| 2397 | + | } | |
| 2398 | + | ||
| 2399 | + | /** | |
| 2400 | + | * Draws the grid in the peakmeter bar designs. | |
| 2401 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 2402 | + | */ | |
| 2403 | + | drawBarGrid(gr) { | |
| 2404 | + | if (!grSet.peakmeterBarGrid) return; | |
| 2405 | + | gr.FillSolidRect(this.x, this.outerLeft_y, this.w, this.bar_h, grCol.peakmeterBarProg); | |
| 2406 | + | gr.FillSolidRect(this.x, this.outerRight_y, this.w, this.bar_h, grCol.peakmeterBarProg); | |
| 2407 | + | } | |
| 2408 | + | ||
| 2409 | + | /** | |
| 2410 | + | * Draws the bar info in the peakmeter bar designs. | |
| 2411 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 2412 | + | */ | |
| 2413 | + | drawBarInfo(gr) { | |
| 2414 | + | if (!grSet.peakmeterBarInfo) return; | |
| 2415 | + | ||
| 2416 | + | const infoTextColor = grCol.lowerBarArtist; | |
| 2417 | + | ||
| 2418 | + | if (grSet.peakmeterBarDesign === 'horizontal') { | |
| 2419 | + | const text_db_w = gr.CalcTextWidth('db', this.textFont); | |
| 2420 | + | ||
| 2421 | + | for (let i = 4; i <= this.points; i += 2) { | |
| 2422 | + | const text_w = gr.CalcTextWidth(this.db[i], this.textFont); | |
| 2423 | + | gr.GdiDrawText(this.db[i], this.textFont, infoTextColor, this.x + this.offset * i - text_w * 0.5, this.text_y, this.w, this.h); | |
| 2424 | + | } | |
| 2425 | + | ||
| 2426 | + | gr.GdiDrawText('db', this.textFont, infoTextColor, this.x + this.offset * 2 - text_db_w, this.text_y, this.w, this.h); | |
| 2427 | + | } | |
| 2428 | + | else if (grSet.peakmeterBarDesign === 'horizontal_center') { | |
| 2429 | + | const text_db_w = gr.CalcTextWidth('db', this.textFont); | |
| 2430 | + | ||
| 2431 | + | for (let i = 4; i <= this.points; i += 2) { | |
| 2432 | + | const textRight_w = gr.CalcTextWidth(this.db[i], this.textFont); | |
| 2433 | + | const textLeft_w2 = gr.CalcTextWidth(`${this.db[this.points + 3 - i]}-`, this.textFont); | |
| 2434 | + | ||
| 2435 | + | gr.GdiDrawText(this.db[i], this.textFont, infoTextColor, this.w * 0.5 + this.offset * i - textRight_w * 0.5, this.text_y, this.w, this.h); | |
| 2436 | + | gr.GdiDrawText(this.db[this.points + 3 - i], this.textFont, infoTextColor, this.x + this.offset * i - textLeft_w2 * 1.5, this.text_y, this.w, this.h); | |
| 2437 | + | } | |
| 2438 | + | ||
| 2439 | + | gr.GdiDrawText('db', this.textFont, infoTextColor, this.w * 0.5 + this.offset * 2 - text_db_w * 0.5, this.text_y, this.w, this.h); | |
| 2440 | + | } | |
| 2441 | + | else if (grSet.peakmeterBarDesign === 'vertical') { | |
| 2442 | + | for (let i = 0; i <= this.points_vert; i++) { | |
| 2443 | + | const dbLeft = this.db_vert[i]; | |
| 2444 | + | const dbRight = this.db_vert[this.points_vert - 1 - i]; | |
| 2445 | + | ||
| 2446 | + | const textLeft_w = gr.CalcTextWidth(`${dbLeft}--`, this.textFont); | |
| 2447 | + | const textRight_w = gr.CalcTextWidth(`${dbRight}--`, this.textFont); | |
| 2448 | + | const textLeft_x = this.vertLeft_x + this.vertBar_offset * i - textLeft_w / 2 + (this.vertBar_offset - this.vertBar_w); | |
| 2449 | + | const textRight_x = this.vertRight_x + this.vertBar_offset * i - textRight_w / 2 + (this.vertBar_offset - this.vertBar_w); | |
| 2450 | + | ||
| 2451 | + | gr.GdiDrawText(dbLeft % 2 === 0 ? dbLeft : '', this.textFont, infoTextColor, textLeft_x, this.y, grm.ui.ww, grm.ui.wh); | |
| 2452 | + | gr.GdiDrawText(dbRight % 2 === 0 ? dbRight : '', this.textFont, infoTextColor, textRight_x, this.y, grm.ui.ww, grm.ui.wh); | |
| 2453 | + | } | |
| 2454 | + | } | |
| 2455 | + | } | |
| 2456 | + | // #endregion | |
| 2457 | + | ||
| 2458 | + | // * PUBLIC METHODS - INITIALIZATION * // | |
| 2459 | + | // #region PUBLIC METHODS - INITIALIZATION | |
| 2460 | + | /** | |
| 2461 | + | * Initializes the decibel arrays for different configurations. | |
| 2462 | + | */ | |
| 2463 | + | initDecibel() { | |
| 2464 | + | this.db = [ | |
| 2465 | + | -20, -19.5, -19, -18.5, -18, -17.5, -17, -16.5, -16, -15.5, -15, -14.5, -14, -13.5, -13, -12.5, -12, -11.5, -11, -10.5, | |
| 2466 | + | -10, -9.5, -9, -8.5, -8, -7.5, -7, -6.5, -6, -5.5, -5, -4.5, -4, -3.5, -3, -2.5, -2, -1.5, -1, -0.5, 0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5 | |
| 2467 | + | ]; | |
| 2468 | + | this.db_middle = [ | |
| 2469 | + | -70, -65, -62.5, -60, -57.5, -55, -52.5, -50, -47.5, -45, -42.5, -40, -37.5, -35, -32.5, -30, -27.5, -25, -22.5 | |
| 2470 | + | ]; | |
| 2471 | + | this.db_vert = { | |
| 2472 | + | 220: [-20, -19, -18, -17, -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2], | |
| 2473 | + | 215: [-15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2], | |
| 2474 | + | 210: [-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2], | |
| 2475 | + | 320: [-20, -19, -18, -17, -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3], | |
| 2476 | + | 315: [-15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3], | |
| 2477 | + | 310: [-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3], | |
| 2478 | + | 520: [-20, -19, -18, -17, -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5], | |
| 2479 | + | 515: [-15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5], | |
| 2480 | + | 510: [-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5] | |
| 2481 | + | }[grSet.peakmeterBarVertDbRange]; | |
| 2482 | + | } | |
| 2483 | + | ||
| 2484 | + | /** | |
| 2485 | + | * Initializes the points for different decibel arrays. | |
| 2486 | + | */ | |
| 2487 | + | initPoints() { | |
| 2488 | + | this.points_middle = this.db_middle.length; | |
| 2489 | + | this.points = this.db.length; | |
| 2490 | + | this.points_vert = this.db_vert.length; | |
| 2491 | + | ||
| 2492 | + | for (let i = 0; i <= this.points_vert; i++) { | |
| 2493 | + | this.leftPeaks_s[i] = 0; | |
| 2494 | + | this.rightPeaks_s[i] = 0; | |
| 2495 | + | } | |
| 2496 | + | } | |
| 2497 | + | ||
| 2498 | + | /** | |
| 2499 | + | * Initializes the peaks arrays for left and right channels. | |
| 2500 | + | */ | |
| 2501 | + | initPeaks() { | |
| 2502 | + | this.leftPeaks_s = new Array(this.points_vert + 1).fill(0); | |
| 2503 | + | this.rightPeaks_s = new Array(this.points_vert + 1).fill(0); | |
| 2504 | + | } | |
| 2505 | + | ||
| 2506 | + | /** | |
| 2507 | + | * Initializes the separator index based on the decibel array. | |
| 2508 | + | */ | |
| 2509 | + | initSeparator() { | |
| 2510 | + | this.separator = this.db.indexOf(0); | |
| 2511 | + | this.sep1 = this.separator; | |
| 2512 | + | this.sep2 = this.points - this.sep1; | |
| 2513 | + | } | |
| 2514 | + | ||
| 2515 | + | /** | |
| 2516 | + | * Initializes bar geometry properties. | |
| 2517 | + | */ | |
| 2518 | + | initGeometry() { | |
| 2519 | + | this.bar_h = grSet.layout !== 'default' ? SCALE(2) : SCALE(4); | |
| 2520 | + | ||
| 2521 | + | this.offset = (grSet.peakmeterBarDesign === 'horizontal_center' ? this.w * 0.5 : this.w) / this.points; | |
| 2522 | + | this.middleOffset = (grSet.peakmeterBarDesign === 'horizontal_center' ? this.w * 0.5 : this.w) / this.points_middle; | |
| 2523 | + | this.middle_w = this.middleOffset - (grSet.peakmeterBarGaps ? 1 : 0); | |
| 2524 | + | this.w2 = this.offset - (grSet.peakmeterBarGaps ? 1 : 0); | |
| 2525 | + | ||
| 2526 | + | this.vertBar_offset = ((this.w / this.points_vert) + ((grSet.peakmeterBarVertSize === 'min' ? 2 : grSet.peakmeterBarVertSize) / this.points_vert * 0.5)) * 0.5; | |
| 2527 | + | this.vertBar_w = grSet.peakmeterBarVertSize === 'min' ? Math.ceil(this.vertBar_offset * 0.1 * 0.5) : this.vertBar_offset - grSet.peakmeterBarVertSize * 0.5; | |
| 2528 | + | this.vertBar_h = 2; | |
| 2529 | + | this.vertLeft_x = this.x; | |
| 2530 | + | this.vertRight_x = this.vertLeft_x + this.vertBar_offset * this.points_vert; | |
| 2531 | + | } | |
| 2532 | + | // #endregion | |
| 2533 | + | ||
| 2534 | + | // * PUBLIC METHODS - COMMON * // | |
| 2535 | + | // #region PUBLIC METHODS - COMMON | |
| 2536 | + | /** | |
| 2537 | + | * Resets the state of the peakmeter bar. | |
| 2538 | + | */ | |
| 2539 | + | reset() { | |
| 2540 | + | this.leftLevel = 0; | |
| 2541 | + | this.leftPeak = 0; | |
| 2542 | + | this.rightLevel = 0; | |
| 2543 | + | this.rightPeak = 0; | |
| 2544 | + | this.leftPeaks_s = []; | |
| 2545 | + | this.rightPeaks_s = []; | |
| 2546 | + | this.progressLength = 0; | |
| 2547 | + | this.tooltipText = ''; | |
| 2548 | + | } | |
| 2549 | + | ||
| 2550 | + | /** | |
| 2551 | + | * Sets all vertical peakmeter bar positions. | |
| 2552 | + | * Bars are ordered from top to bottom. | |
| 2553 | + | * @param {number} y - The y-coordinate. | |
| 2554 | + | */ | |
| 2555 | + | setY(y = grm.ui.seekbarY) { | |
| 2556 | + | this.y = y; | |
| 2557 | + | this.overLeft_y = this.y; | |
| 2558 | + | this.outerLeft_y = this.overLeft_y + this.bar2_h; | |
| 2559 | + | this.mainLeft_y = this.outerLeft_y + this.bar_h; | |
| 2560 | + | this.middleLeft_y = this.mainLeft_y + this.bar_h + SCALE(1); | |
| 2561 | + | this.middleRight_y = this.middleLeft_y + this.bar2_h; | |
| 2562 | + | this.mainRight_y = this.middleRight_y + this.bar2_h + SCALE(1); | |
| 2563 | + | this.outerRight_y = this.mainRight_y + this.bar_h; | |
| 2564 | + | this.overRight_y = this.outerRight_y + this.bar_h; | |
| 2565 | + | this.text_y = this.outerRight_y + this.bar_h * 2; | |
| 2566 | + | } | |
| 2567 | + | ||
| 2568 | + | /** | |
| 2569 | + | * Monitors volume levels and peaks and sets horizontal or vertical animations based on peakmeterBarDesign. | |
| 2570 | + | */ | |
| 2571 | + | setAnimation() { | |
| 2572 | + | // * Set horizontal animation | |
| 2573 | + | if (['horizontal', 'horizontal_center'].includes(grSet.peakmeterBarDesign)) { | |
| 2574 | + | const increment1 = 0.09; // 0.3 ** 2 | |
| 2575 | + | const increment2 = 1.21; // 1.1 ** 2 | |
| 2576 | + | ||
| 2577 | + | // * Main left middle peaks | |
| 2578 | + | if (this.mainLeftAnim_x <= this.mainLeft_x) { | |
| 2579 | + | this.mainLeftAnim_x = this.mainLeft_x; | |
| 2580 | + | this.mainLeftAnim2_x = this.mainLeft_x; | |
| 2581 | + | this.mainLeft_k = 0; | |
| 2582 | + | this.mainLeft2_k = 0; | |
| 2583 | + | } | |
| 2584 | + | this.mainLeft_k += increment1; | |
| 2585 | + | this.mainLeftAnim_x -= this.mainLeft_k; | |
| 2586 | + | this.mainLeft2_k += increment2; | |
| 2587 | + | this.mainLeftAnim2_x += this.mainLeft2_k; | |
| 2588 | + | ||
| 2589 | + | // * Main right middle peaks | |
| 2590 | + | if (this.mainRightAnim_x <= this.mainRight_x) { | |
| 2591 | + | this.mainRightAnim_x = this.mainRight_x; | |
| 2592 | + | this.mainRightAnim2_x = this.mainRight_x; | |
| 2593 | + | this.mainRight_k = 0; | |
| 2594 | + | this.mainRight2_k = 0; | |
| 2595 | + | } | |
| 2596 | + | this.mainRight_k += increment1; | |
| 2597 | + | this.mainRightAnim_x -= this.mainRight_k; | |
| 2598 | + | this.mainRight2_k += increment2; | |
| 2599 | + | this.mainRightAnim2_x += this.mainRight2_k; | |
| 2600 | + | ||
| 2601 | + | // * Outer left peaks | |
| 2602 | + | if (this.outerLeftAnim_x <= this.outerLeft_w) { | |
| 2603 | + | this.outerLeftAnim_x = this.outerLeft_w; | |
| 2604 | + | this.outerLeft_k = 0; | |
| 2605 | + | this.outerLeftAnim_w = this.outerLeft_w - this.outerLeft_w_old < 1 ? this.outerLeftAnim_w : this.outerLeft_w - this.outerLeft_w_old + 10; | |
| 2606 | + | } else { | |
| 2607 | + | this.outerLeft_w_old = this.outerLeft_w; | |
| 2608 | + | } | |
| 2609 | + | this.outerLeft_k += increment1; | |
| 2610 | + | this.outerLeftAnim_x -= this.outerLeft_k; | |
| 2611 | + | this.outerLeftAnim_w -= this.outerLeft_k * 2; | |
| 2612 | + | ||
| 2613 | + | // * Outer right peaks | |
| 2614 | + | if (this.outerRightAnim_x <= this.outerRight_w) { | |
| 2615 | + | this.outerRightAnim_x = this.outerRight_w; | |
| 2616 | + | this.outerRight_k = 0; | |
| 2617 | + | this.outerRightAnim_w = this.outerRight_w - this.outerRight_w_old < 1 ? this.outerRightAnim_w : this.outerRight_w - this.outerRight_w_old + 10; | |
| 2618 | + | } else { | |
| 2619 | + | this.outerRight_w_old = this.outerRight_w; | |
| 2620 | + | } | |
| 2621 | + | this.outerRight_k += increment1; | |
| 2622 | + | this.outerRightAnim_x -= this.outerRight_k; | |
| 2623 | + | this.outerRightAnim_w -= this.outerRight_k * 2; | |
| 2624 | + | } | |
| 2625 | + | // * Set vertical animation | |
| 2626 | + | else if (grSet.peakmeterBarDesign === 'vertical') { | |
| 2627 | + | for (let j = 0; j < this.leftPeaks_s.length; j++) { | |
| 2628 | + | this.leftPeaks_s[j] = this.leftPeaks_s[j] < this.h ? this.leftPeaks_s[j] + 2 : this.h; | |
| 2629 | + | } | |
| 2630 | + | for (let j = 0; j < this.rightPeaks_s.length; j++) { | |
| 2631 | + | this.rightPeaks_s[j] = this.rightPeaks_s[j] < this.h ? this.rightPeaks_s[j] + 2 : this.h; | |
| 2632 | + | } | |
| 2633 | + | } | |
| 2634 | + | } | |
| 2635 | + | ||
| 2636 | + | /** | |
| 2637 | + | * Sets the peakmeter bar colors. | |
| 2638 | + | * @param {FbMetadbHandle} metadb - The metadb of the track. | |
| 2639 | + | */ | |
| 2640 | + | setColors(metadb = fb.GetNowPlaying()) { | |
| 2641 | + | if (grSet.seekbar !== 'peakmeterbar') return; | |
| 2642 | + | ||
| 2643 | + | let img = gdi.CreateImage(1, 1); | |
| 2644 | + | const g = img.GetGraphics(); | |
| 2645 | + | img.ReleaseGraphics(g); | |
| 2646 | + | ||
| 2647 | + | if (metadb) img = utils.GetAlbumArtV2(metadb, 0); | |
| 2648 | + | ||
| 2649 | + | if (img) { | |
| 2650 | + | try { | |
| 2651 | + | grm.ui.albumArtCorrupt = false; | |
| 2652 | + | // this.colors = JSON.parse(img.GetColourSchemeJSON(4)); | |
| 2653 | + | this.c1 = grCol.peakmeterBarFillMiddle; // this.colors[1].col; | |
| 2654 | + | this.c2 = grCol.peakmeterBarFillTop; // this.colors[2].col; | |
| 2655 | + | this.c3 = grCol.peakmeterBarFillBack; // this.colors[3].col; | |
| 2656 | + | } catch (e) { | |
| 2657 | + | grm.ui.noArtwork = true; | |
| 2658 | + | grm.ui.noAlbumArtStub = true; | |
| 2659 | + | grm.ui.albumArtCorrupt = true; | |
| 2660 | + | this.setDefaultColors(); | |
| 2661 | + | } | |
| 2662 | + | } else { | |
| 2663 | + | this.setDefaultColors(); | |
| 2664 | + | } | |
| 2665 | + | ||
| 2666 | + | this.color = []; | |
| 2667 | + | this.color1 = [this.c2, this.c3]; | |
| 2668 | + | this.color2 = [this.c3, this.c1]; | |
| 2669 | + | ||
| 2670 | + | for (let j = 0; j < this.sep1; j++) { | |
| 2671 | + | this.color.push(BlendColors(this.color1[0], this.color1[1], j / this.sep1)); | |
| 2672 | + | } | |
| 2673 | + | for (let j = 0; j < this.sep2; j++) { | |
| 2674 | + | this.color.push(BlendColors(this.color2[0], this.color2[1], j / this.sep2)); | |
| 2675 | + | } | |
| 2676 | + | } | |
| 2677 | + | ||
| 2678 | + | /** | |
| 2679 | + | * Sets the default peakmeter bar colors. | |
| 2680 | + | */ | |
| 2681 | + | setDefaultColors() { | |
| 2682 | + | this.c1 = grCol.peakmeterBarFillMiddle; // RGB(0, 200, 255); | |
| 2683 | + | this.c2 = grCol.peakmeterBarFillTop; // RGB(255, 255, 0); | |
| 2684 | + | this.c3 = grCol.peakmeterBarFillBack; // RGB(230, 230, 30); | |
| 2685 | + | this.color1 = [this.c3, this.c1]; | |
| 2686 | + | this.color2 = [this.c2, this.c3]; | |
| 2687 | + | } | |
| 2688 | + | ||
| 2689 | + | /** | |
| 2690 | + | * Sets monitoring of audio levels and peaks based on playback state. | |
| 2691 | + | * Converts and stores volume levels and peaks for both left and right channels in decibels. | |
| 2692 | + | */ | |
| 2693 | + | setMonitoring() { | |
| 2694 | + | if (!AudioWizard) return; | |
| 2695 | + | ||
| 2696 | + | this.leftLevel = AudioWizard.PeakmeterAdjustedLeftRMS; | |
| 2697 | + | this.rightLevel = AudioWizard.PeakmeterAdjustedRightRMS; | |
| 2698 | + | this.leftPeak = AudioWizard.PeakmeterAdjustedLeftSamplePeak; | |
| 2699 | + | this.rightPeak = AudioWizard.PeakmeterAdjustedRightSamplePeak; | |
| 2700 | + | } | |
| 2701 | + | ||
| 2702 | + | /** | |
| 2703 | + | * Sets the playback time of the progress bar. | |
| 2704 | + | * @param {number} x - The x-coordinate. | |
| 2705 | + | * @private | |
| 2706 | + | */ | |
| 2707 | + | setPlaybackTime(x) { | |
| 2708 | + | const clampedPosition = Clamp((x - this.x) / this.w, 0, 1); | |
| 2709 | + | const newPlaybackTime = clampedPosition * fb.PlaybackLength; | |
| 2710 | + | if (fb.PlaybackTime !== newPlaybackTime) { | |
| 2711 | + | fb.PlaybackTime = newPlaybackTime; | |
| 2712 | + | } | |
| 2713 | + | } | |
| 2714 | + | ||
| 2715 | + | /** | |
| 2716 | + | * Sets the refresh rate for the peakmeter bar. | |
| 2717 | + | */ | |
| 2718 | + | setRefreshRate() { | |
| 2719 | + | if (grm.ui.isStreaming) { // Radio streaming refresh rate | |
| 2720 | + | grm.ui.seekbarTimerInterval = FPS._1; | |
| 2721 | + | } | |
| 2722 | + | else if (grSet.peakmeterBarRefreshRate !== 'variable') { // Fixed refresh rate | |
| 2723 | + | grm.ui.seekbarTimerInterval = grSet.peakmeterBarRefreshRate; | |
| 2724 | + | } | |
| 2725 | + | else { // Variable refresh rate calculation | |
| 2726 | + | const now = Date.now(); | |
| 2727 | + | if (this.updateTimeLast && (now - this.updateTimeLast) < 250) return; // Check every 250 ms | |
| 2728 | + | this.updateTimeLast = now; | |
| 2729 | + | ||
| 2730 | + | if (this.profilerPaintTimeLast === undefined) { | |
| 2731 | + | this.profilerPaintTimeLast = grm.ui.seekbarProfiler.Time; | |
| 2732 | + | } | |
| 2733 | + | ||
| 2734 | + | const timeDifference = grm.ui.seekbarProfiler.Time - this.profilerPaintTimeLast; | |
| 2735 | + | grm.ui.seekbarTimerInterval = Clamp(grm.ui.seekbarTimerInterval + (timeDifference > 0 ? 8 : -5), FPS._20, FPS._10); | |
| 2736 | + | this.profilerPaintTimeLast = grm.ui.seekbarProfiler.Time; | |
| 2737 | + | ||
| 2738 | + | grm.ui.clearTimer('seekbar', true); | |
| 2739 | + | grm.ui.seekbarTimer = !fb.IsPaused ? setInterval(() => grm.ui.refreshSeekbar(), grm.ui.seekbarTimerInterval) : null; | |
| 2740 | + | } | |
| 2741 | + | } | |
| 2742 | + | ||
| 2743 | + | /** | |
| 2744 | + | * Starts Audio Wizard's peakmeter real-time monitoring. | |
| 2745 | + | */ | |
| 2746 | + | startPeakmeter() { | |
| 2747 | + | const refreshRate = grSet.peakmeterBarRefreshRate === 'variable' ? 17 : grSet.peakmeterBarRefreshRate; | |
| 2748 | + | AudioWizard && AudioWizard.StartPeakmeterMonitoring(refreshRate, 50); | |
| 2749 | + | } | |
| 2750 | + | ||
| 2751 | + | /** | |
| 2752 | + | * Stops Audio Wizard's peakmeter real-time monitoring. | |
| 2753 | + | */ | |
| 2754 | + | stopPeakmeter() { | |
| 2755 | + | AudioWizard && AudioWizard.StopPeakmeterMonitoring(); | |
| 2756 | + | } | |
| 2757 | + | // #endregion | |
| 2758 | + | ||
| 2759 | + | // * CALLBACKS * // | |
| 2760 | + | // #region CALLBACKS | |
| 2761 | + | /** | |
| 2762 | + | * Checks if the mouse is within the boundaries of the peakmeter bar. | |
| 2763 | + | * @param {number} x - The x-coordinate. | |
| 2764 | + | * @param {number} y - The y-coordinate. | |
| 2765 | + | * @returns {boolean} True or false. | |
| 2766 | + | */ | |
| 2767 | + | mouseInThis(x, y) { | |
| 2768 | + | return (x >= this.x && y >= this.y && x < this.x + this.w && y <= this.y + this.h); | |
| 2769 | + | } | |
| 2770 | + | ||
| 2771 | + | /** | |
| 2772 | + | * Handles left mouse button down click events and enables dragging. | |
| 2773 | + | * @param {number} x - The x-coordinate. | |
| 2774 | + | * @param {number} y - The y-coordinate. | |
| 2775 | + | */ | |
| 2776 | + | on_mouse_lbtn_down(x, y) { | |
| 2777 | + | this.drag = true; | |
| 2778 | + | } | |
| 2779 | + | ||
| 2780 | + | /** | |
| 2781 | + | * Handles left mouse button up click events and disables dragging and updates the playback time. | |
| 2782 | + | * @param {number} x - The x-coordinate. | |
| 2783 | + | * @param {number} y - The y-coordinate. | |
| 2784 | + | */ | |
| 2785 | + | on_mouse_lbtn_up(x, y) { | |
| 2786 | + | this.drag = false; | |
| 2787 | + | if (this.on_mouse && this.mouseInThis(x, y)) { | |
| 2788 | + | this.setPlaybackTime(x); | |
| 2789 | + | } | |
| 2790 | + | } | |
| 2791 | + | ||
| 2792 | + | /** | |
| 2793 | + | * Handle mouse leave events. | |
| 2794 | + | */ | |
| 2795 | + | on_mouse_leave() { | |
| 2796 | + | this.drag = false; | |
| 2797 | + | this.on_mouse = false; | |
| 2798 | + | } | |
| 2799 | + | ||
| 2800 | + | /** | |
| 2801 | + | * Handles mouse movement events and updates the playback time based on the mouse movement if a drag event is occurring. | |
| 2802 | + | * @param {number} x - The x-coordinate. | |
| 2803 | + | * @param {number} y - The y-coordinate. | |
| 2804 | + | */ | |
| 2805 | + | on_mouse_move(x, y) { | |
| 2806 | + | this.on_mouse = true; | |
| 2807 | + | this.pos_x = x <= this.textWidth ? this.x + this.textWidth : this.x + x; | |
| 2808 | + | this.pos_y = y <= this.textHeight ? this.textHeight : y; | |
| 2809 | + | ||
| 2810 | + | if (this.drag) { | |
| 2811 | + | this.setPlaybackTime(x); | |
| 2812 | + | } | |
| 2813 | + | ||
| 2814 | + | if (this.tooltipText) { | |
| 2815 | + | this.wheel = false; | |
| 2816 | + | this.tooltipTimer = false; | |
| 2817 | + | this.tooltipText = ''; | |
| 2818 | + | grm.ttip.stop(); | |
| 2819 | + | window.Repaint(); | |
| 2820 | + | } | |
| 2821 | + | } | |
| 2822 | + | ||
| 2823 | + | /** | |
| 2824 | + | * Handles mouse wheel events and controls the volume offset. | |
| 2825 | + | * @param {number} step - The wheel scroll direction. | |
| 2826 | + | */ | |
| 2827 | + | on_mouse_wheel(step) { | |
| 2828 | + | this.wheel = true; | |
| 2829 | + | ||
| 2830 | + | if (!AudioWizard) return; | |
| 2831 | + | ||
| 2832 | + | AudioWizard.PeakmeterOffset = AudioWizard.PeakmeterOffset + step; | |
| 2833 | + | this.tooltipText = `${Math.round(AudioWizard.PeakmeterOffset)} db`; | |
| 2834 | + | grm.ttip.showImmediate(this.tooltipText); | |
| 2835 | + | } | |
| 2836 | + | ||
| 2837 | + | /** | |
| 2838 | + | * Updates peakmeter bar colors when playing a new track. | |
| 2839 | + | * @param {FbMetadbHandle} metadb - The metadb of the track. | |
| 2840 | + | */ | |
| 2841 | + | on_playback_new_track(metadb) { | |
| 2842 | + | if (!metadb) return; | |
| 2843 | + | this.startPeakmeter(); | |
| 2844 | + | this.progressLength = 0; | |
| 2845 | + | this.setColors(metadb); | |
| 2846 | + | } | |
| 2847 | + | ||
| 2848 | + | /** | |
| 2849 | + | * Resets the peakmeter bar on playback stop. | |
| 2850 | + | * @param {number} reason - The type of playback stop. | |
| 2851 | + | */ | |
| 2852 | + | on_playback_stop(reason = -1) { | |
| 2853 | + | if (['progressbar', 'waveformbar'].includes(grSet.seekbar)) { | |
| 2854 | + | return; | |
| 2855 | + | } | |
| 2856 | + | ||
| 2857 | + | if (reason !== 2) { | |
| 2858 | + | this.stopPeakmeter(); | |
| 2859 | + | this.reset(); | |
| 2860 | + | window.Repaint(); | |
| 2861 | + | } | |
| 2862 | + | } | |
| 2863 | + | ||
| 2864 | + | /** | |
| 2865 | + | * Sets the size and position of the peakmeter bar and updates them on window resizing. | |
| 2866 | + | */ | |
| 2867 | + | on_size() { | |
| 2868 | + | this.x = grm.ui.edgeMargin; | |
| 2869 | + | this.y = grm.ui.seekbarY; | |
| 2870 | + | this.w = grm.ui.ww - grm.ui.edgeMarginBoth; | |
| 2871 | + | this.h = grm.ui.seekbarHeight; | |
| 2872 | + | ||
| 2873 | + | this.initGeometry(); | |
| 2874 | + | this.setY(); | |
| 2875 | + | ||
| 2876 | + | this.textFont = gdi.Font('Segoe UI', HD_4K(9, 16), 1); | |
| 2877 | + | } | |
| 2878 | + | // #endregion | |
| 2879 | + | } | |
| 2880 | + | ||
| 2881 | + | ||
| 2882 | + | ////////////////////// | |
| 2883 | + | // * WAVEFORM BAR * // | |
| 2884 | + | ////////////////////// | |
| 2885 | + | /** | |
| 2886 | + | * A class that creates the waveform bar in the lower bar when enabled. | |
| 2887 | + | * Quick access via right click context menu on lower bar. | |
| 2888 | + | */ | |
| 2889 | + | class WaveformBar { | |
| 2890 | + | /** | |
| 2891 | + | * Creates the `WaveformBar` instance. | |
| 2892 | + | */ | |
| 2893 | + | constructor() { | |
| 2894 | + | // * Dependencies | |
| 2895 | + | include(`${fb.ProfilePath}georgia-reborn\\externals\\Codepages.js`); | |
| 2896 | + | include(`${fb.ProfilePath}georgia-reborn\\externals\\lz-utf8\\lzutf8.js`); | |
| 2897 | + | include(`${fb.ProfilePath}georgia-reborn\\externals\\lz-string\\lz-string.min.js`); | |
| 2898 | + | ||
| 2899 | + | /** @private @type {string} The match pattern used to create folder path. */ | |
| 2900 | + | this.matchPattern = '$replace($ascii([$replace($if2($meta(ALBUMARTIST,0),$meta(ARTIST,0)),\\,)]\\[$replace([$if3(%original release date%,%originaldate%,%date%,%fy_upload_date%,) - ]%ALBUM%,\\,)]\\%TRACKNUMBER% - $replace(%TITLE%,\\,)), ?,,= ,,?,)'; | |
| 2901 | + | /** @private @type {boolean} The debug flag for logging debug information. */ | |
| 2902 | + | this.debug = false; | |
| 2903 | + | /** @private @type {boolean} The profile flag for logging performance information. */ | |
| 2904 | + | this.profile = false; | |
| 2905 | + | /** @private @type {FbProfiler} The profiler for logging performance information. */ | |
| 2906 | + | this.profiler = null; | |
| 2907 | + | ||
| 2908 | + | // * Easy access | |
| 2909 | + | /** @public @type {number} The x-coordinate of the waveform bar. */ | |
| 2910 | + | this.x = grm.ui.edgeMargin; | |
| 2911 | + | /** @public @type {number} The y-coordinate of the waveform bar. */ | |
| 2912 | + | this.y = 0; | |
| 2913 | + | /** @public @type {number} The width of the waveform bar. */ | |
| 2914 | + | this.w = grm.ui.ww - grm.ui.edgeMarginBoth; | |
| 2915 | + | /** @public @type {number} The height of the waveform bar. */ | |
| 2916 | + | this.h = grm.ui.seekbarHeight; | |
| 2917 | + | ||
| 2918 | + | // * Internals | |
| 2919 | + | /** @private @type {boolean} The active state of the waveform bar. */ | |
| 2920 | + | this.active = true; | |
| 2921 | + | /** @private @type {string} The title format used for the waveform bar. */ | |
| 2922 | + | this.Tf = fb.TitleFormat(this.matchPattern); | |
| 2923 | + | /** @private @type {number} The maximum step for the title format. */ | |
| 2924 | + | this.TfMaxStep = fb.TitleFormat('[%BPM%]'); | |
| 2925 | + | /** @private @type {string[]} The cache storage for the waveform data. */ | |
| 2926 | + | this.cache = null; | |
| 2927 | + | /** @private @type {string} The directory for the waveform cache. */ | |
| 2928 | + | this.cacheDir = grSet.customWaveformBarDir ? $(`${grCfg.customWaveformBarDir}`, undefined, true) : `${fb.ProfilePath}cache\\waveform\\`; | |
| 2929 | + | /** @private @type {string} The code page for character encoding conversion. */ | |
| 2930 | + | this.codePage = convertCharsetToCodepage('UTF-8'); | |
| 2931 | + | /** @private @type {string} The code page for UTF-16LE character encoding conversion. */ | |
| 2932 | + | this.codePageV2 = convertCharsetToCodepage('UTF-16LE'); | |
| 2933 | + | /** @private @type {number} The queue identifier for the waveform bar. */ | |
| 2934 | + | this.queueId = null; | |
| 2935 | + | /** @private @type {number} The queue interval in milliseconds. */ | |
| 2936 | + | this.queueMs = 1000; | |
| 2937 | + | /** @private @type {string[]} The current waveform data. */ | |
| 2938 | + | this.current = []; | |
| 2939 | + | /** @private @type {number[]} The offset values for the waveform data. */ | |
| 2940 | + | this.offset = []; | |
| 2941 | + | /** @private @type {number} The current step in the waveform animation. */ | |
| 2942 | + | this.step = 0; // 0 - maxStep | |
| 2943 | + | /** @private @type {number} The maximum step for the waveform animation. */ | |
| 2944 | + | this.maxStep = 4; | |
| 2945 | + | /** @private @type {number} The current playback time for the waveform bar. */ | |
| 2946 | + | this.time = 0; | |
| 2947 | + | /** @private @type {boolean} The state indicating if the mouse is down. */ | |
| 2948 | + | this.mouseDown = false; | |
| 2949 | + | /** @private @type {boolean} The state indicating if the file is allowed. Set at checkAllowedFile(). */ | |
| 2950 | + | this.isAllowedFile = true; | |
| 2951 | + | /** @private @type {boolean} The state indicating if the file is a zipped file. Set at checkAllowedFile(). */ | |
| 2952 | + | this.isZippedFile = false; | |
| 2953 | + | /** @private @type {boolean} The state indicating if there was an error. Set at verifyData() after retrying analysis. */ | |
| 2954 | + | this.isError = false; | |
| 2955 | + | /** @private @type {boolean} The state indicating if fallback mode is active. For visualizerFallback, set at checkAllowedFile(). */ | |
| 2956 | + | this.isFallback = false; | |
| 2957 | + | /** @private @type {number} The number of audio channels in the current waveform data. */ | |
| 2958 | + | this.currentChannels = 1; | |
| 2959 | + | ||
| 2960 | + | /** | |
| 2961 | + | * The waveform bar analysis settings. | |
| 2962 | + | * @typedef {object} waveformBarAnalysis | |
| 2963 | + | * @property {string} binaryMode - The analysis mode: 'audioWizard' | 'visualizer'. | |
| 2964 | + | * @property {number} resolution - The temporal resolution in points per second from 1-1000, recommended preset ranges are: | |
| 2965 | + | * - 100 points/sec: Very High Details (10ms, for transient-heavy audio like EDM). | |
| 2966 | + | * - 50 points/sec: High Details (20ms, for mastering and detailed visualization). | |
| 2967 | + | * - 20 points/sec: Standard Details (50ms, matches FFmpeg/astats, ideal for broadcast). | |
| 2968 | + | * - 15 points/sec: Balanced Details (~67ms, for smooth audio like pop or jazz). | |
| 2969 | + | * - 10 points/sec: Low Details (100ms, for basic visualization). | |
| 2970 | + | * - 5 points/sec: Very Low Details (200ms, for low-performance devices, very smooth audio). | |
| 2971 | + | * - 1 points/sec: Minimum Details (1000ms, for ultra-minimal previews on very slow devices). | |
| 2972 | + | * @property {number} timeout - The maximum duration for waveform analysis in milliseconds. | |
| 2973 | + | * @property {string} compressionMode - The compression mode: 'none' | 'utf-8' | 'utf-16' | '7zip'. | |
| 2974 | + | * @property {string} saveMode - The save behavior: 'always' | 'library' | 'never'. | |
| 2975 | + | * @property {boolean} autoAnalysis - Whether to automatically analyze files. | |
| 2976 | + | * @property {boolean} autoDelete - Whether to auto-delete analysis files when unloading the script. | |
| 2977 | + | * @property {boolean} visualizerFallbackAnalysis - Whether to use visualizer mode when analyzing the file. | |
| 2978 | + | * @property {boolean} visualizerFallback - Whether to use visualizer mode for incompatible file formats. | |
| 2979 | + | * @public | |
| 2980 | + | */ | |
| 2981 | + | /** @public @type {waveformBarAnalysis} */ | |
| 2982 | + | this.analysis = { | |
| 2983 | + | binaryMode: grSet.waveformBarMode, | |
| 2984 | + | resolution: grSet.waveformBarResolution, | |
| 2985 | + | timeout: 60000, | |
| 2986 | + | compressionMode: 'utf-16', | |
| 2987 | + | saveMode: grSet.waveformBarSaveMode, | |
| 2988 | + | autoAnalysis: true, | |
| 2989 | + | autoDelete: grSet.waveformBarAutoDelete, | |
| 2990 | + | visualizerFallbackAnalysis: grSet.waveformBarFallbackAnalysis, | |
| 2991 | + | visualizerFallback: grSet.waveformBarFallback | |
| 2992 | + | }; | |
| 2993 | + | ||
| 2994 | + | /** | |
| 2995 | + | * The waveform bar binary settings. | |
| 2996 | + | * @typedef {object} waveformBarBinaries | |
| 2997 | + | * @property {string} visualizer - The visualizer binary to use. | |
| 2998 | + | * @public | |
| 2999 | + | */ | |
| 3000 | + | /** @public @type {waveformBarBinaries} */ | |
| 3001 | + | this.binaries = { | |
| 3002 | + | audioWizard: Component.AudioWizard, | |
| 3003 | + | visualizer: `${fb.ProfilePath}running` | |
| 3004 | + | }; | |
| 3005 | + | ||
| 3006 | + | /** | |
| 3007 | + | * The waveform bar compatible file settings. | |
| 3008 | + | * @typedef {object} waveformBarCompatibility | |
| 3009 | + | * @property {RegExp} audioWizard - The regular expression to test for file types compatible with audioWizard. | |
| 3010 | + | * @public | |
| 3011 | + | */ | |
| 3012 | + | /** @private @type {waveformBarCompatibility} */ | |
| 3013 | + | this.compatibleFiles = { | |
| 3014 | + | audioWizardList: ['2sf', 'aa', 'aac', 'ac3', 'ac4', 'aiff', 'ape', 'cue', 'dff', 'dsf', 'dts', 'eac3', 'flac', 'hmi', 'iso', 'la', 'lpcm', 'm4a', 'minincsf', 'mp2', 'mp3', 'mp4', 'mpc', 'ogg', 'ogx', 'opus', 'ra', 'snd', 'shn', 'spc', 'tak', 'tta', 'vgm', 'wav', 'wma', 'wv'], | |
| 3015 | + | audioWizard: null | |
| 3016 | + | }; | |
| 3017 | + | for (const key of ['audioWizard']) { | |
| 3018 | + | this.compatibleFiles[key] = new RegExp(`\\.(${this.compatibleFiles[`${key}List`].join('|')})$`, 'i'); | |
| 3019 | + | } | |
| 3020 | + | ||
| 3021 | + | /** | |
| 3022 | + | * The waveform bar fallback mode settings for visualizerFallbackAnalysis. | |
| 3023 | + | * @typedef {object} waveformBarFallbackMode | |
| 3024 | + | * @property {boolean} paint - The state that indicates whether to use the paint fallback mode. | |
| 3025 | + | * @property {boolean} analysis - The state that indicates whether to use the analysis fallback mode. | |
| 3026 | + | * @public | |
| 3027 | + | */ | |
| 3028 | + | /** @private @type {waveformBarFallbackMode} */ | |
| 3029 | + | this.fallbackMode = { | |
| 3030 | + | paint: false, | |
| 3031 | + | analysis: false | |
| 3032 | + | }; | |
| 3033 | + | ||
| 3034 | + | /** | |
| 3035 | + | * The waveform bar metrics configuration. | |
| 3036 | + | * @typedef {object} waveformBarMetricsConfig | |
| 3037 | + | * @property {number} count - The number of metrics per frame. | |
| 3038 | + | * @property {object} index - The metric names to their frame indexes (e.g., rms: 0). | |
| 3039 | + | * @property {object} range - The valid ranges for each metric (e.g., [-100, 0] for dB metrics). | |
| 3040 | + | * @property {object} mode - The analysisMode values to metric names (e.g., rms: 'rms'). | |
| 3041 | + | * @public | |
| 3042 | + | */ | |
| 3043 | + | /** @private @type {waveformBarMetricsConfig} */ | |
| 3044 | + | this.metrics = { | |
| 3045 | + | count: 5, | |
| 3046 | + | index: { | |
| 3047 | + | rms: 0, | |
| 3048 | + | rms_peak: 1, | |
| 3049 | + | sample_peak: 2, | |
| 3050 | + | min: 3, | |
| 3051 | + | max: 4 | |
| 3052 | + | }, | |
| 3053 | + | mode: { | |
| 3054 | + | rms: 'rms', | |
| 3055 | + | rms_peak: 'rms_peak', | |
| 3056 | + | sample_peak: 'sample_peak', | |
| 3057 | + | waveform: 'waveform' | |
| 3058 | + | }, | |
| 3059 | + | range: { | |
| 3060 | + | rms: [-100, 0], | |
| 3061 | + | rms_peak: [-100, 0], | |
| 3062 | + | sample_peak: [-100, 0], | |
| 3063 | + | min: [-1, 1], | |
| 3064 | + | max: [-1, 1] | |
| 3065 | + | } | |
| 3066 | + | }; | |
| 3067 | + | ||
| 3068 | + | /** | |
| 3069 | + | * The waveform bar preset settings. | |
| 3070 | + | * @typedef {object} waveformBarPreset | |
| 3071 | + | * @property {string} analysisMode - The waveform bar analysis mode `rms`, `rms_peak`, `sample_peak`, `waveform`. | |
| 3072 | + | * @property {string} barDesign - The waveform bar design `waveform`, `bars`, `dots`, `halfbars`. | |
| 3073 | + | * @property {string} paintMode - The waveform bar paint mode `full`, `partial`. | |
| 3074 | + | * @property {boolean} animate - The flag to display animation. | |
| 3075 | + | * @property {boolean} useBPM - The flag to use synced BPM. | |
| 3076 | + | * @property {boolean} indicator - The flag to show waveform bar progress indicator. | |
| 3077 | + | * @property {boolean} prepaint - The flag to prepaint waveform bar progress. | |
| 3078 | + | * @property {number} prepaintFront - The prepaint waveform bar progress length. | |
| 3079 | + | * @property {boolean} invertHalfbars - The flag to invert waveform bar halfbars. | |
| 3080 | + | * @public | |
| 3081 | + | */ | |
| 3082 | + | /** @public @type {waveformBarPreset} */ | |
| 3083 | + | this.preset = { | |
| 3084 | + | analysisMode: grSet.waveformBarAnalysis, | |
| 3085 | + | barDesign: grSet.waveformBarDesign, | |
| 3086 | + | paintMode: grSet.waveformBarPaint, | |
| 3087 | + | animate: grSet.waveformBarAnimate, | |
| 3088 | + | useBPM: grSet.waveformBarBPM, | |
| 3089 | + | indicator: grSet.waveformBarIndicator, | |
| 3090 | + | prepaint: grSet.waveformBarPrepaint, | |
| 3091 | + | prepaintFront: grSet.waveformBarPrepaintFront, | |
| 3092 | + | invertHalfbars: grSet.waveformBarInvertHalfbars | |
| 3093 | + | }; | |
| 3094 | + | ||
| 3095 | + | /** | |
| 3096 | + | * The waveform bar ui settings. | |
| 3097 | + | * @typedef {object} waveformBarUI | |
| 3098 | + | * @property {number} sizeWave - The width size of drawn waveform. | |
| 3099 | + | * @property {number} sizeBars - The width size of drawn bars. | |
| 3100 | + | * @property {number} sizeDots - The width size of drawn dots. | |
| 3101 | + | * @property {number} sizeHalf - The width size of drawn halfbars. | |
| 3102 | + | * @property {number} sizeNormalizeWidth - The visualizer binary to use. | |
| 3103 | + | * @property {number} refreshRate - The refresh rate in ms when using animations for any type. 100 is smooth enough but the performance hit is high. | |
| 3104 | + | * @public | |
| 3105 | + | */ | |
| 3106 | + | /** @public @type {waveformBarUI} */ | |
| 3107 | + | this.ui = { | |
| 3108 | + | sizeWave: grSet.waveformBarSizeWave, | |
| 3109 | + | sizeBars: grSet.waveformBarSizeBars, | |
| 3110 | + | sizeDots: grSet.waveformBarSizeDots, | |
| 3111 | + | sizeHalf: grSet.waveformBarSizeHalf, | |
| 3112 | + | sizeNormalizeWidth: grSet.waveformBarSizeNormalize, | |
| 3113 | + | refreshRate: grSet.waveformBarRefreshRate === 'variable' ? FPS._5 : grSet.waveformBarRefreshRate | |
| 3114 | + | }; | |
| 3115 | + | ||
| 3116 | + | /** | |
| 3117 | + | * The waveform bar wheel settings. | |
| 3118 | + | * @typedef {object} waveformBarWheel | |
| 3119 | + | * @property {number} seekSpeed - The mouse wheel seek type, 'seconds' or 'percentage. | |
| 3120 | + | * @property {string} seekType - The mouse wheel seek speed. | |
| 3121 | + | * @public | |
| 3122 | + | */ | |
| 3123 | + | /** @public @type {waveformBarWheel} */ | |
| 3124 | + | this.wheel = { | |
| 3125 | + | seekSpeed: grSet.waveformBarWheelSeekSpeed, | |
| 3126 | + | seekType: grSet.waveformBarWheelSeekType | |
| 3127 | + | }; | |
| 3128 | + | ||
| 3129 | + | // * Initialization | |
| 3130 | + | this.checkConfig(); | |
| 3131 | + | this.defaultSteps(); | |
| 3132 | + | this.setThrottlePaint(); | |
| 3133 | + | if (!IsFolder(this.cacheDir)) { CreateFolder(this.cacheDir); } | |
| 3134 | + | } | |
| 3135 | + | ||
| 3136 | + | // * PUBLIC METHODS - DRAW * // | |
| 3137 | + | // #region PUBLIC METHODS - DRAW | |
| 3138 | + | /** | |
| 3139 | + | * Draws the waveform bar with various designs based on the current settings. | |
| 3140 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 3141 | + | */ | |
| 3142 | + | draw(gr) { | |
| 3143 | + | if (!fb.IsPlaying) { | |
| 3144 | + | gr.FillSolidRect(this.x, grm.ui.seekbarY, this.w, SCALE(grSet.layout !== 'default' ? 10 : 12), grCol.progressBar); | |
| 3145 | + | this.reset(); | |
| 3146 | + | return; | |
| 3147 | + | } | |
| 3148 | + | ||
| 3149 | + | if (this.current.length === 0) { | |
| 3150 | + | this.drawBarInfo(gr); | |
| 3151 | + | return; | |
| 3152 | + | } | |
| 3153 | + | ||
| 3154 | + | if (grSet.waveformBarRefreshRate === 'variable' || grm.debug.showDrawExtendedTiming) { | |
| 3155 | + | grm.ui.seekbarProfiler.Reset(); | |
| 3156 | + | } | |
| 3157 | + | ||
| 3158 | + | // * Set shared properties | |
| 3159 | + | /** @private @type {number} The time constant for the waveform bar calculation. */ | |
| 3160 | + | this.timeConstant = fb.PlaybackLength / this.current.length; | |
| 3161 | + | /** @private @type {number} The current X position based on playback time. */ | |
| 3162 | + | this.currX = this.x + this.w * ((fb.PlaybackTime / fb.PlaybackLength) || 0); | |
| 3163 | + | /** @private @type {number} The width of each bar in the waveform. */ | |
| 3164 | + | this.barW = this.w / this.current.length; | |
| 3165 | + | /** @private @type {boolean} The state whether prepaint mode is active. */ | |
| 3166 | + | this.prepaint = this.preset.paintMode === 'partial' && this.preset.prepaint; | |
| 3167 | + | /** @private @type {boolean} The state whether visualizer mode is active. */ | |
| 3168 | + | this.visualizer = this.analysis.binaryMode === 'visualizer' || this.isFallback || this.fallbackMode.paint; | |
| 3169 | + | ||
| 3170 | + | const minPointDiff = 1; // in px | |
| 3171 | + | const past = [{ x: 0, y: 1 }, { x: 0, y: -1 }]; | |
| 3172 | + | let pastIndex = 0; | |
| 3173 | + | ||
| 3174 | + | gr.SetSmoothingMode(SmoothingMode.AntiAlias); | |
| 3175 | + | ||
| 3176 | + | for (let n = 0; n < this.current.length; n++) { | |
| 3177 | + | const frame = this.current[n]; | |
| 3178 | + | const current = this.timeConstant * n; | |
| 3179 | + | const isPrepaintAllowed = (current - this.time) < this.preset.prepaintFront; | |
| 3180 | + | ||
| 3181 | + | /** @private @type {boolean} The state whether the current frame is in prepaint mode. */ | |
| 3182 | + | this.isPrepaint = current > this.time; | |
| 3183 | + | /** @private @type {number} The scaled size of the current frame. */ | |
| 3184 | + | this.scaledSize = this.h / 2 * frame; | |
| 3185 | + | /** @private @type {number} The x-position of the current frame. */ | |
| 3186 | + | this.frameX = this.x + this.barW * n; | |
| 3187 | + | ||
| 3188 | + | // * Exit loop if prepaint mode conditions are met | |
| 3189 | + | if (this.preset.paintMode === 'partial' && !this.prepaint && this.isPrepaint) break; | |
| 3190 | + | if (this.prepaint && this.isPrepaint && !isPrepaintAllowed) break; | |
| 3191 | + | if (!this.offset[n]) this.offset[n] = 0; | |
| 3192 | + | ||
| 3193 | + | // * Calculate offsets for prepainting and visualizer animation | |
| 3194 | + | /** @private @type {number} The offset value for prepainting and visualizer animation. */ | |
| 3195 | + | this.offset[n] += (this.prepaint && this.isPrepaint && this.preset.animate || this.visualizer ? // Add movement when pre-painting | |
| 3196 | + | this.preset.barDesign === 'dots' ? Math.random() * Math.abs(this.step / this.maxStep) : | |
| 3197 | + | -Math.sign(frame) * Math.random() * this.scaledSize / 10 * this.step / this.maxStep : 0); | |
| 3198 | + | ||
| 3199 | + | /** @private @type {number} The random offset value for the current frame. */ | |
| 3200 | + | this.offsetRandom = this.preset.barDesign === 'dots' ? this.offset[n] : Math.sign(frame) * this.offset[n]; | |
| 3201 | + | ||
| 3202 | + | // * Draw the waveform bar | |
| 3203 | + | if (past.every((p) => | |
| 3204 | + | (p.y !== Math.sign(frame) && this.preset.barDesign !== 'halfbars') || | |
| 3205 | + | (p.y === Math.sign(frame) || this.preset.barDesign === 'halfbars') && (this.frameX - p.x) >= minPointDiff)) { | |
| 3206 | + | this.drawWaveformBar(gr); | |
| 3207 | + | ||
| 3208 | + | past[pastIndex] = { x: this.frameX, y: Math.sign(frame) }; | |
| 3209 | + | pastIndex = (pastIndex + 1) % past.length; | |
| 3210 | + | } | |
| 3211 | + | } | |
| 3212 | + | ||
| 3213 | + | this.drawBarProgressLine(gr); | |
| 3214 | + | this.drawBarAnimation(); | |
| 3215 | + | } | |
| 3216 | + | ||
| 3217 | + | /** | |
| 3218 | + | * Draws the waveform bar based on the preset design. | |
| 3219 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 3220 | + | */ | |
| 3221 | + | drawWaveformBar(gr) { | |
| 3222 | + | const drawBarDesign = { | |
| 3223 | + | waveform: () => this.drawBarDesignWaveform(gr), | |
| 3224 | + | bars: () => this.drawBarDesignBars(gr), | |
| 3225 | + | halfbars: () => this.drawBarDesignHalfbars(gr), | |
| 3226 | + | dots: () => this.drawBarDesignDots(gr) | |
| 3227 | + | }; | |
| 3228 | + | ||
| 3229 | + | drawBarDesign[this.preset.barDesign](); | |
| 3230 | + | } | |
| 3231 | + | ||
| 3232 | + | /** | |
| 3233 | + | * Draws the waveform bar in "waveform" design. | |
| 3234 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 3235 | + | */ | |
| 3236 | + | drawBarDesignWaveform(gr) { | |
| 3237 | + | const yOffset = this.scaledSize > 0 ? Math.max(this.scaledSize + this.offsetRandom, 1) : Math.min(this.scaledSize + this.offsetRandom, -1); | |
| 3238 | + | const zTop = this.visualizer ? Math.abs(yOffset) : yOffset; | |
| 3239 | + | const zBottom = this.visualizer ? -Math.abs(yOffset) : yOffset; | |
| 3240 | + | const { sizeWave } = this.ui; | |
| 3241 | + | const { colorBack, colorFront, colorsDiffer } = this.getColors(); | |
| 3242 | + | ||
| 3243 | + | if (zTop > 0) { | |
| 3244 | + | if (colorsDiffer) { | |
| 3245 | + | gr.FillSolidRect(this.frameX, this.y - zTop, sizeWave, zTop / 2, colorBack); | |
| 3246 | + | gr.FillSolidRect(this.frameX, this.y - zTop / 2, sizeWave, zTop / 2, colorFront); | |
| 3247 | + | } else { | |
| 3248 | + | gr.FillSolidRect(this.frameX, this.y - zTop, sizeWave, zTop, colorBack); | |
| 3249 | + | } | |
| 3250 | + | } | |
| 3251 | + | ||
| 3252 | + | if (zBottom < 0) { | |
| 3253 | + | if (colorsDiffer) { | |
| 3254 | + | gr.FillSolidRect(this.frameX, this.y - zBottom / 2, sizeWave, -zBottom / 2, colorBack); | |
| 3255 | + | gr.FillSolidRect(this.frameX, this.y, sizeWave, -zBottom / 2, colorFront); | |
| 3256 | + | } else { | |
| 3257 | + | gr.FillSolidRect(this.frameX, this.y, sizeWave, -zBottom, colorBack); | |
| 3258 | + | } | |
| 3259 | + | } | |
| 3260 | + | } | |
| 3261 | + | ||
| 3262 | + | /** | |
| 3263 | + | * Draws the waveform bar in "bars" design. | |
| 3264 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 3265 | + | */ | |
| 3266 | + | drawBarDesignBars(gr) { | |
| 3267 | + | const yOffset = this.scaledSize > 0 ? Math.max(this.scaledSize + this.offsetRandom, 1) : Math.min(this.scaledSize + this.offsetRandom, -1); | |
| 3268 | + | const zTop = this.visualizer ? Math.abs(yOffset) : yOffset; | |
| 3269 | + | const zBottom = this.visualizer ? -Math.abs(yOffset) : yOffset; | |
| 3270 | + | const sizeBars = this.barW * this.ui.sizeBars; | |
| 3271 | + | const { colorBack, colorFront, colorsDiffer } = this.getColors(true, true); | |
| 3272 | + | ||
| 3273 | + | if (zTop > 0) { | |
| 3274 | + | if (colorsDiffer) { | |
| 3275 | + | gr.DrawRect(this.frameX, this.y - zTop, sizeBars, zTop / 2, 1, colorBack); | |
| 3276 | + | gr.DrawRect(this.frameX, this.y - zTop / 2, sizeBars, zTop / 2, 1, colorFront); | |
| 3277 | + | } else { | |
| 3278 | + | gr.DrawRect(this.frameX, this.y - zTop, sizeBars, zTop, 1, colorBack); | |
| 3279 | + | } | |
| 3280 | + | } | |
| 3281 | + | ||
| 3282 | + | if (zBottom < 0) { | |
| 3283 | + | if (colorsDiffer) { | |
| 3284 | + | gr.DrawRect(this.frameX, this.y - zBottom / 2, sizeBars, -zBottom / 2, 1, colorBack); | |
| 3285 | + | gr.DrawRect(this.frameX, this.y, sizeBars, -zBottom / 2, 1, colorFront); | |
| 3286 | + | } else { | |
| 3287 | + | gr.DrawRect(this.frameX, this.y, sizeBars, -zBottom, 1, colorBack); | |
| 3288 | + | } | |
| 3289 | + | } | |
| 3290 | + | } | |
| 3291 | + | ||
| 3292 | + | /** | |
| 3293 | + | * Draws the waveform bar in "halfbars" design. | |
| 3294 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 3295 | + | */ | |
| 3296 | + | drawBarDesignHalfbars(gr) { | |
| 3297 | + | const yOffset = this.scaledSize > 0 ? Math.max(this.scaledSize + this.offsetRandom, 1) : Math.min(this.scaledSize + this.offsetRandom, -1); | |
| 3298 | + | const y = this.preset.invertHalfbars ? Math.abs(yOffset) : yOffset; | |
| 3299 | + | const sizeHalf = this.visualizer ? this.barW * this.ui.sizeHalf * (this.visualizer ? 0.2 : 0.5) : this.ui.sizeHalf; | |
| 3300 | + | const { colorBack, colorFront, colorsDiffer } = this.getColors(false, true); | |
| 3301 | + | ||
| 3302 | + | if (y > 0) { | |
| 3303 | + | if (colorsDiffer) { | |
| 3304 | + | gr.FillSolidRect(this.frameX, this.y - 2 * y + this.h * 0.5, sizeHalf, y, colorBack); | |
| 3305 | + | gr.FillSolidRect(this.frameX, this.y - y + this.h * 0.5, sizeHalf, y, colorFront); | |
| 3306 | + | } else { | |
| 3307 | + | gr.FillSolidRect(this.frameX, this.y - 2 * y + this.h * 0.5, sizeHalf, 2 * y, colorBack); | |
| 3308 | + | } | |
| 3309 | + | } | |
| 3310 | + | } | |
| 3311 | + | ||
| 3312 | + | /** | |
| 3313 | + | * Draws the waveform bar in "dots" design. | |
| 3314 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 3315 | + | */ | |
| 3316 | + | drawBarDesignDots(gr) { | |
| 3317 | + | const yOffset = this.scaledSize > 0 ? Math.max(this.scaledSize, 1) : Math.min(this.scaledSize, -1); | |
| 3318 | + | const dotStep = Math.max(this.h / 80, 5) + (this.offsetRandom || 1); | |
| 3319 | + | const dotSize = Math.max(dotStep / 25, 1) * this.ui.sizeDots; | |
| 3320 | + | const { colorBack, colorFront } = this.getColors(); | |
| 3321 | + | ||
| 3322 | + | const drawDots = (direction, startY, yOffset, color1, color2) => { | |
| 3323 | + | const sign = this.visualizer ? direction : Math.sign(yOffset); | |
| 3324 | + | const step = direction * yOffset / 2; | |
| 3325 | + | let currentY = startY; | |
| 3326 | + | ||
| 3327 | + | for (const endY = startY - step; sign * (currentY - endY) > 0; currentY -= sign * dotStep) { | |
| 3328 | + | gr.DrawEllipse(this.frameX, currentY, dotSize, dotSize, 1, color1); | |
| 3329 | + | } | |
| 3330 | + | ||
| 3331 | + | for (const endY = startY - 2 * step; sign * (currentY - endY) > 0; currentY -= sign * dotStep) { | |
| 3332 | + | gr.DrawEllipse(this.frameX, currentY, dotSize, dotSize, 1, color2); | |
| 3333 | + | } | |
| 3334 | + | }; | |
| 3335 | + | ||
| 3336 | + | drawDots(1, this.y, yOffset, colorFront, colorBack); | |
| 3337 | + | if (!this.visualizer) return; | |
| 3338 | + | drawDots(-1, this.y, yOffset, colorFront, colorBack); | |
| 3339 | + | } | |
| 3340 | + | ||
| 3341 | + | /** | |
| 3342 | + | * Draws the progress line on the waveform bar. | |
| 3343 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 3344 | + | */ | |
| 3345 | + | drawBarProgressLine(gr) { | |
| 3346 | + | if (!this.preset.indicator && !this.mouseDown) return; | |
| 3347 | + | ||
| 3348 | + | gr.SetSmoothingMode(0); | |
| 3349 | + | ||
| 3350 | + | if (this.analysis.binaryMode === 'audioWizard' || ['waveform', 'dots'].includes(this.preset.barDesign)) { | |
| 3351 | + | const minBarW = Math.round(Math.max(this.barW, SCALE(1))); | |
| 3352 | + | gr.DrawLine(this.currX, this.y - this.h * 0.5, this.currX, this.y + this.h * 0.5, minBarW, grCol.waveformBarIndicator); | |
| 3353 | + | } | |
| 3354 | + | } | |
| 3355 | + | ||
| 3356 | + | /** | |
| 3357 | + | * Draws information text when waveform data is loading or when it is not available. | |
| 3358 | + | * @param {GdiGraphics} gr - The GDI graphics object. | |
| 3359 | + | */ | |
| 3360 | + | drawBarInfo(gr) { | |
| 3361 | + | if (pl.col.row_nowplaying_bg === null) return; // * Wait until nowplaying bg has a new color to prevent flashing | |
| 3362 | + | ||
| 3363 | + | const DT_CENTER = DrawText.VCenter | DrawText.Center | DrawText.EndEllipsis | DrawText.CalcRect | DrawText.NoPrefix; | |
| 3364 | + | const bgColor = grSet.theme === 'reborn' ? pl.col.row_nowplaying_bg : grCol.transportEllipseBg; | |
| 3365 | + | const message = | |
| 3366 | + | !this.isAllowedFile && !this.isFallback && this.analysis.binaryMode !== 'visualizer' ? 'Incompatible file format' : | |
| 3367 | + | !this.analysis.autoAnalysis ? 'Waveform bar file not found' : | |
| 3368 | + | this.isError ? 'Waveform bar file can not be analyzed' : | |
| 3369 | + | this.active ? 'Loading' : ''; | |
| 3370 | + | ||
| 3371 | + | gr.FillSolidRect(this.x, this.y - this.h * 0.5, this.w, this.h, bgColor); | |
| 3372 | + | gr.GdiDrawText(message, grFont.lowerBarWave, pl.col.header_artist_normal, this.x, this.y - this.h * 0.5, this.w, this.h, DT_CENTER); | |
| 3373 | + | } | |
| 3374 | + | ||
| 3375 | + | /** | |
| 3376 | + | * Draw the waveform bar animation. | |
| 3377 | + | */ | |
| 3378 | + | drawBarAnimation() { | |
| 3379 | + | if (this.prepaint && this.preset.animate || this.visualizer) { | |
| 3380 | + | if (this.step >= this.maxStep) { | |
| 3381 | + | this.step = -this.step; | |
| 3382 | + | } else { | |
| 3383 | + | if (this.step === 0) { this.offset = []; } | |
| 3384 | + | this.step++; | |
| 3385 | + | } | |
| 3386 | + | } | |
| 3387 | + | ||
| 3388 | + | if (fb.IsPlaying && !fb.IsPaused) { | |
| 3389 | + | this.setRefreshRate(); | |
| 3390 | + | ||
| 3391 | + | if (this.visualizer) { | |
| 3392 | + | this.throttlePaint(); | |
| 3393 | + | } | |
| 3394 | + | else if (this.current.length && (this.prepaint || this.preset.paintMode === 'partial' || this.preset.indicator)) { | |
| 3395 | + | const paintRect = this.setPaintRect(this.time); | |
| 3396 | + | this.throttlePaintRect(paintRect.x, paintRect.y, paintRect.width, paintRect.height); | |
| 3397 | + | } | |
| 3398 | + | } | |
| 3399 | + | } | |
| 3400 | + | // #endregion | |
| 3401 | + | ||
| 3402 | + | // * PUBLIC METHODS - INITALIZATION * // | |
| 3403 | + | // #region PUBLIC METHODS - INITALIZATION | |
| 3404 | + | /** | |
| 3405 | + | * Checks if the current file is allowed to be played, i.e not corrupted. | |
| 3406 | + | * @param {object} handle - The current file handle. | |
| 3407 | + | */ | |
| 3408 | + | checkAllowedFile(handle = fb.GetNowPlaying()) { | |
| 3409 | + | if (!handle) return; | |
| 3410 | + | ||
| 3411 | + | const noVisual = this.analysis.binaryMode !== 'visualizer'; | |
| 3412 | + | const validExt = this.checkCompatibleFileExtension(handle); | |
| 3413 | + | ||
| 3414 | + | this.isZippedFile = handle.RawPath.includes('unpack://'); | |
| 3415 | + | this.isAllowedFile = noVisual && validExt && !this.isZippedFile; | |
| 3416 | + | this.isFallback = !this.isAllowedFile && this.analysis.visualizerFallback; | |
| 3417 | + | } | |
| 3418 | + | ||
| 3419 | + | /** | |
| 3420 | + | * Checks if the file extension of the current file handle is compatible. | |
| 3421 | + | * @param {object} handle - The current file handle. | |
| 3422 | + | * @param {string} mode - The analysis binary mode. | |
| 3423 | + | * @returns {boolean} True if the file extension is compatible, otherwise false. | |
| 3424 | + | */ | |
| 3425 | + | checkCompatibleFileExtension(handle = fb.GetNowPlaying(), mode = this.analysis.binaryMode) { | |
| 3426 | + | return (mode === 'visualizer') || (handle && this.compatibleFiles[mode].test(handle.Path)); | |
| 3427 | + | } | |
| 3428 | + | ||
| 3429 | + | /** | |
| 3430 | + | * Checks the report list of compatible file extensions for the given mode. | |
| 3431 | + | * @param {string} mode - The analysis binary mode. | |
| 3432 | + | * @returns {Array<string>} An array of compatible file extensions. | |
| 3433 | + | */ | |
| 3434 | + | checkCompatibleFileExtensionReport(mode = this.analysis.binaryMode) { | |
| 3435 | + | return [...this.compatibleFiles[`${mode}List`]]; | |
| 3436 | + | } | |
| 3437 | + | ||
| 3438 | + | /** | |
| 3439 | + | * Checks the configuration for validity, called from the constructor. | |
| 3440 | + | */ | |
| 3441 | + | checkConfig() { | |
| 3442 | + | if (!Object.prototype.hasOwnProperty.call(this.binaries, this.analysis.binaryMode)) { | |
| 3443 | + | this.analysis.binaryMode = 'visualizer'; | |
| 3444 | + | } | |
| 3445 | + | if (!this.binaries[this.analysis.binaryMode]) { | |
| 3446 | + | fb.ShowPopupMessage(`Waveform bar => required dependency not found: ${this.analysis.binaryMode}\n\n${JSON.stringify(this.binaries[this.analysis.binaryMode])}`, window.Name); | |
| 3447 | + | } | |
| 3448 | + | ||
| 3449 | + | if (this.preset.prepaintFront <= 0 || this.preset.prepaintFront === null) { | |
| 3450 | + | this.preset.prepaintFront = Infinity; | |
| 3451 | + | } | |
| 3452 | + | ||
| 3453 | + | if (this.wheel.seekSpeed < 0) { | |
| 3454 | + | this.wheel.seekSpeed = 1; | |
| 3455 | + | } else if (this.wheel.seekSpeed > 100 && this.wheel.seekType === 'percentage') { | |
| 3456 | + | this.wheel.seekSpeed = 100; | |
| 3457 | + | } | |
| 3458 | + | } | |
| 3459 | + | ||
| 3460 | + | /** | |
| 3461 | + | * Updates the config and ensures the UI is being updated properly after changing settings. | |
| 3462 | + | * @param {object} newConfig - The new configuration object with settings to be applied. | |
| 3463 | + | */ | |
| 3464 | + | updateConfig(newConfig) { | |
| 3465 | + | if (newConfig) { | |
| 3466 | + | DeepAssign()(this, newConfig); | |
| 3467 | + | } | |
| 3468 | + | ||
| 3469 | + | this.checkConfig(); | |
| 3470 | + | let recalculate = false; | |
| 3471 | + | ||
| 3472 | + | if (newConfig.preset) { | |
| 3473 | + | if (this.preset.paintMode === 'partial' && this.preset.prepaint || this.analysis.binaryMode === 'visualizer') { | |
| 3474 | + | this.offset = []; | |
| 3475 | + | this.step = 0; | |
| 3476 | + | } | |
| 3477 | + | if (Object.prototype.hasOwnProperty.call(newConfig.preset, 'animate') || | |
| 3478 | + | Object.prototype.hasOwnProperty.call(newConfig.preset, 'useBPM')) { | |
| 3479 | + | if (this.preset.animate && this.preset.useBPM) { | |
| 3480 | + | this.bpmSteps(); | |
| 3481 | + | } else { | |
| 3482 | + | this.defaultSteps(); | |
| 3483 | + | } | |
| 3484 | + | } | |
| 3485 | + | } | |
| 3486 | + | ||
| 3487 | + | if (newConfig.ui) { | |
| 3488 | + | if (Object.prototype.hasOwnProperty.call(newConfig.ui, 'refreshRate')) { | |
| 3489 | + | this.setThrottlePaint(); | |
| 3490 | + | } | |
| 3491 | + | if (Object.prototype.hasOwnProperty.call(newConfig.ui, 'sizeNormalizeWidth') || | |
| 3492 | + | Object.prototype.hasOwnProperty.call(newConfig.ui, 'normalizeWidth')) { | |
| 3493 | + | recalculate = true; | |
| 3494 | + | } | |
| 3495 | + | } | |
| 3496 | + | ||
| 3497 | + | if (newConfig.analysis) { | |
| 3498 | + | recalculate = true; | |
| 3499 | + | } | |
| 3500 | + | ||
| 3501 | + | if (recalculate) { | |
| 3502 | + | this.on_playback_new_track(); | |
| 3503 | + | } else { | |
| 3504 | + | this.throttlePaint(); | |
| 3505 | + | } | |
| 3506 | + | } | |
| 3507 | + | // #endregion | |
| 3508 | + | ||
| 3509 | + | // * PUBLIC METHODS - DATA * // | |
| 3510 | + | // #region PUBLIC METHODS - DATA | |
| 3511 | + | /** | |
| 3512 | + | * Starts the analysis process of the waveform data and updates the current state. | |
| 3513 | + | * @param {FbMetadbHandle} handle - The handle of the current track. | |
| 3514 | + | * @param {boolean} isRetry - The flag indicating whether the method call is a retry attempt. | |
| 3515 | + | * @returns {Promise<boolean>} The promise that resolves to `true` if analysis is successful, `false` otherwise. | |
| 3516 | + | */ | |
| 3517 | + | async analyzeDataStart(handle, isRetry) { | |
| 3518 | + | if (this.analysis.binaryMode === 'visualizer' || this.analysis.visualizerFallbackAnalysis) { | |
| 3519 | + | this.current = this.visualizerData(handle); | |
| 3520 | + | ||
| 3521 | + | if (this.analysis.binaryMode === 'visualizer') { | |
| 3522 | + | this.normalizePoints(); | |
| 3523 | + | return; | |
| 3524 | + | } | |
| 3525 | + | } | |
| 3526 | + | ||
| 3527 | + | const { waveformBarFolder, waveformBarFile, sourceFile } = this.getPaths(handle); | |
| 3528 | + | const files = this.getFileConfigs(); | |
| 3529 | + | let analysisComplete = false; | |
| 3530 | + | ||
| 3531 | + | for (const file of files) { | |
| 3532 | + | const fileWithExt = `${waveformBarFile}${file.ext}`; | |
| 3533 | + | if (IsFile(fileWithExt)) { | |
| 3534 | + | const str = Open(fileWithExt, file.codePage) || ''; | |
| 3535 | + | const parsed = file.decompress(str) || {}; | |
| 3536 | + | ||
| 3537 | + | if (parsed.data && Array.isArray(parsed.data)) { | |
| 3538 | + | this.current = parsed.data; | |
| 3539 | + | this.currentChannels = parsed.channels || 1; | |
| 3540 | + | } else { | |
| 3541 | + | this.current = Array.isArray(parsed) ? parsed : []; | |
| 3542 | + | this.currentChannels = 1; | |
| 3543 | + | } | |
| 3544 | + | ||
| 3545 | + | if (this.verifyData(handle, fileWithExt, isRetry)) { | |
| 3546 | + | analysisComplete = true; | |
| 3547 | + | break; | |
| 3548 | + | } | |
| 3549 | + | } | |
| 3550 | + | } | |
| 3551 | + | ||
| 3552 | + | if (!analysisComplete && this.analysis.autoAnalysis && IsFile(sourceFile)) { | |
| 3553 | + | if (this.analysis.visualizerFallbackAnalysis && this.isAllowedFile) { | |
| 3554 | + | this.fallbackMode.analysis = this.fallbackMode.paint = true; | |
| 3555 | + | this.normalizePoints(); | |
| 3556 | + | if (this.preset.animate && this.preset.useBPM) this.bpmSteps(handle); | |
| 3557 | + | if (fb.IsPlaying) this.time = fb.PlaybackTime; | |
| 3558 | + | } | |
| 3559 | + | ||
| 3560 | + | this.throttlePaint(true); | |
| 3561 | + | if (this.analysis.visualizerFallbackAnalysis) { | |
| 3562 | + | this.fallbackMode.analysis = false; | |
| 3563 | + | } | |
| 3564 | + | ||
| 3565 | + | await this.analyzeData(handle, waveformBarFolder, waveformBarFile, sourceFile); | |
| 3566 | + | this.fallbackMode.analysis = this.fallbackMode.paint = false; | |
| 3567 | + | analysisComplete = this.verifyData(handle, undefined, isRetry); | |
| 3568 | + | } | |
| 3569 | + | ||
| 3570 | + | this.isFallback = !analysisComplete; | |
| 3571 | + | this.normalizePoints(this.analysis.binaryMode !== 'visualizer' && this.ui.sizeNormalizeWidth); | |
| 3572 | + | } | |
| 3573 | + | ||
| 3574 | + | /** | |
| 3575 | + | * Analyzes data of the given handle(s) and saves the results in the waveform bar cache directory. | |
| 3576 | + | * @param {FbMetadbHandle|FbMetadbHandleList} handle - The handle(s) to analyze. | |
| 3577 | + | * @param {string} waveformBarFolder - The folder where the waveform bar data should be saved. | |
| 3578 | + | * @param {string} waveformBarFile - The name of the waveform bar file. | |
| 3579 | + | * @param {string} [sourceFile] - The path of the source file. | |
| 3580 | + | * @returns {Promise<void>} The promise that resolves when the analysis has finished. | |
| 3581 | + | */ | |
| 3582 | + | async analyzeData(handle, waveformBarFolder, waveformBarFile, sourceFile = handle.Path || handle[0].Path) { | |
| 3583 | + | if (!this.isAllowedFile || !AudioWizard || AudioWizard.FullTrackProcessing) return; | |
| 3584 | + | ||
| 3585 | + | if (!IsFolder(waveformBarFolder)) { | |
| 3586 | + | CreateFolder(waveformBarFolder); | |
| 3587 | + | } | |
| 3588 | + | ||
| 3589 | + | try { | |
| 3590 | + | const handleList = (handle instanceof FbMetadbHandleList) ? handle : | |
| 3591 | + | new FbMetadbHandleList(Array.isArray(handle) ? handle : [handle] | |
| 3592 | + | ); | |
| 3593 | + | const startTime = Date.now(); | |
| 3594 | + | ||
| 3595 | + | grm.debug.debugLog(`Audio Wizard => Starting waveform analysis: mode=${this.preset.analysisMode}, resolution=${this.analysis.resolution}`); | |
| 3596 | + | ||
| 3597 | + | const success = await new Promise((resolve) => { | |
| 3598 | + | const { metadata } = GetMetadata(handleList); | |
| 3599 | + | AudioWizard.SetFullTrackWaveformCallback((res) => resolve(res)); | |
| 3600 | + | AudioWizard.StartWaveformAnalysis(metadata, this.analysis.resolution); | |
| 3601 | + | }); | |
| 3602 | + | ||
| 3603 | + | if (!success) { | |
| 3604 | + | console.log('Audio Wizard => Waveform analysis failed - API returned error'); | |
| 3605 | + | return; | |
| 3606 | + | } | |
| 3607 | + | ||
| 3608 | + | const metricsPerChannel = 5; | |
| 3609 | + | const trackCount = AudioWizard.GetWaveformTrackCount(); | |
| 3610 | + | ||
| 3611 | + | for (let i = 0; i < trackCount; i++) { | |
| 3612 | + | const trackHandle = handleList[i]; | |
| 3613 | + | const data = []; | |
| 3614 | + | const rawData = AudioWizard.GetWaveformData(i); | |
| 3615 | + | const channels = AudioWizard.GetWaveformTrackChannels(i); | |
| 3616 | + | const stepSize = metricsPerChannel * channels; | |
| 3617 | + | ||
| 3618 | + | // Restructure flat array into array of arrays (one array per time step) | |
| 3619 | + | for (let j = 0; j < rawData.length; j += stepSize) { | |
| 3620 | + | const pointSlice = rawData.slice(j, j + stepSize); | |
| 3621 | + | const roundedPoint = pointSlice.map(v => Math.round(v * 1000) / 1000); | |
| 3622 | + | data.push(roundedPoint); | |
| 3623 | + | } | |
| 3624 | + | ||
| 3625 | + | if (this.saveDataAllowed(trackHandle)) { | |
| 3626 | + | this.analyzeDataSave(waveformBarFile, JSON.stringify({ channels, data })); | |
| 3627 | + | } | |
| 3628 | + | ||
| 3629 | + | if (handleList.Count === 1) { | |
| 3630 | + | this.current = data; | |
| 3631 | + | this.currentChannels = channels; // Store for normalizePoints | |
| 3632 | + | } | |
| 3633 | + | } | |
| 3634 | + | ||
| 3635 | + | grm.debug.debugLog(`Audio Wizard => Analysis completed in ${(Date.now() - startTime) / 1000} seconds`); | |
| 3636 | + | this.throttlePaint(); | |
| 3637 | + | } | |
| 3638 | + | catch (e) { | |
| 3639 | + | console.log(`Audio Wizard => Analysis error: ${e.message}`); | |
| 3640 | + | AudioWizard.StopWaveformAnalysis(); | |
| 3641 | + | } | |
| 3642 | + | } | |
| 3643 | + | ||
| 3644 | + | /** | |
| 3645 | + | * Saves the compressed data to a file. | |
| 3646 | + | * @param {string} waveformBarFile - The name of the waveform bar file. | |
| 3647 | + | * @param {string} dataStr - The data to be saved. | |
| 3648 | + | */ | |
| 3649 | + | analyzeDataSave(waveformBarFile, dataStr) { | |
| 3650 | + | if (this.analysis.binaryMode === 'visualizer') return; | |
| 3651 | + | ||
| 3652 | + | const compression = { | |
| 3653 | + | 'utf-16': () => SaveFSO(`${waveformBarFile}.awz.lz16`, LZString.compressToUTF16(dataStr), true), | |
| 3654 | + | 'utf-8': () => Save(`${waveformBarFile}.awz.lz`, LZUTF8.compress(dataStr, { outputEncoding: 'Base64' })), | |
| 3655 | + | 'none': () => Save(`${waveformBarFile}.awz.json`, dataStr) | |
| 3656 | + | }; | |
| 3657 | + | ||
| 3658 | + | (compression[this.analysis.compressionMode] || compression.none)(); | |
| 3659 | + | } | |
| 3660 | + | ||
| 3661 | + | /** | |
| 3662 | + | * Generates data for the visualizer. | |
| 3663 | + | * @param {FbMetadbHandle} handle - The handle to analyze. | |
| 3664 | + | * @param {string} preset - The preset to use for the visualizer. | |
| 3665 | + | * @param {boolean} variableLen - The flag whether the length of the data should be variable. | |
| 3666 | + | * @returns {Array} The data for the visualizer bar. | |
| 3667 | + | */ | |
| 3668 | + | visualizerData(handle, preset = 'classic spectrum analyzer', variableLen = false) { | |
| 3669 | + | const barW = this.getBarWidth(); | |
| 3670 | + | const samplesMax = Math.floor(this.w / barW); | |
| 3671 | + | const samplesTotal = Math.floor(handle.Length * this.analysis.resolution); | |
| 3672 | + | const samples = variableLen ? samplesTotal : Math.min(samplesMax, samplesTotal); | |
| 3673 | + | const data = new Array(samples); | |
| 3674 | + | ||
| 3675 | + | if (preset === 'classic spectrum analyzer') { | |
| 3676 | + | const third = Math.round(samples / 3); | |
| 3677 | + | const half = Math.round(samples / 2); | |
| 3678 | + | ||
| 3679 | + | // * Filling first half | |
| 3680 | + | for (let i = 0; i < third; i++) { | |
| 3681 | + | const val = (Math.random() * i) / third; | |
| 3682 | + | data[i] = val; | |
| 3683 | + | } | |
| 3684 | + | for (let i = third; i < half; i++) { | |
| 3685 | + | const val = (Math.random() * i) / third; | |
| 3686 | + | data[i] = val; | |
| 3687 | + | } | |
| 3688 | + | // * Filling second half with reversed first half | |
| 3689 | + | for (let i = half, j = 0; i < samples; i++, j++) { | |
| 3690 | + | data[i] = data[half - 1 - j]; | |
| 3691 | + | } | |
| 3692 | + | } | |
| 3693 | + | ||
| 3694 | + | return data; | |
| 3695 | + | } | |
| 3696 | + | ||
| 3697 | + | /** | |
| 3698 | + | * Checks if the processed waveform data is valid for audioWizard mode. | |
| 3699 | + | * @returns {boolean} True if the data is valid. | |
| 3700 | + | */ | |
| 3701 | + | validData() { | |
| 3702 | + | if (!Array.isArray(this.current) || !this.current.length) { | |
| 3703 | + | return false; | |
| 3704 | + | } | |
| 3705 | + | ||
| 3706 | + | const channels = this.currentChannels || 1; | |
| 3707 | + | const expectedLength = this.metrics.count * channels; | |
| 3708 | + | ||
| 3709 | + | return this.current.every(frame => { | |
| 3710 | + | if (!Array.isArray(frame) || frame.length < expectedLength) { | |
| 3711 | + | return false; | |
| 3712 | + | } | |
| 3713 | + | ||
| 3714 | + | // Validate metrics for each channel | |
| 3715 | + | for (let ch = 0; ch < channels; ch++) { | |
| 3716 | + | const offset = ch * this.metrics.count; | |
| 3717 | + | ||
| 3718 | + | for (const [metric, index] of Object.entries(this.metrics.index)) { | |
| 3719 | + | const value = frame[offset + index]; | |
| 3720 | + | if (typeof value !== 'number' || !isFinite(value)) { | |
| 3721 | + | return false; | |
| 3722 | + | } | |
| 3723 | + | const [min, max] = this.metrics.range[metric]; | |
| 3724 | + | if (value < min || value > max) { | |
| 3725 | + | return false; | |
| 3726 | + | } | |
| 3727 | + | } | |
| 3728 | + | } | |
| 3729 | + | ||
| 3730 | + | return true; | |
| 3731 | + | }); | |
| 3732 | + | } | |
| 3733 | + | ||
| 3734 | + | /** | |
| 3735 | + | * Verifies if the processed data is valid. | |
| 3736 | + | * @param {FbMetadbHandle} handle - The handle to analyze. | |
| 3737 | + | * @param {string} file - The file to analyze. | |
| 3738 | + | * @param {boolean} isRetry - The flag whether the data should be retried. | |
| 3739 | + | * @returns {boolean} True if the data is valid. | |
| 3740 | + | */ | |
| 3741 | + | verifyData(handle, file, isRetry = false) { | |
| 3742 | + | if (this.validData()) return true; | |
| 3743 | + | ||
| 3744 | + | if (file) DeleteFile(file); | |
| 3745 | + | ||
| 3746 | + | if (isRetry) { | |
| 3747 | + | console.log('File was not successfully analyzed after retrying.'); | |
| 3748 | + | this.isAllowedFile = false; | |
| 3749 | + | this.isFallback = this.analysis.visualizerFallback; | |
| 3750 | + | this.isError = true; | |
| 3751 | + | this.current = []; | |
| 3752 | + | } else { | |
| 3753 | + | console.log(`Waveform bar file not valid. Creating new one${file ? `: ${file}` : '.'}`); | |
| 3754 | + | this.on_playback_new_track(handle, true); | |
| 3755 | + | } | |
| 3756 | + | ||
| 3757 | + | return false; | |
| 3758 | + | } | |
| 3759 | + | ||
| 3760 | + | /** | |
| 3761 | + | * Deletes the waveform file(s) associated with the given track handle. | |
| 3762 | + | * @param {FbMetadbHandle} handle - The handle of the track. | |
| 3763 | + | */ | |
| 3764 | + | deleteWaveformFile(handle) { | |
| 3765 | + | if (!handle) return; | |
| 3766 | + | ||
| 3767 | + | const { waveformBarFile } = this.getPaths(handle); | |
| 3768 | + | const fileConfigs = this.getFileConfigs(); | |
| 3769 | + | ||
| 3770 | + | for (const config of fileConfigs) { | |
| 3771 | + | const filePath = `${waveformBarFile}${config.ext}`; | |
| 3772 | + | if (IsFile(filePath)) { | |
| 3773 | + | try { | |
| 3774 | + | DeleteFile(filePath); | |
| 3775 | + | } catch (e) { | |
| 3776 | + | console.log(`Error deleting waveform file: ${filePath}`, e); | |
| 3777 | + | } | |
| 3778 | + | } | |
| 3779 | + | } | |
| 3780 | + | } | |
| 3781 | + | ||
| 3782 | + | /** | |
| 3783 | + | * Deletes the waveform bar cache directory with its processed data. | |
| 3784 | + | */ | |
| 3785 | + | removeData() { | |
| 3786 | + | DeleteFolder(this.cacheDir); | |
| 3787 | + | } | |
| 3788 | + | ||
| 3789 | + | /** | |
| 3790 | + | * Determines whether data should be saved based on the current analysis save mode and the handle. | |
| 3791 | + | * @param {FbMetadbHandle} handle - The handle to check against the save mode and media library. | |
| 3792 | + | * @returns {boolean} - Returns `true` if the data should be saved, `false` otherwise. | |
| 3793 | + | */ | |
| 3794 | + | saveDataAllowed(handle) { | |
| 3795 | + | return this.analysis.saveMode === 'always' || (this.analysis.saveMode === 'library' && handle && fb.IsMetadbInMediaLibrary(handle)); | |
| 3796 | + | } | |
| 3797 | + | // #endregion | |
| 3798 | + | ||
| 3799 | + | // * PUBLIC METHODS - COMMON * // | |
| 3800 | + | // #region PUBLIC METHODS - COMMON | |
| 3801 | + | /** | |
| 3802 | + | * Sets the max step based on the BPM of the track. | |
| 3803 | + | * @param {object} handle - The handle of the track. | |
| 3804 | + | * @returns {number} The max steps. | |
| 3805 | + | */ | |
| 3806 | + | bpmSteps(handle = fb.GetNowPlaying()) { | |
| 3807 | + | if (!handle) return this.defaultSteps(); | |
| 3808 | + | ||
| 3809 | + | // Don't allow anything faster than 2 steps or slower than 10 (scaled to 200 ms refresh rate) and consider setting tracks having 100 BPM as default. | |
| 3810 | + | const BPM = Number(this.TfMaxStep.EvalWithMetadb(handle)); | |
| 3811 | + | this.maxStep = Math.round(Math.min(Math.max(200 / (BPM || 100) * 2, 2), 10) * (200 / this.ui.refreshRate) ** (1 / 2)); | |
| 3812 | + | ||
| 3813 | + | return this.maxStep; | |
| 3814 | + | } | |
| 3815 | + | ||
| 3816 | + | /** | |
| 3817 | + | * Sets the max step to a default value. | |
| 3818 | + | * @returns {number} The max steps. | |
| 3819 | + | */ | |
| 3820 | + | defaultSteps() { | |
| 3821 | + | this.maxStep = Math.round(4 * (200 / this.ui.refreshRate) ** (1 / 2)); | |
| 3822 | + | return this.maxStep; | |
| 3823 | + | } | |
| 3824 | + | ||
| 3825 | + | /** | |
| 3826 | + | * Gets the bar width based on the bar design preset. | |
| 3827 | + | * @returns {number} The width of the bar corresponding to the design preset. | |
| 3828 | + | */ | |
| 3829 | + | getBarWidth() { | |
| 3830 | + | const barWidth = { | |
| 3831 | + | waveform: this.ui.sizeWave, | |
| 3832 | + | bars: this.ui.sizeBars, | |
| 3833 | + | dots: this.ui.sizeDots, | |
| 3834 | + | halfbars: this.ui.sizeHalf | |
| 3835 | + | }; | |
| 3836 | + | ||
| 3837 | + | return barWidth[this.preset.barDesign] || 1; | |
| 3838 | + | } | |
| 3839 | + | ||
| 3840 | + | /** | |
| 3841 | + | * Gets the colors for the waveform bars. | |
| 3842 | + | * @param {boolean} useShadeColor - The flag indicating whether to use the ShadeColor for adjustments. | |
| 3843 | + | * @param {boolean} highlightCurrentPosition - The flag indicating whether to highlight the current position indicator. | |
| 3844 | + | * @returns {object} The object containing colorBack, colorFront and colorsDiffer. | |
| 3845 | + | */ | |
| 3846 | + | getColors(useShadeColor = true, highlightCurrentPosition = false) { | |
| 3847 | + | if (highlightCurrentPosition && (this.preset.indicator || this.mouseDown) && this.analysis.binaryMode === 'audioWizard' && | |
| 3848 | + | (this.frameX <= this.currX && this.frameX >= this.currX - 2 * this.barW)) { | |
| 3849 | + | return { colorBack: grCol.waveformBarIndicator, colorFront: grCol.waveformBarIndicator, colorsDiffer: false }; | |
| 3850 | + | } | |
| 3851 | + | ||
| 3852 | + | const colorBack = this.prepaint && this.isPrepaint ? | |
| 3853 | + | useShadeColor ? ShadeColor(grCol.waveformBarFillBack, 40) : grCol.waveformBarFillPreBack : | |
| 3854 | + | grCol.waveformBarFillBack; | |
| 3855 | + | ||
| 3856 | + | const colorFront = this.prepaint && this.isPrepaint ? | |
| 3857 | + | useShadeColor ? ShadeColor(grCol.waveformBarFillFront, 20) : grCol.waveformBarFillPreFront : | |
| 3858 | + | grCol.waveformBarFillFront; | |
| 3859 | + | ||
| 3860 | + | return { colorBack, colorFront, colorsDiffer: colorFront !== colorBack }; | |
| 3861 | + | } | |
| 3862 | + | ||
| 3863 | + | /** | |
| 3864 | + | * Gets the configuration for the different file types to be analyzed. | |
| 3865 | + | * @returns {Array<object>} An array of file configuration objects. Each object contains: | |
| 3866 | + | * - {string} ext - The file extension. | |
| 3867 | + | * - {Function} decompress - The function to decompress and parse the file content. | |
| 3868 | + | * - {string} codePage - The code page to be used when reading the file. | |
| 3869 | + | */ | |
| 3870 | + | getFileConfigs() { | |
| 3871 | + | return [ | |
| 3872 | + | { ext: '.awz.json', decompress: JSON.parse, codePage: this.codePage }, | |
| 3873 | + | { ext: '.awz.lz', decompress: str => JSON.parse(LZUTF8.decompress(str, { inputEncoding: 'Base64' })), codePage: this.codePage }, | |
| 3874 | + | { ext: '.awz.lz16', decompress: str => JSON.parse(LZString.decompressFromUTF16(str)), codePage: this.codePageV2 } | |
| 3875 | + | ]; | |
| 3876 | + | } | |
| 3877 | + | ||
| 3878 | + | /** | |
| 3879 | + | * Gets the paths to the waveform bar cache folder and file. | |
| 3880 | + | * @param {object} handle - The handle of the track. | |
| 3881 | + | * @returns {object} The paths to the waveform bar cache folder and file. | |
| 3882 | + | */ | |
| 3883 | + | getPaths(handle) { | |
| 3884 | + | const id = CleanFilePath(this.Tf.EvalWithMetadb(handle)); // Ensures paths are valid! | |
| 3885 | + | const fileName = id.split('\\').pop(); | |
| 3886 | + | const waveformBarFolder = this.cacheDir + (this.saveDataAllowed(handle) ? id.replace(fileName, '') : ''); | |
| 3887 | + | const waveformBarFile = this.cacheDir + id; | |
| 3888 | + | const sourceFile = this.isZippedFile ? handle.Path.split('|')[0] : handle.Path; | |
| 3889 | + | ||
| 3890 | + | return { waveformBarFolder, waveformBarFile, sourceFile }; | |
| 3891 | + | } | |
| 3892 | + | ||
| 3893 | + | /** | |
| 3894 | + | * Gets the maximum and minimum values from the frames. | |
| 3895 | + | * @param {number[]} frames - The array of frame values. | |
| 3896 | + | * @returns {object} The object containing the `upper` and `lower` values. | |
| 3897 | + | */ | |
| 3898 | + | getMaxValue(frames) { | |
| 3899 | + | let upper = 0; | |
| 3900 | + | let lower = 0; | |
| 3901 | + | ||
| 3902 | + | for (let i = 0; i < frames.length; i++) { | |
| 3903 | + | const frame = frames[i]; | |
| 3904 | + | upper = Math.max(upper, frame); | |
| 3905 | + | lower = Math.min(lower, frame); | |
| 3906 | + | } | |
| 3907 | + | ||
| 3908 | + | return { upper, lower }; | |
| 3909 | + | } | |
| 3910 | + | ||
| 3911 | + | /** | |
| 3912 | + | * Gets the minimum value at a specific position in the frames. | |
| 3913 | + | * @param {Array} frames - The array of frame data. | |
| 3914 | + | * @param {number} pos - The position index in the frame data. | |
| 3915 | + | * @returns {number} The minimum value at the specified position. | |
| 3916 | + | */ | |
| 3917 | + | getMinValuePos(frames, pos) { | |
| 3918 | + | let minVal = Infinity; | |
| 3919 | + | ||
| 3920 | + | for (let i = 0; i < frames.length; i++) { | |
| 3921 | + | const frame = frames[i]; | |
| 3922 | + | if (frame[pos] === null) frame[pos] = -Infinity; | |
| 3923 | + | const val = frame[pos]; | |
| 3924 | + | if (isFinite(val)) { | |
| 3925 | + | minVal = Math.min(minVal, val); | |
| 3926 | + | } | |
| 3927 | + | } | |
| 3928 | + | ||
| 3929 | + | return minVal === Infinity ? 0 : minVal; | |
| 3930 | + | } | |
| 3931 | + | ||
| 3932 | + | /** | |
| 3933 | + | * Gets the Normalized frame values by subtracting the maximum value from each frame. | |
| 3934 | + | * @param {Array} frames - The array of frame data. | |
| 3935 | + | * @param {number} maxVal - The maximum value to be subtracted from each frame. | |
| 3936 | + | * @returns {Array} The normalized frame data. | |
| 3937 | + | */ | |
| 3938 | + | getNormalizedFrameValues(frames, maxVal) { | |
| 3939 | + | const normalizedFrames = new Array(frames.length); | |
| 3940 | + | const scaledIndex = this.metrics.count; // Scaled value stored at metric.count | |
| 3941 | + | ||
| 3942 | + | for (let i = 0; i < frames.length; i++) { | |
| 3943 | + | const frame = frames[i]; | |
| 3944 | + | const newFrame = frame.slice(); | |
| 3945 | + | ||
| 3946 | + | if (newFrame[scaledIndex] !== 1) newFrame[scaledIndex] -= maxVal; | |
| 3947 | + | if (!isFinite(newFrame[scaledIndex])) newFrame[scaledIndex] = 0; | |
| 3948 | + | ||
| 3949 | + | normalizedFrames[i] = newFrame; | |
| 3950 | + | } | |
| 3951 | + | ||
| 3952 | + | return normalizedFrames; | |
| 3953 | + | } | |
| 3954 | + | ||
| 3955 | + | /** | |
| 3956 | + | * Gets the scaled frames based on the given position, maximum value, and level type. | |
| 3957 | + | * @param {Array} frames - The array of frame data. | |
| 3958 | + | * @param {number} pos - The position index in the frame data to be scaled. | |
| 3959 | + | * @param {number} max - The maximum value for scaling. | |
| 3960 | + | * @param {boolean} isRmsLevel - Whether if RMS level scaling should be applied. | |
| 3961 | + | * @returns {Array} The scaled frame data. | |
| 3962 | + | */ | |
| 3963 | + | getScaledFrames(frames, pos, max, isRmsLevel) { | |
| 3964 | + | const scaledFrames = new Array(frames.length); | |
| 3965 | + | const logMax = Math.log(Math.abs(max)); | |
| 3966 | + | ||
| 3967 | + | for (let i = 0; i < frames.length; i++) { | |
| 3968 | + | const frame = frames[i]; | |
| 3969 | + | const value = isFinite(frame[pos]) ? frame[pos] : -Infinity; | |
| 3970 | + | ||
| 3971 | + | let scaledVal = | |
| 3972 | + | !isFinite(value) ? 1 : | |
| 3973 | + | isRmsLevel ? 1 - Math.abs((value - max) / max) : | |
| 3974 | + | Math.abs(1 - (logMax + Math.log(Math.abs(value))) / logMax); | |
| 3975 | + | ||
| 3976 | + | if (!isFinite(scaledVal)) scaledVal = 0; | |
| 3977 | + | ||
| 3978 | + | const newFrame = frame.slice(0, this.metrics.count); | |
| 3979 | + | newFrame.push(scaledVal); | |
| 3980 | + | scaledFrames[i] = newFrame; | |
| 3981 | + | } | |
| 3982 | + | ||
| 3983 | + | return scaledFrames; | |
| 3984 | + | } | |
| 3985 | + | ||
| 3986 | + | /** | |
| 3987 | + | * Gets the resized frames based on the given scale and new frame count. | |
| 3988 | + | * @param {number} scale - The scale factor for resizing. | |
| 3989 | + | * @param {number} frames - The current number of frames. | |
| 3990 | + | * @param {number} newFrames - The desired number of frames after resizing. | |
| 3991 | + | * @returns {Array} The resized frame data. | |
| 3992 | + | */ | |
| 3993 | + | getResizedFrames(scale, frames, newFrames) { | |
| 3994 | + | const data = Array(newFrames).fill(null).map(() => ({ maxAbs: 0, maxSigned: 0, val: 0, count: 0 })); | |
| 3995 | + | const scaleFactor = newFrames < frames ? frames / newFrames : newFrames / frames; | |
| 3996 | + | const isWaveform = this.preset.analysisMode === 'waveform'; | |
| 3997 | + | ||
| 3998 | + | if (frames === 0 || newFrames === 0) return []; | |
| 3999 | + | ||
| 4000 | + | for (let i = 0, j = 0, h = 0; i < frames; i++) { | |
| 4001 | + | const frame = this.current[i]; | |
| 4002 | + | ||
| 4003 | + | if (newFrames < frames) { | |
| 4004 | + | if (isWaveform) { // Track max absolute and signed values for waveform | |
| 4005 | + | data[j].maxAbs = Math.max(data[j].maxAbs, Math.abs(frame)); | |
| 4006 | + | data[j].maxSigned = Math.abs(frame) > Math.abs(data[j].maxSigned) ? frame : data[j].maxSigned; | |
| 4007 | + | data[j].count++; | |
| 4008 | + | h += 1; | |
| 4009 | + | if (h >= scaleFactor) { | |
| 4010 | + | j++; | |
| 4011 | + | h -= scaleFactor; | |
| 4012 | + | if (j >= newFrames) break; | |
| 4013 | + | } | |
| 4014 | + | } | |
| 4015 | + | else { // Averaging logic for other modes | |
| 4016 | + | while (h >= scaleFactor) { | |
| 4017 | + | const w = h - scaleFactor; | |
| 4018 | + | if (j + 1 < newFrames) { | |
| 4019 | + | data[j + 1].val += frame * w; | |
| 4020 | + | data[j + 1].count += w; | |
| 4021 | + | } | |
| 4022 | + | j += 2; | |
| 4023 | + | h = 0; | |
| 4024 | + | if (j >= newFrames) break; | |
| 4025 | + | data[j].val += frame * (1 - w); | |
| 4026 | + | data[j].count += (1 - w); | |
| 4027 | + | } | |
| 4028 | + | if (i % 2 === 0 && j + 1 < newFrames) { | |
| 4029 | + | data[j + 1].val += frame; | |
| 4030 | + | data[j + 1].count++; | |
| 4031 | + | } else { | |
| 4032 | + | data[j].val += frame; | |
| 4033 | + | data[j].count++; | |
| 4034 | + | h++; | |
| 4035 | + | } | |
| 4036 | + | } | |
| 4037 | + | } | |
| 4038 | + | else { // Upsampling: repeat or interpolate frames | |
| 4039 | + | while (h < scaleFactor && j < newFrames) { | |
| 4040 | + | if (isWaveform) { | |
| 4041 | + | data[j].maxAbs = Math.max(data[j].maxAbs, Math.abs(frame)); | |
| 4042 | + | data[j].maxSigned = Math.abs(frame) > Math.abs(data[j].maxSigned) ? frame : data[j].maxSigned; | |
| 4043 | + | } else { | |
| 4044 | + | data[j].val += frame; | |
| 4045 | + | } | |
| 4046 | + | data[j].count++; | |
| 4047 | + | j++; | |
| 4048 | + | h++; | |
| 4049 | + | } | |
| 4050 | + | h -= scaleFactor; | |
| 4051 | + | } | |
| 4052 | + | } | |
| 4053 | + | ||
| 4054 | + | return data.filter(el => el.count > 0).map(el => isWaveform ? el.maxSigned : el.val / el.count); | |
| 4055 | + | } | |
| 4056 | + | ||
| 4057 | + | /** | |
| 4058 | + | * Normalizes points to ensure all points are on the same scale to prevent distortion of the waveform. | |
| 4059 | + | * @param {boolean} normalizeWidth - If `true`, adjusts the number of frames to match the window size. | |
| 4060 | + | */ | |
| 4061 | + | normalizePoints(normalizeWidth = false) { | |
| 4062 | + | if (!this.current.length) return; | |
| 4063 | + | ||
| 4064 | + | // Safety filter for any unexpected invalid frames | |
| 4065 | + | if (this.analysis.binaryMode === 'audioWizard' && !this.isFallback && !this.fallbackMode.paint) { | |
| 4066 | + | this.current = this.current.filter(frame => frame != null && Array.isArray(frame) && frame.length >= this.metrics.count); | |
| 4067 | + | } | |
| 4068 | + | ||
| 4069 | + | if (this.analysis.binaryMode === 'audioWizard' && !this.isFallback && !this.fallbackMode.paint && this.preset.analysisMode === 'waveform') { | |
| 4070 | + | const channels = this.currentChannels || 1; | |
| 4071 | + | const minIdx = this.metrics.index.min; | |
| 4072 | + | const maxIdx = this.metrics.index.max; | |
| 4073 | + | const metricsPerChannel = this.metrics.count; | |
| 4074 | + | ||
| 4075 | + | this.current = this.current.map(frame => { | |
| 4076 | + | let globalMin = Infinity; | |
| 4077 | + | let globalMax = -Infinity; | |
| 4078 | + | ||
| 4079 | + | // Process ALL channels | |
| 4080 | + | for (let ch = 0; ch < channels; ch++) { | |
| 4081 | + | const offset = ch * metricsPerChannel; | |
| 4082 | + | const chMin = frame[offset + minIdx]; | |
| 4083 | + | const chMax = frame[offset + maxIdx]; | |
| 4084 | + | ||
| 4085 | + | globalMin = Math.min(globalMin, chMin); | |
| 4086 | + | globalMax = Math.max(globalMax, chMax); | |
| 4087 | + | } | |
| 4088 | + | ||
| 4089 | + | // Return the value with larger magnitude | |
| 4090 | + | return Math.abs(globalMax) > Math.abs(globalMin) ? globalMax : globalMin; | |
| 4091 | + | }); | |
| 4092 | + | } | |
| 4093 | + | ||
| 4094 | + | let { upper, lower } = this.getMaxValue(this.current); | |
| 4095 | + | ||
| 4096 | + | if (this.analysis.binaryMode === 'audioWizard' && !this.isFallback && !this.fallbackMode.paint && this.preset.analysisMode !== 'waveform') { | |
| 4097 | + | const metric = this.metrics.mode[this.preset.analysisMode]; | |
| 4098 | + | const pos = this.metrics.index[metric]; | |
| 4099 | + | const minVal = this.getMinValuePos(this.current, pos); | |
| 4100 | + | ||
| 4101 | + | this.current = this.getScaledFrames(this.current, pos, minVal, this.preset.analysisMode === 'rms'); | |
| 4102 | + | this.current = this.getNormalizedFrameValues(this.current, Math.min(...this.current.map(frame => frame[this.metrics.count]))); | |
| 4103 | + | this.current = this.current.map((x, i) => Math.sign((0.5 - i % 2)) * (1 - x[this.metrics.count])); | |
| 4104 | + | } | |
| 4105 | + | else if (this.analysis.binaryMode === 'visualizer' || this.isFallback || this.fallbackMode.paint) { | |
| 4106 | + | const maxVal = Math.max(Math.abs(upper), Math.abs(lower)); | |
| 4107 | + | this.current = this.current.map(frame => frame / maxVal); | |
| 4108 | + | } | |
| 4109 | + | ||
| 4110 | + | if (normalizeWidth) { | |
| 4111 | + | const barW = this.getBarWidth(); | |
| 4112 | + | const frames = this.current.length; | |
| 4113 | + | const newFrames = Math.floor(this.w / barW); | |
| 4114 | + | ||
| 4115 | + | if (newFrames === frames) return; | |
| 4116 | + | ||
| 4117 | + | this.current = this.getResizedFrames(frames / newFrames, frames, newFrames); | |
| 4118 | + | ||
| 4119 | + | const bias = Math.abs(upper / lower); | |
| 4120 | + | upper = lower = 0; | |
| 4121 | + | ({ upper, lower } = this.getMaxValue(this.current)); | |
| 4122 | + | const newBias = Math.abs(upper / lower); | |
| 4123 | + | const biasDiff = bias - newBias; | |
| 4124 | + | ||
| 4125 | + | if (biasDiff > 0.1) { | |
| 4126 | + | const distort = bias / newBias; | |
| 4127 | + | const sign = Math.sign(biasDiff); | |
| 4128 | + | this.current = this.current.map(frame => (sign === 1 && frame > 0) || (sign !== 1 && frame < 0) ? frame * distort : frame); | |
| 4129 | + | } | |
| 4130 | + | } | |
| 4131 | + | ||
| 4132 | + | // Clamp frame values to [-1, 1] to prevent overflow/distortion from imbalanced data or edge cases | |
| 4133 | + | this.current = this.current.map(frame => Math.max(-1, Math.min(1, frame))); | |
| 4134 | + | } | |
| 4135 | + | ||
| 4136 | + | /** | |
| 4137 | + | * Resets the state of the waveform bar. | |
| 4138 | + | */ | |
| 4139 | + | reset() { | |
| 4140 | + | this.current = []; | |
| 4141 | + | this.cache = null; | |
| 4142 | + | this.time = 0; | |
| 4143 | + | this.step = 0; | |
| 4144 | + | this.maxStep = 6; | |
| 4145 | + | this.offset = []; | |
| 4146 | + | this.isAllowedFile = true; | |
| 4147 | + | this.isZippedFile = false; | |
| 4148 | + | this.isError = false; | |
| 4149 | + | this.isFallback = false; | |
| 4150 | + | this.fallbackMode.paint = this.fallbackMode.analysis = false; | |
| 4151 | + | this.resetAnimation(); | |
| 4152 | + | clearTimeout(this.queueId); | |
| 4153 | + | } | |
| 4154 | + | ||
| 4155 | + | /** | |
| 4156 | + | * Resets the state of the waveform bar animation. | |
| 4157 | + | */ | |
| 4158 | + | resetAnimation() { | |
| 4159 | + | this.step = 0; | |
| 4160 | + | this.offset = []; | |
| 4161 | + | this.defaultSteps(); | |
| 4162 | + | } | |
| 4163 | + | ||
| 4164 | + | /** | |
| 4165 | + | * Sets the refresh rate for the waveform bar. | |
| 4166 | + | */ | |
| 4167 | + | setRefreshRate() { | |
| 4168 | + | if (grm.ui.isStreaming) { // Radio streaming refresh rate | |
| 4169 | + | this.ui.refreshRate = grm.ui.seekbarTimerInterval = FPS._1; | |
| 4170 | + | } | |
| 4171 | + | else if (grSet.waveformBarRefreshRate !== 'variable') { // Fixed refresh rate | |
| 4172 | + | this.ui.refreshRate = grm.ui.seekbarTimerInterval = grSet.waveformBarRefreshRate; | |
| 4173 | + | } | |
| 4174 | + | else { // Variable refresh rate calculation | |
| 4175 | + | const now = Date.now(); | |
| 4176 | + | if (this.updateTimeLast && (now - this.updateTimeLast) < 250) return; // Check every 250 ms | |
| 4177 | + | this.updateTimeLast = now; | |
| 4178 | + | ||
| 4179 | + | if (this.profilerPaintTimeLast === undefined) { | |
| 4180 | + | this.profilerPaintTimeLast = grm.ui.seekbarProfiler.Time; | |
| 4181 | + | } | |
| 4182 | + | ||
| 4183 | + | const timeDifference = grm.ui.seekbarProfiler.Time - this.profilerPaintTimeLast; | |
| 4184 | + | this.ui.refreshRate = grm.ui.seekbarTimerInterval = Clamp(grm.ui.seekbarTimerInterval + (timeDifference > 0 ? 12 : -7), FPS._10, FPS._5); | |
| 4185 | + | this.profilerPaintTimeLast = grm.ui.seekbarProfiler.Time; | |
| 4186 | + | ||
| 4187 | + | grm.ui.clearTimer('seekbar', true); | |
| 4188 | + | grm.ui.seekbarTimer = !fb.IsPaused ? setInterval(() => grm.ui.refreshSeekbar(), grm.ui.seekbarTimerInterval) : null; | |
| 4189 | + | } | |
| 4190 | + | } | |
| 4191 | + | ||
| 4192 | + | /** | |
| 4193 | + | * Sets the rectangular area to be painted. | |
| 4194 | + | * @param {number} time - The current playback time. | |
| 4195 | + | * @returns {{ x: number, y: number, width: number, height: number }} The object containing the dimensions of the rectangle to be painted. | |
| 4196 | + | */ | |
| 4197 | + | setPaintRect(time) { | |
| 4198 | + | const widerModesScale = ['bars', 'halfbars'].includes(this.preset.barDesign) ? 2 : 1; | |
| 4199 | + | const barW = Math.ceil(Math.max(this.w / this.current.length, SCALE(2))) * widerModesScale; | |
| 4200 | + | const currX = this.x + (this.w * time / fb.PlaybackLength); | |
| 4201 | + | ||
| 4202 | + | const prePaintW = Math.min( | |
| 4203 | + | this.prepaint && this.preset.prepaintFront !== Infinity || this.preset.animate | |
| 4204 | + | ? this.preset.prepaintFront === Infinity && this.preset.animate | |
| 4205 | + | ? Infinity | |
| 4206 | + | : (this.preset.prepaintFront / this.timeConstant * barW) + barW | |
| 4207 | + | : 2.5 * barW, | |
| 4208 | + | this.w - currX + barW | |
| 4209 | + | ); | |
| 4210 | + | ||
| 4211 | + | return { | |
| 4212 | + | x: currX - barW - grm.ui.edgeMargin, | |
| 4213 | + | y: this.y, | |
| 4214 | + | width: prePaintW + grm.ui.edgeMarginBoth, | |
| 4215 | + | height: this.h | |
| 4216 | + | }; | |
| 4217 | + | } | |
| 4218 | + | ||
| 4219 | + | /** | |
| 4220 | + | * Sets the throttle paint methods based on the current UI refresh rate. | |
| 4221 | + | */ | |
| 4222 | + | setThrottlePaint() { | |
| 4223 | + | /** | |
| 4224 | + | * Throttles the window repaint to improve performance by limiting the rate of repaint operations. | |
| 4225 | + | * This function is specifically tailored to repaint a defined rectangular area of the window. | |
| 4226 | + | * The repaint is controlled by the UI refresh rate. | |
| 4227 | + | * @param {boolean} [force] - If set to true, the repaint will be forced even if the window is not dirty. | |
| 4228 | + | * @private | |
| 4229 | + | */ | |
| 4230 | + | this.throttlePaint = Throttle((force = false) => | |
| 4231 | + | window.RepaintRect(this.x - SCALE(2), this.y - this.h * 0.5 - SCALE(4), this.w + SCALE(4), this.h + SCALE(8), force), this.ui.refreshRate); | |
| 4232 | + | ||
| 4233 | + | /** | |
| 4234 | + | * Throttles the window repaint to improve performance by limiting the rate of repaint operations. | |
| 4235 | + | * This function allows for the specification of the rectangular area to be repainted. | |
| 4236 | + | * The repaint is controlled by the UI refresh rate. | |
| 4237 | + | * @param {number} x - The x-coordinate of the upper-left corner of the rectangle to repaint. | |
| 4238 | + | * @param {number} y - The y-coordinate of the upper-left corner of the rectangle to repaint. | |
| 4239 | + | * @param {number} w - The width of the rectangle to repaint. | |
| 4240 | + | * @param {number} h - The height of the rectangle to repaint. | |
| 4241 | + | * @param {boolean} [force] - If set to true, the repaint will be forced even if the window is not dirty. | |
| 4242 | + | * @private | |
| 4243 | + | */ | |
| 4244 | + | this.throttlePaintRect = Throttle((x, y, w, h, force = false) => | |
| 4245 | + | window.RepaintRect(x - SCALE(2), y - h * 0.5 - SCALE(4), w + SCALE(4), h + SCALE(8), force), this.ui.refreshRate); | |
| 4246 | + | } | |
| 4247 | + | ||
| 4248 | + | /** | |
| 4249 | + | * Sets the vertical waveform bar position. | |
| 4250 | + | * @param {number} y - The y-coordinate. | |
| 4251 | + | */ | |
| 4252 | + | setY(y) { | |
| 4253 | + | this.y = y + SCALE(10); | |
| 4254 | + | } | |
| 4255 | + | ||
| 4256 | + | /** | |
| 4257 | + | * This method is currently not used. | |
| 4258 | + | * @param {boolean} [enable] - If true, activates the component; if false, deactivates it. | |
| 4259 | + | */ | |
| 4260 | + | switch(enable = !this.active) { | |
| 4261 | + | if (!fb.IsPlaying) return; | |
| 4262 | + | ||
| 4263 | + | const wasActive = this.active; | |
| 4264 | + | this.active = enable; | |
| 4265 | + | ||
| 4266 | + | if (!wasActive && this.active) { | |
| 4267 | + | window.Repaint(); | |
| 4268 | + | setTimeout(() => { | |
| 4269 | + | this.on_playback_new_track(fb.GetNowPlaying()); | |
| 4270 | + | this.on_playback_time(fb.PlaybackTime); | |
| 4271 | + | }, 0); | |
| 4272 | + | } | |
| 4273 | + | else if (wasActive && !this.active) { | |
| 4274 | + | this.on_playback_stop(-1); | |
| 4275 | + | } | |
| 4276 | + | } | |
| 4277 | + | ||
| 4278 | + | /** | |
| 4279 | + | * Checks if the mouse is within the boundaries of the waveform bar. | |
| 4280 | + | * @param {number} x - The x-coordinate. | |
| 4281 | + | * @param {number} y - The y-coordinate. | |
| 4282 | + | * @returns {boolean} True or false. | |
| 4283 | + | */ | |
| 4284 | + | trace(x, y) { | |
| 4285 | + | return (x >= this.x && y >= this.y && x <= this.x + this.w && y <= this.y + this.h); | |
| 4286 | + | } | |
| 4287 | + | ||
| 4288 | + | /** | |
| 4289 | + | * Updates the waveform bar with the current track information, playback time and size. | |
| 4290 | + | * @param {boolean} current - Whether the current track has changed or not. | |
| 4291 | + | */ | |
| 4292 | + | updateBar(current) { | |
| 4293 | + | if (!current) this.on_playback_new_track(fb.GetNowPlaying()); | |
| 4294 | + | this.on_playback_time(fb.PlaybackTime); | |
| 4295 | + | this.on_size(grm.ui.ww, grm.ui.wh); | |
| 4296 | + | } | |
| 4297 | + | // #endregion | |
| 4298 | + | ||
| 4299 | + | // * CALLBACKS * // | |
| 4300 | + | // #region CALLBACKS | |
| 4301 | + | /** | |
| 4302 | + | * Handles left mouse button up click events and disables dragging and updates the playback time. | |
| 4303 | + | * @param {number} x - The x-coordinate. | |
| 4304 | + | * @param {number} y - The y-coordinate. | |
| 4305 | + | * @param {number} mask - The mouse mask. | |
| 4306 | + | * @returns {boolean} True or false. | |
| 4307 | + | */ | |
| 4308 | + | on_mouse_lbtn_up(x, y, mask) { | |
| 4309 | + | if (['progressbar', 'peakmeterbar'].includes(grSet.seekbar) || | |
| 4310 | + | !this.active || !this.trace(x, y) || !fb.IsPlaying || this.current.length === 0) { | |
| 4311 | + | this.mouseDown = false; | |
| 4312 | + | return false; | |
| 4313 | + | } | |
| 4314 | + | ||
| 4315 | + | this.mouseDown = false; | |
| 4316 | + | ||
| 4317 | + | if (!fb.GetSelection()) return; | |
| 4318 | + | ||
| 4319 | + | const barW = this.w / this.current.length; | |
| 4320 | + | const time = Math.round(fb.PlaybackLength / this.current.length * (x - this.x) / barW); | |
| 4321 | + | fb.PlaybackTime = Clamp(time, 0, fb.PlaybackLength); | |
| 4322 | + | this.throttlePaint(true); | |
| 4323 | + | ||
| 4324 | + | return true; | |
| 4325 | + | } | |
| 4326 | + | ||
| 4327 | + | /** | |
| 4328 | + | * Handles mouse movement events on the waveform bar. | |
| 4329 | + | * @param {number} x - The x-coordinate. | |
| 4330 | + | * @param {number} y - The y-coordinate. | |
| 4331 | + | * @param {number} mask - The mouse mask. | |
| 4332 | + | */ | |
| 4333 | + | on_mouse_move(x, y, mask) { | |
| 4334 | + | if (['progressbar', 'peakmeterbar'].includes(grSet.seekbar)) { | |
| 4335 | + | return; | |
| 4336 | + | } | |
| 4337 | + | ||
| 4338 | + | this.mouseDown = (mask === MouseKey.LButton && this.on_mouse_lbtn_up(x, y, mask)); | |
| 4339 | + | } | |
| 4340 | + | ||
| 4341 | + | /** | |
| 4342 | + | * Handles the mouse wheel event to seek through the playback. | |
| 4343 | + | * @param {number} step - The wheel scroll direction. | |
| 4344 | + | * @returns {boolean} True or false. | |
| 4345 | + | */ | |
| 4346 | + | on_mouse_wheel(step) { | |
| 4347 | + | if (!this.active || !fb.GetSelection() || !fb.IsPlaying || this.current.length === 0) { | |
| 4348 | + | return false; | |
| 4349 | + | } | |
| 4350 | + | ||
| 4351 | + | const seekType = { | |
| 4352 | + | seconds: (scroll) => scroll * this.wheel.seekSpeed, | |
| 4353 | + | percentage: (scroll) => (scroll * this.wheel.seekSpeed) / 100 * fb.PlaybackLength | |
| 4354 | + | }; | |
| 4355 | + | ||
| 4356 | + | const seekTypeFunc = seekType[this.wheel.seekType] || seekType.seconds; | |
| 4357 | + | const newTime = fb.PlaybackTime + seekTypeFunc(step); | |
| 4358 | + | fb.PlaybackTime = Clamp(newTime, 0, fb.PlaybackLength); | |
| 4359 | + | this.throttlePaint(true); | |
| 4360 | + | ||
| 4361 | + | return true; | |
| 4362 | + | } | |
| 4363 | + | ||
| 4364 | + | /** | |
| 4365 | + | * Resets the current waveform and processes new data for the new current playing track. | |
| 4366 | + | * @param {FbMetadbHandle} handle - The handle of the new track. | |
| 4367 | + | * @param {boolean} [isRetry] - The flag indicating whether the method call is a retry attempt. | |
| 4368 | + | * @returns {Promise<void>} The promise that resolves when the processing has finished. | |
| 4369 | + | */ | |
| 4370 | + | async on_playback_new_track(handle = fb.GetNowPlaying(), isRetry = false) { | |
| 4371 | + | if (['progressbar', 'peakmeterbar'].includes(grSet.seekbar) || !this.active || !handle) { | |
| 4372 | + | return; | |
| 4373 | + | } | |
| 4374 | + | ||
| 4375 | + | this.reset(); | |
| 4376 | + | this.checkAllowedFile(handle); | |
| 4377 | + | await this.analyzeDataStart(handle, isRetry); | |
| 4378 | + | this.resetAnimation(); | |
| 4379 | + | ||
| 4380 | + | if (this.preset.animate && this.preset.useBPM) this.bpmSteps(handle); | |
| 4381 | + | if (fb.IsPlaying) this.time = fb.PlaybackTime; | |
| 4382 | + | ||
| 4383 | + | this.throttlePaint(); | |
| 4384 | + | } | |
| 4385 | + | ||
| 4386 | + | /** | |
| 4387 | + | * Schedules the `on_playback_new_track` event to be triggered after a specified delay. | |
| 4388 | + | * This is useful for debouncing the event, ensuring it is fired only once after a series of track changes. | |
| 4389 | + | */ | |
| 4390 | + | on_playback_new_track_queue() { | |
| 4391 | + | clearTimeout(this.queueId); | |
| 4392 | + | ||
| 4393 | + | this.queueId = setTimeout(() => { | |
| 4394 | + | this.on_playback_new_track(...arguments); | |
| 4395 | + | }, this.queueMs); | |
| 4396 | + | } | |
| 4397 | + | ||
| 4398 | + | /** | |
| 4399 | + | * Resets the waveform bar on playback stop. | |
| 4400 | + | * @param {number} reason - The type of playback stop. | |
| 4401 | + | */ | |
| 4402 | + | on_playback_stop(reason = -1) { | |
| 4403 | + | if (['progressbar', 'peakmeterbar'].includes(grSet.seekbar) || reason !== -1 && !this.active) { | |
| 4404 | + | return; | |
| 4405 | + | } | |
| 4406 | + | ||
| 4407 | + | this.reset(); | |
| 4408 | + | if (reason !== 2) this.throttlePaint(); | |
| 4409 | + | } | |
| 4410 | + | ||
| 4411 | + | /** | |
| 4412 | + | * Updates the waveform bar with throttled repaints. | |
| 4413 | + | * @param {number} time - The current playback time. | |
| 4414 | + | */ | |
| 4415 | + | on_playback_time(time) { | |
| 4416 | + | if (['progressbar', 'peakmeterbar'].includes(grSet.seekbar) || !this.active) { | |
| 4417 | + | return; | |
| 4418 | + | } | |
| 4419 | + | ||
| 4420 | + | this.time = time; | |
| 4421 | + | ||
| 4422 | + | if ((this.preset.paintMode === 'full' || this.preset.indicator || this.analysis.binaryMode === 'visualizer') && | |
| 4423 | + | this.cache === this.current) { | |
| 4424 | + | return; | |
| 4425 | + | } | |
| 4426 | + | ||
| 4427 | + | this.cache = this.current; | |
| 4428 | + | ||
| 4429 | + | if (this.analysis.binaryMode === 'visualizer' || !this.current.length) { | |
| 4430 | + | this.throttlePaint(); | |
| 4431 | + | } | |
| 4432 | + | else if (this.preset.paintMode === 'partial' || this.preset.indicator) { | |
| 4433 | + | const paintRect = this.setPaintRect(this.time); | |
| 4434 | + | this.throttlePaintRect(paintRect.x, paintRect.y, paintRect.width, paintRect.height); | |
| 4435 | + | } | |
| 4436 | + | } | |
| 4437 | + | ||
| 4438 | + | /** | |
| 4439 | + | * Handles the waveform bar state when reloading the theme. | |
| 4440 | + | */ | |
| 4441 | + | on_script_unload() { | |
| 4442 | + | if (['progressbar', 'peakmeterbar'].includes(grSet.seekbar)) return; | |
| 4443 | + | if (this.analysis.autoDelete) this.removeData(); | |
| 4444 | + | } | |
| 4445 | + | ||
| 4446 | + | /** | |
| 4447 | + | * Sets the size and position of the waveform bar and updates them on window resizing. | |
| 4448 | + | * @param {number} w - The width of the waveform bar. | |
| 4449 | + | * @param {number} h - The height of the waveform bar. | |
| 4450 | + | */ | |
| 4451 | + | on_size(w, h) { | |
| 4452 | + | if (['progressbar', 'peakmeterbar'].includes(grSet.seekbar)) return; | |
| 4453 | + | this.x = grm.ui.edgeMargin; | |
| 4454 | + | this.y = 0; | |
| 4455 | + | this.w = w - grm.ui.edgeMarginBoth; | |
| 4456 | + | this.h = grm.ui.seekbarHeight; | |
| 4457 | + | } | |
| 4458 | + | // #endregion | |
| 4459 | + | } | |