From 41061a18732272b2992a97ad74d8073800eac7d7 Mon Sep 17 00:00:00 2001 From: Artem Korsakov Date: Fri, 14 Mar 2025 09:37:04 +0300 Subject: [PATCH] Add progress bar --- helpers/htmlGenerators.ts | 89 +++++++++++++++++++++++++++++---------- helpers/parsers.ts | 29 ++++++++++++- helpers/types.ts | 3 ++ main.ts | 14 ------ styles.css | 45 ++++++++++++++------ 5 files changed, 130 insertions(+), 50 deletions(-) diff --git a/helpers/htmlGenerators.ts b/helpers/htmlGenerators.ts index df63e45..ae05123 100644 --- a/helpers/htmlGenerators.ts +++ b/helpers/htmlGenerators.ts @@ -33,23 +33,35 @@ export function generateImageHTML(account: string): string { return `Profile ${account}`; } -export function determineTop(solved: number, rating: RatingData): string { +interface Result { + message: string; + percentage: number; +} + +export function determineTop(solved: number, rating: RatingData): Result { const thresholds = [ - { limit: 100, top: rating.top100, message: 'Top 100' }, - { limit: 50, top: rating.top50, message: 'Top 50' }, - { limit: 25, top: rating.top25, message: 'Top 25' }, - { limit: 10, top: rating.top10, message: 'Top 10' }, - { limit: 5, top: rating.top5, message: 'Top 5' }, - { limit: 1, top: rating.top1, message: 'Top 1' }, + { limit: 100, top: rating.top100, title: 'Top 100' }, + { limit: 50, top: rating.top50, title: 'Top 50' }, + { limit: 25, top: rating.top25, title: 'Top 25' }, + { limit: 10, top: rating.top10, title: 'Top 10' }, + { limit: 5, top: rating.top5, title: 'Top 5' }, + { limit: 1, top: rating.top1, title: 'Top 1' }, ]; - for (const { limit, top, message } of thresholds) { + for (let i = 0; i < thresholds.length; i++) { + const { limit, top, title } = thresholds[i]; if (rating.place > limit) { - return `${top - solved + 1} problems away from ${message}`; + const prevTop = i > 0 ? thresholds[i - 1].top : 0; // Получаем предыдущее значение top + const rest = top - solved + 1; + const all = top - prevTop + 1; + + const percentage = all > 0 ? Math.round(((all - rest) / all) * 100) : 0; + const message = `${rest} problems away from ${title}`; + return { message, percentage }; } } - return 'You are in the Top 1'; + return { message: 'You are in the Top 1', percentage: 100 }; } /** @@ -68,6 +80,17 @@ export function findAwardWithMaxMembers(awardBlocks: AwardBlockData[]): AwardDat }, allAwards[0] || null); } +function createProgressBar(percentage: number): string { + const clampedPercentage = Math.min(100, Math.max(0, percentage)); + + return ` +
+ + ${clampedPercentage}% +
+ `; +} + /** * Generates HTML for the progress table. * @param accountData - The account data. @@ -91,25 +114,44 @@ export function generateProgressTableHTML( ): string { const locationPlace = locationRating.place > 100 ? 'You are not in the Top 100' : locationRating.place; const languagePlace = languageRating.place > 100 ? 'You are not in the Top 100' : languageRating.place; + + const progressPercentage = createProgressBar(progressData.percentage); + + const locationPlaceToTop = locationRating.place > 100 ? 0 : 100 - locationRating.place; + const locationPercentage = createProgressBar(locationPlaceToTop); + const languagePlaceToTop = languageRating.place > 100 ? 0 : 100 - languageRating.place; + const languagePercentage = createProgressBar(languagePlaceToTop); + + const matchToTheNext = progressData.toTheNext.match(/(\d+)/); + const countToTheNext = matchToTheNext ? (25 - parseInt(matchToTheNext[1], 10)) * 4 : 0; + const toTheNextPercentage = createProgressBar(countToTheNext); + + const locationResult = determineTop(progressData.solved, locationRating) + const locationLiElement = `
  • ${accountData.location}: ${locationResult.message}. ${createProgressBar(locationResult.percentage)}
  • `; + + const languageResult = determineTop(progressData.solved, languageRating) + const languageLiElement = `
  • ${accountData.language}: ${languageResult.message}. ${createProgressBar(languageResult.percentage)}
  • `; + const award = findAwardWithMaxMembers(awardsData); - const awardPlace = award ? `
  • Most unresolved award: ${award.award} (${award.description}) by ${award.members}
  • ` : ''; + const awardPlace = award ? `
  • Most unresolved award: ${award.award} (${award.description}) by ${award.members}. ${createProgressBar(award.percentage)}
  • ` : ''; return `

    Progress

    + ${progressPercentage} - - - - - + + + + +
    CompetitionStatus
    Progress${progressData.progress}
    Place in Eulerians${euleriansPlace}
    Place in ${accountData.location}${locationPlace}
    Place in ${accountData.language}${languagePlace}
    CompetitionStatusProgress bar
    Progress${progressData.progress}${progressPercentage}
    Place in Eulerians${euleriansPlace}
    Place in ${accountData.location}${locationPlace}${locationPercentage}
    Place in ${accountData.language}${languagePlace}${languagePercentage}

    Tasks

    `; @@ -200,16 +242,19 @@ export function generateAwardsTableHTML(awardsData: AwardBlockData[]): string { const length = awards.length; // Подсчитываем завершенные и создаем строки таблицы за один проход - const { completed, rows } = awards.reduce((acc, { award, link, description, isCompleted, progress, members }) => { + const { completed, rows } = awards.reduce((acc, { award, link, description, isCompleted, progress, percentage, members }) => { if (isCompleted) { acc.completed++; } else { + const progressBar = createProgressBar(percentage); + acc.rows.push(` ${award} ${description} - ${progress} ${members} + ${progress} + ${progressBar} `); } @@ -222,7 +267,7 @@ export function generateAwardsTableHTML(awardsData: AwardBlockData[]): string {

    Uncompleted awards: ${length - completed}

    - + ${rows.join('')}
    AwardDescriptionProgressMembers
    AwardDescriptionMembersProgressProgress bar
    diff --git a/helpers/parsers.ts b/helpers/parsers.ts index bc20b8a..9235ef5 100644 --- a/helpers/parsers.ts +++ b/helpers/parsers.ts @@ -12,6 +12,13 @@ export function extractSolvedCount(text: string): number { return match && match[1] ? parseInt(match[1], 10) : 0; } +function extractPercentage(input: string): number { + const regex = /(\d+(\.\d+)?)%/; + const match = input.match(regex); + + return match ? parseFloat(match[1]) : 0; +} + /** * Converts a string to a number by removing non-numeric characters. * @param input - The input string. @@ -52,9 +59,10 @@ export function parseProgressData(html: string): ProgressData { const level = doc.querySelector('h3#level_text')?.textContent || ''; const progress = doc.querySelector('div#progress_page > h3')?.textContent || ''; const solved = extractSolvedCount(progress); + const percentage = extractPercentage(progress); const toTheNext = doc.querySelector('#progress_page > .progress_bar_with_threshold > span')?.textContent || ''; - return new ProgressData(level, solved, progress, toTheNext); + return new ProgressData(level, solved, percentage, progress, toTheNext); } /** @@ -108,6 +116,22 @@ export function parseLevelData(html: string): LevelData[] { }); } +function calculatePercentage(input: string): number { + const regex = /(\d+)\s*\/\s*(\d+)/; + const match = input.match(regex); + + if (match) { + const solved = parseInt(match[1], 10); + const total = parseInt(match[2], 10); + + if (total > 0) { + return ((solved / total) * 100).toFixed(2); + } + } + + return 0; +} + export function parseAwardsData(myAwardsHtml: string, awardsHtml: string): AwardBlockData[] { const parser = new DOMParser(); const myAwardsDoc = parser.parseFromString(myAwardsHtml, 'text/html'); @@ -131,11 +155,12 @@ export function parseAwardsData(myAwardsHtml: string, awardsHtml: string): Award const description = tooltipTextParts[0].replace(award, '').replace('Completed', '').trim(); const isCompleted = tooltipText.endsWith('Completed'); const progress = tooltipTextParts[1]?.trim() || ''; + const percentage = calculatePercentage(progress); const membersSource = membersTextArray.find(memberText => memberText.startsWith(award)) ?? '\n0 members'; const members = membersSource.split('\n').pop() || '0 members'; - return new AwardData(award, link, description, isCompleted, progress, members); + return new AwardData(award, link, description, isCompleted, progress, percentage, members); }); return new AwardBlockData(name, awards); diff --git a/helpers/types.ts b/helpers/types.ts index 62ed4e0..606e849 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -17,6 +17,7 @@ export class ProgressData { constructor( public level: string, public solved: number, + public percentage: number, public progress: string, public toTheNext: string ) {} @@ -78,6 +79,7 @@ export class AwardData { * @param description - A description of the award. * @param isCompleted - A boolean indicating whether the award has been completed. * @param progress - A string representing the progress associated with the award. + * @param percentage - A number representing the percentage of completion. * @param members - A string representing the members associated with this award. */ constructor( @@ -86,6 +88,7 @@ export class AwardData { public description: string, public isCompleted: boolean, public progress: string, + public percentage: number, public members: string ) {} } diff --git a/main.ts b/main.ts index 9b86f09..3e3333a 100644 --- a/main.ts +++ b/main.ts @@ -16,20 +16,6 @@ export default class ProjectEulerStatsPlugin extends Plugin { this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000)); - this.registerMarkdownCodeBlockProcessor('euler-stats-profile', (source, el, ctx) => { - const matchingLine = source.split('\n').find((line) => line.startsWith("account=")); - - if (matchingLine) { - const account = matchingLine.split("=")[1].trim(); - const imgElement = el.createEl('img'); - imgElement.src = 'https://projecteuler.net/profile/' + account + '.png'; - imgElement.alt = 'Profile ' + account; - } else { - const divElement = el.createEl('div'); - divElement.textContent = 'The "account=" parameter is not set or is set incorrectly!'; - } - }); - const session = this.settings.session_id; const keep_alive = this.settings.keep_alive; const cookies = 'PHPSESSID=' + session + '; keep_alive=' + keep_alive; diff --git a/styles.css b/styles.css index 0ce20ec..9d0f964 100644 --- a/styles.css +++ b/styles.css @@ -120,24 +120,12 @@ h5 { /* Медиа-запросы для адаптивности */ @media (max-width: 768px) { - .progress-bar-container { - margin: 10px 0; - } - .tasks-list { padding: 10px; } - - .award-row { - padding: 10px; - } } @media (max-width: 480px) { - .progress-bar-height { - height: 15px; - } - .profile-table td { padding: 8px; font-size: 14px; @@ -147,3 +135,36 @@ h5 { #your-account td { background-color: #90ee90; /* Светло-зеленый цвет */ } + +/* Контейнер для progress bar */ +.progress-container { + width: 100%; /* Занимает всю ширину ячейки таблицы */ + background-color: #e0e0e0; /* Фон контейнера */ + border-radius: 4px; /* Закругленные углы */ + overflow: hidden; /* Скрываем содержимое, выходящее за пределы */ + height: 12px; /* Компактная высота */ + position: relative; /* Для позиционирования текста */ +} + +/* Сам progress bar */ +.progress-bar { + position: absolute; /* Позиционируем текст поверх progress bar */ + top: 0%; /* Выравниваем по вертикали */ + left: 0%; /* Выравниваем по горизонтали */ + height: 100%; /* Занимает всю высоту контейнера */ + background-color: #1abc9c; /* Цвет прогресса */ + border-radius: 4px; /* Закругленные углы */ + padding: 0; /* Отсутствие отступов */ +} + +/* Текст с процентом */ +.progress-text { + position: absolute; /* Позиционируем текст поверх progress bar */ + top: 50%; /* Выравниваем по вертикали */ + left: 50%; /* Выравниваем по горизонтали */ + transform: translate(-50%, -50%); /* Центрируем текст */ + color: #2c3e50; /* Цвет текста */ + font-size: 10px; /* Компактный размер шрифта */ + font-weight: bold; /* Жирный шрифт */ + white-space: nowrap; /* Текст не переносится */ +}