diff --git a/websites/C/Chess.com/Chess.com.json b/websites/C/Chess.com/Chess.com.json new file mode 100644 index 000000000000..bcb515ce7107 --- /dev/null +++ b/websites/C/Chess.com/Chess.com.json @@ -0,0 +1,262 @@ +{ + "chess.com.play": { + "message": "Playing", + "description": "General playing status" + }, + "chess.com.pause": { + "message": "Paused", + "description": "Paused status" + }, + "chess.com.browsing": { + "message": "Browsing", + "description": "Default status when browsing the site" + }, + "chess.com.menu": { + "message": "Menu", + "description": "General menu navigation" + }, + "chess.com.overview": { + "message": "Overview", + "description": "Viewing overview page" + }, + "chess.com.library": { + "message": "Library", + "description": "Browsing library section" + }, + "chess.com.vs_separator": { + "message": "vs", + "description": "Short versus separator" + }, + "chess.com.min": { + "message": "min", + "description": "Abbreviation for minutes" + }, + "chess.com.survival": { + "message": "Survival", + "description": "Survival game mode" + }, + "chess.com.home": { + "message": "Home Page", + "description": "Viewing the home page" + }, + "chess.com.computerVs": { + "message": "Playing against a bot", + "description": "Playing against computer" + }, + "chess.com.computerSelecting": { + "message": "Choosing the bot", + "description": "When on the bot selection screen" + }, + "chess.com.computerAi": { + "message": "Vs AI", + "description": "Fallback state when playing computer" + }, + "chess.com.computerName": { + "message": "Computer", + "description": "Name of the activity" + }, + "chess.com.computerWhite": { + "message": "Playing as White", + "description": "Playing as white pieces against computer" + }, + "chess.com.computerBlack": { + "message": "Playing as Black", + "description": "Playing as black pieces against computer" + }, + "chess.com.puzzleSolving": { + "message": "Solving Puzzles", + "description": "Doing rated puzzles" + }, + "chess.com.puzzleRush": { + "message": "Puzzle Rush", + "description": "Playing Puzzle Rush mode" + }, + "chess.com.puzzleBattle": { + "message": "Puzzle Battle", + "description": "Playing Puzzle Battle mode" + }, + "chess.com.puzzleTactics": { + "message": "Tactics", + "description": "Small image text for puzzles" + }, + "chess.com.puzzleScore": { + "message": "Score", + "description": "Current puzzle score" + }, + "chess.com.puzzleLevel": { + "message": "Level", + "description": "Current puzzle level" + }, + "chess.com.puzzleRating": { + "message": "Rating", + "description": "Puzzle rating" + }, + "chess.com.playOnline": { + "message": "Playing Online", + "description": "Generic status for online play" + }, + "chess.com.playDaily": { + "message": "Daily Chess", + "description": "Playing correspondence chess" + }, + "chess.com.playMatch": { + "message": "In Match", + "description": "State when playing but opponent unknown" + }, + "chess.com.playLive": { + "message": "Live", + "description": "Small image text for live chess" + }, + "chess.com.playLobby": { + "message": "In Lobby / Matching", + "description": "Waiting for a game" + }, + "chess.com.gameSearching": { + "message": "Searching", + "description": "Searching for an opponent" + }, + "chess.com.gameFinished": { + "message": "Game Finished", + "description": "Game has ended" + }, + "chess.com.gameOver": { + "message": "Game Over", + "description": "Game over status" + }, + "chess.com.gameReplay": { + "message": "Watching Replay", + "description": "Watching a game replay" + }, + "chess.com.gameSpectating": { + "message": "Spectating", + "description": "Spectating a live game" + }, + "chess.com.gameWaiting": { + "message": "Waiting...", + "description": "Waiting for game to start" + }, + "chess.com.gameProgress": { + "message": "Match in Progress", + "description": "Match currently ongoing" + }, + "chess.com.gameArchive": { + "message": "Master Game Archive", + "description": "Browsing master games archive" + }, + "chess.com.gameReview": { + "message": "Game Review", + "description": "Reviewing a completed game" + }, + "chess.com.mediaAnalysis": { + "message": "Analyzing a game", + "description": "Using the analysis board" + }, + "chess.com.mediaLearning": { + "message": "Learning", + "description": "General learning status" + }, + "chess.com.mediaLessons": { + "message": "Watching Lessons", + "description": "Watching a video lesson" + }, + "chess.com.mediaTv": { + "message": "Watching Chess TV", + "description": "Watching official broadcast" + }, + "chess.com.mediaVideo": { + "message": "Watching a video", + "description": "Watching a recorded video" + }, + "chess.com.videoBrowsing": { + "message": "Browsing Videos", + "description": "Looking through video library" + }, + "chess.com.friendsList": { + "message": "Viewing Friends List", + "description": "Looking at friends page" + }, + "chess.com.friendsSingle": { + "message": "Friend", + "description": "Singular form for friend" + }, + "chess.com.friendsPlural": { + "message": "Friends", + "description": "Plural form for friends" + }, + "chess.com.profileViewing": { + "message": "Viewing Profile", + "description": "Viewing a user profile" + }, + "chess.com.profileGeneral": { + "message": "Profile", + "description": "General profile page" + }, + "chess.com.buttonViewGame": { + "message": "View Game", + "description": "Button label to view the game" + }, + "chess.com.buttonWatchVideo": { + "message": "Watch Video", + "description": "Button label to watch the video" + }, + "chess.com.variantsMenu": { + "message": "Chess Variants", + "description": "Playing variants like 4PC etc" + }, + "chess.com.classroomTitle": { + "message": "Classroom", + "description": "Classroom section" + }, + "chess.com.classroomSession": { + "message": "In Session", + "description": "In a classroom session" + }, + "chess.com.practiceTitle": { + "message": "Practice", + "description": "Practice mode" + }, + "chess.com.learnOpenings": { + "message": "Openings", + "description": "Learning openings" + }, + "chess.com.learnAllLessons": { + "message": "All Lessons", + "description": "Viewing all lessons" + }, + "chess.com.insightsTitle": { + "message": "Insights", + "description": "Insights/statistics section" + }, + "chess.com.insightsStats": { + "message": "Stats", + "description": "Statistics label" + }, + "chess.com.gameReviewing": { + "message": "Reviewing Game", + "description": "Reviewing a completed game or variant" + }, + "chess.com.gameOnline": { + "message": "Online", + "description": "Generic online game mode" + }, + "chess.com.tvChecking": { + "message": "Checking Schedule", + "description": "Checking TV schedule or loading stream" + }, + "chess.com.mediaFinished": { + "message": "Finished", + "description": "Game or media finished" + }, + "chess.com.profileGeneralAlt": { + "message": "Profile", + "description": "Alternative profile label" + }, + "chess.com.videoLibrary": { + "message": "Library", + "description": "Browsing video library" + }, + "chess.com.videoWatching": { + "message": "Video", + "description": "Watching a video" + } +} diff --git a/websites/C/Chess/metadata.json b/websites/C/Chess.com/metadata.json similarity index 55% rename from websites/C/Chess/metadata.json rename to websites/C/Chess.com/metadata.json index 16dd10689ca4..af69e0045e43 100644 --- a/websites/C/Chess/metadata.json +++ b/websites/C/Chess.com/metadata.json @@ -5,7 +5,13 @@ "name": "Funeoz", "id": "256093420206948352" }, - "service": "Chess", + "contributors": [ + { + "name": "Linkredible", + "id": "188370726279970816" + } + ], + "service": "Chess.com", "description": { "en": "Chess.com is the main internet chess server. It is also an Internet forum and a social network dedicated to chess.", "es": "Chess.com es el principal servidor web para el ajedrez. Es también un foro web y un red social dedicado al ajedrez.", @@ -15,12 +21,52 @@ }, "url": "www.chess.com", "regExp": "^https?[:][/][/](www[.])?chess[.]com[/]", - "version": "1.3.0", + "version": "1.0.0", "logo": "https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/logo.png", "thumbnail": "https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/thumbnail.png", "color": "#4d7737", "category": "games", "tags": [ "games" + ], + "settings": [ + { + "id": "lang", + "multiLanguage": true + }, + { + "id": "privacyMode", + "title": "Privacy Mode", + "icon": "fas fa-user-secret", + "value": false + }, + { + "id": "hideButtons", + "title": "Hide Buttons", + "icon": "fas fa-eye-slash", + "value": false, + "if": { + "privacyMode": false + } + }, + { + "id": "displayFormat", + "title": "Display Format", + "icon": "fas fa-paragraph", + "value": 0, + "values": ["Vs Opponent", "Player vs Opponent", "Player Only"], + "if": { + "privacyMode": false + } + }, + { + "id": "hideRating", + "title": "Hide Rating", + "icon": "fas fa-eye-slash", + "value": false, + "if": { + "privacyMode": false + } + } ] } diff --git a/websites/C/Chess.com/presence.ts b/websites/C/Chess.com/presence.ts new file mode 100644 index 000000000000..9dfc636da0d6 --- /dev/null +++ b/websites/C/Chess.com/presence.ts @@ -0,0 +1,225 @@ +import type { AppStrings, Resolver } from './util/interfaces.js' +import { ActivityType } from 'premid' +import analysisResolver from './sources/analysis.js' + +import classroomResolver from './sources/classroom.js' +import computerResolver from './sources/computer.js' +import friendsResolver from './sources/friends.js' +import gameResolver from './sources/game.js' +import insightsResolver from './sources/insights.js' +import learnResolver from './sources/learn.js' +import memberResolver from './sources/member.js' +import practiceResolver from './sources/practice.js' +import puzzleResolver from './sources/puzzle.js' +import tvResolver from './sources/tv.js' +import variantsResolver from './sources/variants.js' +import videoResolver from './sources/video.js' +import { ActivityAssets, presence } from './util/index.js' + +const resolvers: Resolver[] = [ + tvResolver, + analysisResolver, + insightsResolver, + gameResolver, + classroomResolver, + memberResolver, + friendsResolver, + learnResolver, + practiceResolver, + variantsResolver, + videoResolver, + computerResolver, + puzzleResolver, +] + +presence.on('UpdateData', async () => { + const pathname = document.location.pathname + const doc = document + + const strings: AppStrings = await presence.getStrings({ + play: 'chess.com.play', + pause: 'chess.com.pause', + browsing: 'chess.com.browsing', + menu: 'chess.com.menu', + common_menu: 'chess.com.menu', + overview: 'chess.com.overview', + library: 'chess.com.library', + vs_separator: 'chess.com.vs_separator', + min: 'chess.com.min', + survival: 'chess.com.survival', + home: 'chess.com.home', + + // Computer + computer_vs: 'chess.com.computerVs', + computer_selecting: 'chess.com.computerSelecting', + computer_ai: 'chess.com.computerAi', + computer_name: 'chess.com.computerName', + playing_as_white: 'chess.com.computerWhite', + playing_as_black: 'chess.com.computerBlack', + + // Puzzles + puzzle_solving: 'chess.com.puzzleSolving', + puzzle_rush: 'chess.com.puzzleRush', + puzzle_battle: 'chess.com.puzzleBattle', + puzzle_tactics: 'chess.com.puzzleTactics', + score: 'chess.com.puzzleScore', + level: 'chess.com.puzzleLevel', + rating: 'chess.com.puzzleRating', + + // Play + play_online: 'chess.com.playOnline', + play_daily: 'chess.com.playDaily', + play_match: 'chess.com.playMatch', + play_live: 'chess.com.playLive', + play_lobby: 'chess.com.playLobby', + game_online: 'chess.com.gameOnline', + searching: 'chess.com.gameSearching', + game_finished: 'chess.com.gameFinished', + game_over: 'chess.com.gameOver', + game_reviewing: 'chess.com.gameReviewing', + watching_replay: 'chess.com.gameReplay', + spectating: 'chess.com.gameSpectating', + waiting: 'chess.com.gameWaiting', + match_in_progress: 'chess.com.gameProgress', + archive: 'chess.com.gameArchive', + game_review: 'chess.com.gameReview', + + // Media + media_analysis: 'chess.com.mediaAnalysis', + media_learning: 'chess.com.mediaLearning', + media_lessons: 'chess.com.mediaLessons', + media_tv: 'chess.com.mediaTv', + media_video: 'chess.com.mediaVideo', + media_finished: 'chess.com.mediaFinished', + tv_checking: 'chess.com.tvChecking', + video_browsing: 'chess.com.videoBrowsing', + video_watching: 'chess.com.videoWatching', + video_library: 'chess.com.videoLibrary', + + // Social + friends_list: 'chess.com.friendsList', + friends_single: 'chess.com.friendsSingle', + friends_plural: 'chess.com.friendsPlural', + profile_viewing: 'chess.com.profileViewing', + viewing_profile: 'chess.com.profileViewing', + profile_general_alt: 'chess.com.profileGeneralAlt', + profile: 'chess.com.profileGeneral', + + // UI + button_view_game: 'chess.com.buttonViewGame', + button_watch_video: 'chess.com.buttonWatchVideo', + variants_menu: 'chess.com.variantsMenu', + + // Classroom + classroom_title: 'chess.com.classroomTitle', + classroom_session: 'chess.com.classroomSession', + + // Practice + practice_title: 'chess.com.practiceTitle', + + // Learn + learn_openings: 'chess.com.learnOpenings', + learn_all_lessons: 'chess.com.learnAllLessons', + + // Insights + insights_title: 'chess.com.insightsTitle', + insights_stats: 'chess.com.insightsStats', + }) + + const [isPrivacyMode, hideButtons, displayFormat, hideRating, lang] = await Promise.all([ + presence.getSetting('privacyMode'), + presence.getSetting('hideButtons'), + presence.getSetting('displayFormat'), + presence.getSetting('hideRating'), + presence.getSetting('lang'), + ]) + + const presenceData: PresenceData = { + largeImageKey: ActivityAssets.Logo, + details: strings.browsing, + type: ActivityType.Playing, + } + + const activeResolver = resolvers.find(r => r.isActive(pathname)) + + if (activeResolver) { + if (activeResolver.getDetails) { + const details = activeResolver.getDetails(strings, doc, lang) + if (details) + presenceData.details = details + } + + if (activeResolver.getState) { + const state = activeResolver.getState(strings, doc, displayFormat, hideRating) + if (state) + presenceData.state = state + } + + if (activeResolver.getType) { + const type = activeResolver.getType(strings, doc) + if (type !== undefined) + presenceData.type = type + } + + if (activeResolver.getLargeImageKey) { + const largeImage = activeResolver.getLargeImageKey(strings, doc) + if (largeImage) + presenceData.largeImageKey = largeImage + } + + if (activeResolver.getSmallImageKey) { + presenceData.smallImageKey = activeResolver.getSmallImageKey(strings, doc) + } + + if (activeResolver.getSmallImageText) { + presenceData.smallImageText = activeResolver.getSmallImageText(strings, doc) + } + + if (!isPrivacyMode && !hideButtons && activeResolver.getButtons) { + const buttons = activeResolver.getButtons(strings, doc) + if (buttons) + presenceData.buttons = buttons + } + + if (isPrivacyMode) { + delete presenceData.state + + if (activeResolver === computerResolver + || activeResolver === gameResolver + || activeResolver === variantsResolver) { + presenceData.details = strings.play + } + else if (activeResolver === analysisResolver) { + presenceData.details = strings.media_analysis + } + else if (activeResolver === puzzleResolver) { + presenceData.details = strings.puzzle_solving + } + } + + if (activeResolver.getTimestamps) { + const times = activeResolver.getTimestamps(strings, doc) + if (times) { + presenceData.startTimestamp = times.start + presenceData.endTimestamp = times.end + } + else if ( + activeResolver === videoResolver + && activeResolver.getType + && activeResolver.getType(strings, doc) === ActivityType.Watching + ) { + delete presenceData.startTimestamp + if (!presenceData.smallImageText) { + presenceData.smallImageText = strings.pause + } + } + } + } + + if (presenceData.details) { + presence.setActivity(presenceData) + } + else { + presence.clearActivity() + } +}) diff --git a/websites/C/Chess.com/sources/analysis.ts b/websites/C/Chess.com/sources/analysis.ts new file mode 100644 index 000000000000..92f6dbe47aeb --- /dev/null +++ b/websites/C/Chess.com/sources/analysis.ts @@ -0,0 +1,63 @@ +import type { ButtonTuple, Resolver } from '../util/interfaces.js' +import { ActivityType } from 'premid' +import { ActivityAssets, formatMatch, getPlayerData, getText } from '../util/index.js' + +const analysisResolver: Resolver = { + isActive: pathname => pathname.includes('/analysis') || pathname.includes('/review'), + + getDetails: (t, doc) => { + if (doc.location.pathname.includes('/review')) { + return t.game_review + } + return t.media_analysis + }, + + getState: (t, doc, displayFormat, hideRating) => { + const topNode = doc.querySelector('#player-top') + const bottomNode = doc.querySelector('#player-bottom') + + const top = getPlayerData(topNode as ParentNode) + const bottom = getPlayerData(bottomNode as ParentNode) + + if (bottom.name || top.name) { + const state = formatMatch(top, bottom, displayFormat, hideRating, t.vs_separator) + if (state) + return state + } + + if (doc.location.pathname.includes('/review')) { + const coachMsg = getText(['[data-cy="bot-speech-content-message"]', '.bot-speech-content-content-container']) + if (coachMsg) { + return coachMsg.length > 128 ? `${coachMsg.substring(0, 125)}...` : coachMsg + } + } + + const whiteStandard = getText(['.board-player-default-white .user-username-component']) + const blackStandard = getText(['.board-player-default-black .user-username-component']) + + if (whiteStandard && blackStandard) { + return `${whiteStandard} ${t.vs_separator} ${blackStandard}` + } + + return undefined + }, + + getType: () => ActivityType.Watching, + getButtons: (t, doc) => { + const href = doc.location?.href + if (!href) + return undefined + const cleanUrl = href.split('?')[0] || href + return [{ label: t.button_view_game, url: cleanUrl }] as ButtonTuple + }, + + getLargeImageKey: () => ActivityAssets.Logo, + getSmallImageKey: () => ActivityAssets.Analysis, + getSmallImageText: (t, doc) => { + if (doc.location.pathname.includes('/review')) + return t.game_review + return t.media_analysis + }, +} + +export default analysisResolver diff --git a/websites/C/Chess.com/sources/classroom.ts b/websites/C/Chess.com/sources/classroom.ts new file mode 100644 index 000000000000..2e60a46f01c5 --- /dev/null +++ b/websites/C/Chess.com/sources/classroom.ts @@ -0,0 +1,13 @@ +import type { Resolver } from '../util/interfaces.js' +import { ActivityAssets } from '../util/index.js' + +const classroomResolver: Resolver = { + isActive: pathname => pathname.includes('/classroom'), + getDetails: t => t.classroom_title, + getState: t => t.classroom_session, + getLargeImageKey: () => ActivityAssets.Logo, + getSmallImageKey: () => ActivityAssets.Lessons, + getSmallImageText: t => t.classroom_title, +} + +export default classroomResolver diff --git a/websites/C/Chess.com/sources/computer.ts b/websites/C/Chess.com/sources/computer.ts new file mode 100644 index 000000000000..3030f338e8c1 --- /dev/null +++ b/websites/C/Chess.com/sources/computer.ts @@ -0,0 +1,46 @@ +import type { Resolver } from '../util/interfaces.js' +import { ActivityAssets, cleanRating } from '../util/index.js' + +const computerResolver: Resolver = { + isActive: pathname => pathname.includes('/play/computer') || pathname.includes('/play/coach'), + + getDetails: t => t.computer_vs, + + getState: (t, doc, _, hideRating) => { + const nameEl = doc.querySelector('[data-test-element="user-tagline-username"]') + const name = nameEl?.textContent?.trim() + + if (!name) + return t.computer_selecting + + const ratingEl = doc.querySelector('[data-cy="user-tagline-rating"]') + const rating = ratingEl?.textContent ? cleanRating(ratingEl.textContent) : null + + return (rating && !hideRating) ? `${name} (${rating})` : name + }, + + getLargeImageKey: (t, doc) => { + const img = doc.querySelector('img[data-cy="avatar"]') + + if (img && img.src && !img.src.includes('svg') && !img.src.includes('transparent')) { + return img.src + } + + return ActivityAssets.Logo + }, + + getSmallImageKey: (t, doc) => { + const board = doc.querySelector('chess-board') || doc.querySelector('.board') + const isFlipped = board?.classList.contains('flipped') + + return isFlipped ? ActivityAssets.BlackKing : ActivityAssets.WhiteKing + }, + + getSmallImageText: (t, doc) => { + const board = doc.querySelector('chess-board') || doc.querySelector('.board') + const isFlipped = board?.classList.contains('flipped') + return isFlipped ? t.playing_as_black : t.playing_as_white + }, +} + +export default computerResolver diff --git a/websites/C/Chess.com/sources/friends.ts b/websites/C/Chess.com/sources/friends.ts new file mode 100644 index 000000000000..f6c6b63291a2 --- /dev/null +++ b/websites/C/Chess.com/sources/friends.ts @@ -0,0 +1,28 @@ +import type { Resolver } from '../util/interfaces.js' +import { ActivityType } from 'premid' +import { ActivityAssets, getText } from '../util/index.js' + +const friendsResolver: Resolver = { + isActive: pathname => pathname === '/friends', + + getDetails: t => t.friends_list, + + getState: (t, _doc) => { + const count = getText(['.friends-section-count']) + + if (count) { + const num = Number.parseInt(count.replace(/\D/g, '')) + return num === 1 ? `${count} ${t.friends_single}` : `${count} ${t.friends_plural}` + } + + return t.overview + }, + + getType: () => ActivityType.Watching, + + getLargeImageKey: () => ActivityAssets.Logo, + + getSmallImageText: t => t.friends_list, +} + +export default friendsResolver diff --git a/websites/C/Chess.com/sources/game.ts b/websites/C/Chess.com/sources/game.ts new file mode 100644 index 000000000000..0c2da497fa6c --- /dev/null +++ b/websites/C/Chess.com/sources/game.ts @@ -0,0 +1,181 @@ +import type { Resolver } from '../util/interfaces.js' +import { ActivityType } from 'premid' +import { ActivityAssets, formatMatch, getGameMode, getPlayerData, hasPlayerControls } from '../util/index.js' + +enum GameStatus { + Searching, + Playing, + Spectating, + Finished, + Archive, + Lobby, + Daily, + Loading, +} + +function getGameStatus(doc: Document): GameStatus { + const path = doc.location.pathname + + if (path.includes('/games/view/')) + return GameStatus.Archive + if (path.includes('/play/online/watch')) + return GameStatus.Spectating + if (path.includes('/daily')) + return GameStatus.Daily + + const hasControls = hasPlayerControls(doc) + const hasClock = !!doc.querySelector('.clock-component, [data-cy="clock-time"]') + + const topNode = doc.querySelector('.player-component.player-top, .player-row-component') + const topName = topNode?.querySelector('[data-test-element="user-tagline-username"], .user-username-component') + + if (path.includes('/play/online') || path.includes('/live')) { + if (!hasControls && !hasClock && !topNode) + return GameStatus.Lobby + } + + if (hasControls) + return GameStatus.Playing + + if (doc.querySelector('.game-over-message-component, .game-result-component')) + return GameStatus.Finished + + if (topName || hasClock) + return GameStatus.Spectating + if (topNode && !topName) + return GameStatus.Searching + + return GameStatus.Loading +} + +const gameResolver: Resolver = { + isActive: (pathname) => { + const isGame = pathname.includes('/game/') || pathname.includes('/games/') + const isPlay = pathname.includes('/play/online') || pathname.includes('/live') || pathname.includes('/daily') + const isWatch = pathname.includes('/watch') + + return (isGame || isPlay || isWatch) && !pathname.includes('/variants') + }, + + getType: (t, doc) => { + const status = getGameStatus(doc) + if (status === GameStatus.Playing || status === GameStatus.Daily || status === GameStatus.Lobby) { + return ActivityType.Playing + } + return ActivityType.Watching + }, + + getDetails: (t, doc) => { + const status = getGameStatus(doc) + const mode = getGameMode(doc) || t.game_online + + switch (status) { + case GameStatus.Archive: + return t.archive + case GameStatus.Lobby: + return t.play_lobby + case GameStatus.Searching: + return `${t.searching} ${mode}...` + case GameStatus.Playing: + case GameStatus.Daily: + return `${t.play} ${mode}` + case GameStatus.Finished: + { + const gameOverEl = doc.querySelector('.game-over-message-component strong, .game-result-component') + return gameOverEl?.textContent?.trim() || t.game_finished + } + case GameStatus.Spectating: + if (doc.location.pathname.includes('/game/live/')) + return t.watching_replay + return `${t.spectating} ${mode}` + default: + return t.home + } + }, + + getState: (t, doc, displayFormat, hideRating) => { + const status = getGameStatus(doc) + + if (status === GameStatus.Searching || status === GameStatus.Loading) { + return t.waiting + } + + if (status === GameStatus.Lobby) { + return undefined + } + + const topNode = doc.querySelector('.player-component.player-top, .player-row-container') + const bottomNode = doc.querySelector('.player-component.player-bottom') + + const top = getPlayerData(topNode) + const bottom = getPlayerData(bottomNode) + + if (status === GameStatus.Archive) { + if (bottom.name && top.name) { + return `${bottom.name} ${t.vs_separator} ${top.name}` + } + return document.title.replace(' - Chess.com', '').trim() + } + + if (top.name && bottom.name) { + const state = formatMatch(top, bottom, displayFormat, hideRating, t.vs_separator) + if (state) + return state + + const p1 = bottom.rating ? `${bottom.name} (${bottom.rating})` : bottom.name + const p2 = top.rating ? `${top.name} (${top.rating})` : top.name + return `${p1} ${t.vs_separator} ${p2}` + } + + if (status === GameStatus.Playing && doc.querySelector('.clock-component')) { + return t.match_in_progress + } + + return 'Chess.com' + }, + + getButtons: (t, doc) => { + const status = getGameStatus(doc) + if (status !== GameStatus.Searching && status !== GameStatus.Loading && status !== GameStatus.Lobby) { + const currentUrl = doc.location.href + const cleanUrl = currentUrl.split('?')[0] || currentUrl + return [{ label: t.button_view_game, url: cleanUrl }] + } + return undefined + }, + + getLargeImageKey: () => ActivityAssets.Logo, + + getSmallImageKey: (t, doc) => { + const mode = getGameMode(doc)?.toLowerCase() + + if (mode === 'bullet') + return ActivityAssets.Bullet + if (mode === 'blitz') + return ActivityAssets.Blitz + if (mode === 'rapid') + return ActivityAssets.Rapid + if (mode === 'daily') + return ActivityAssets.Daily + + return ActivityAssets.Logo + }, + + getSmallImageText: (t, doc) => { + const status = getGameStatus(doc) + const mode = getGameMode(doc) || t.play_live + + switch (status) { + case GameStatus.Playing: + case GameStatus.Daily: + return `${t.play} ${mode}` + case GameStatus.Spectating: return `${t.spectating} ${mode}` + case GameStatus.Archive: return t.archive + case GameStatus.Finished: return t.media_finished + case GameStatus.Searching: return t.searching + default: return t.home + } + }, +} + +export default gameResolver diff --git a/websites/C/Chess.com/sources/insights.ts b/websites/C/Chess.com/sources/insights.ts new file mode 100644 index 000000000000..8b79e375ef06 --- /dev/null +++ b/websites/C/Chess.com/sources/insights.ts @@ -0,0 +1,29 @@ +import type { Resolver } from '../util/interfaces.js' +import { ActivityAssets } from '../util/index.js' + +const insightsResolver: Resolver = { + isActive: pathname => pathname.includes('/insights'), + + getDetails: (t, doc) => { + const title = doc.querySelector('.cc-page-header-title')?.textContent?.trim() + return title || t.insights_title + }, + + getState: (t, doc) => { + const parts = doc.location.pathname.split('/') + const insightsIndex = parts.indexOf('insights') + const username = (insightsIndex !== -1 && parts[insightsIndex + 1]) ? parts[insightsIndex + 1] : null + + if (username) { + const formattedUser = username.charAt(0).toUpperCase() + username.slice(1) + return `${t.insights_stats}: ${formattedUser}` + } + return t.overview + }, + + getLargeImageKey: () => ActivityAssets.Logo, + getSmallImageKey: () => ActivityAssets.Statistics, + getSmallImageText: t => t.insights_title, +} + +export default insightsResolver diff --git a/websites/C/Chess.com/sources/learn.ts b/websites/C/Chess.com/sources/learn.ts new file mode 100644 index 000000000000..1f7934a5bb77 --- /dev/null +++ b/websites/C/Chess.com/sources/learn.ts @@ -0,0 +1,48 @@ +import type { Resolver } from '../util/interfaces.js' +import { ActivityAssets } from '../util/index.js' + +const learnResolver: Resolver = { + isActive: pathname => pathname.includes('/learn') || pathname.includes('/lessons') || pathname.includes('/courses'), + + getDetails: t => t.media_learning, + + getState: (t, doc) => { + const path = doc.location.pathname + + if (path === '/learn' || path === '/lessons' || path === '/courses') { + return t.menu + } + if (path.includes('/learn-the-openings')) + return t.learn_openings + if (path.includes('/all-lessons')) + return t.learn_all_lessons + + const seed = doc.getElementById('lesson-data-seed') + if (seed && seed.dataset.initialLesson) { + try { + const data = JSON.parse(seed.dataset.initialLesson) + if (data.course && data.course.title) { + return data.course.title + } + if (data.title) + return data.title + } + catch {} + } + + const ogTitle = doc.querySelector('meta[property="og:title"]') + if (ogTitle) { + const content = ogTitle.getAttribute('content') + if (content && !content.includes('Chess.com')) + return content + } + + return t.browsing + }, + + getLargeImageKey: () => ActivityAssets.Logo, + getSmallImageKey: () => ActivityAssets.Lessons, + getSmallImageText: t => t.media_lessons, +} + +export default learnResolver diff --git a/websites/C/Chess.com/sources/member.ts b/websites/C/Chess.com/sources/member.ts new file mode 100644 index 000000000000..ed31d6c3a33b --- /dev/null +++ b/websites/C/Chess.com/sources/member.ts @@ -0,0 +1,46 @@ +import type { Resolver } from '../util/interfaces.js' +import { ActivityAssets, getText } from '../util/index.js' + +const memberResolver: Resolver = { + isActive: pathname => pathname.includes('/member/'), + + getDetails: t => t.profile_viewing, + + getState: (t, doc) => { + const username = getText(['.profile-card-username']) + const realName = getText(['.profile-card-name']) + + if (username && realName) { + return `${username} (${realName})` + } + + if (username) + return username + + const parts = doc.location.pathname.split('/') + const memberIndex = parts.indexOf('member') + const urlName = parts[memberIndex + 1] + + if (memberIndex !== -1 && urlName) { + return urlName.charAt(0).toUpperCase() + urlName.slice(1) + } + + return t.profile_general_alt + }, + + getLargeImageKey: (_t, doc) => { + const avatarImg = doc.querySelector('.profile-header-avatar .cc-avatar-img') + + if (avatarImg && avatarImg.src) { + return avatarImg.src + } + + return ActivityAssets.Logo + }, + + getSmallImageKey: () => ActivityAssets.Logo, + + getSmallImageText: t => t.profile_general_alt, +} + +export default memberResolver diff --git a/websites/C/Chess.com/sources/practice.ts b/websites/C/Chess.com/sources/practice.ts new file mode 100644 index 000000000000..5b9f354b01e1 --- /dev/null +++ b/websites/C/Chess.com/sources/practice.ts @@ -0,0 +1,44 @@ +import type { Resolver } from '../util/interfaces.js' +import { ActivityAssets, getText } from '../util/index.js' + +const practiceResolver: Resolver = { + isActive: pathname => pathname.includes('/practice'), + + getDetails: (t, doc) => { + const activeTab = getText([ + '.game-details-tabs .cc-tab-item-active .cc-tab-item-label', + '.game-details-tabs button[aria-selected="true"]', + ], doc) + + if (activeTab) + return activeTab + + return t.practice_title + }, + + getState: (t, doc) => { + if (doc.location.pathname === '/practice') + return t.menu + + const mainTitle = getText(['h1.main-heading-title'], doc) + if (mainTitle) + return mainTitle + + const secondary = getText(['header.secondary-header-title', '.secondary-header-title'], doc) + + if (secondary) { + if (secondary.includes(':')) { + return secondary.split(':')[1]?.trim() + } + return secondary + } + + return undefined + }, + + getLargeImageKey: () => ActivityAssets.Logo, + getSmallImageKey: () => ActivityAssets.Lessons, + getSmallImageText: t => t.practice_title, +} + +export default practiceResolver diff --git a/websites/C/Chess.com/sources/puzzle.ts b/websites/C/Chess.com/sources/puzzle.ts new file mode 100644 index 000000000000..17d02f830c76 --- /dev/null +++ b/websites/C/Chess.com/sources/puzzle.ts @@ -0,0 +1,96 @@ +import type { Resolver } from '../util/interfaces.js' +import { ActivityAssets, getText } from '../util/index.js' + +const puzzleResolver: Resolver = { + isActive: pathname => pathname.includes('/puzzles'), + + getDetails: (t, doc, lang) => { + if (doc.location.pathname.includes('/rush')) { + if (doc.querySelector('[data-cy="startSession"]')) { + return t.puzzle_rush + } + + const icon3Min = doc.querySelector('svg[data-glyph="game-time-blitz"]') + const icon5Min = doc.querySelector('svg[data-glyph="game-time-rapid"]') + + const getDuration = (minutes: number) => { + const IntlAny = Intl as any + if (lang && IntlAny.DurationFormat) { + return new IntlAny.DurationFormat(lang, { style: 'narrow' }).format({ minutes }) + } + return `${minutes} ${t.min}` + } + + if (icon3Min) + return `${t.puzzle_rush} (${getDuration(3)})` + if (icon5Min) + return `${t.puzzle_rush} (${getDuration(5)})` + return `${t.puzzle_rush} (${t.survival})` + } + + if (doc.location.pathname.includes('/battle')) { + return t.puzzle_battle + } + + return t.puzzle_solving + }, + + getState: (t, doc) => { + if (doc.location.pathname.includes('/rush')) { + if (doc.querySelector('[data-cy="startSession"]')) + return t.common_menu + const score = getText(['[data-cy="solved-count"]']) + return score ? `${t.score}: ${score}` : `${t.score}: 0` + } + + if (doc.location.pathname.includes('/battle')) { + const scores = doc.querySelectorAll('.battle-player-details-playing-score') + + const myScoreEl = scores[0] + const opponentScoreEl = scores[1] + + if (myScoreEl && opponentScoreEl) { + const myScore = myScoreEl.textContent?.trim() || '0' + const opponentScore = opponentScoreEl.textContent?.trim() || '0' + return `${t.score}: ${myScore} - ${opponentScore}` + } + + return t.common_menu + } + + const rating = getText([ + '[data-cy="path-points"]', + '.puzzle-rating-component', + '.ui-label-item-value', + '.rating-score', + ]) + const level = getText(['.puzzle-tier-icon-level', '[data-cy^="level-"]']) + + if (rating) { + if (level) + return `${t.level} ${level} • ${rating}` + return `${t.rating}: ${rating}` + } + return undefined + }, + + getLargeImageKey: (t, doc) => { + if (doc.location.pathname.includes('/rush')) + return ActivityAssets.PuzzleRush + if (doc.location.pathname.includes('/battle')) + return ActivityAssets.PuzzleRush + + return ActivityAssets.Logo + }, + + getSmallImageKey: (t, doc) => { + if (doc.location.pathname.includes('/rush') || doc.location.pathname.includes('/battle')) { + return ActivityAssets.Logo + } + return ActivityAssets.Puzzle + }, + + getSmallImageText: t => t.puzzle_tactics, +} + +export default puzzleResolver diff --git a/websites/C/Chess.com/sources/tv.ts b/websites/C/Chess.com/sources/tv.ts new file mode 100644 index 000000000000..2aca23dd1d48 --- /dev/null +++ b/websites/C/Chess.com/sources/tv.ts @@ -0,0 +1,55 @@ +import type { Resolver } from '../util/interfaces.js' +import { ActivityType } from 'premid' +import { ActivityAssets } from '../util/index.js' + +const tvResolver: Resolver = { + isActive: pathname => pathname.includes('/tv'), + + getDetails: t => t.media_tv, + + getState: (t, doc) => { + let streamerName = null + + const playerDiv = doc.querySelector('#tv-player') + if (playerDiv) { + streamerName = playerDiv.getAttribute('data-channel') + } + + if (!streamerName) { + const containerDiv = doc.querySelector('#view-tv-index') + if (containerDiv) { + streamerName = containerDiv.getAttribute('data-live-video-show-title') + } + } + + if (!streamerName) { + const iframe = doc.querySelector('iframe.tv-player-iframe') + if (iframe && iframe.src) { + try { + const url = new URL(iframe.src) + if (url.hostname.includes('kick.com')) + streamerName = url.pathname.replace(/^\//, '') + else if (url.hostname.includes('twitch.tv')) + streamerName = url.searchParams.get('channel') + } + catch {} + } + } + + if (streamerName) { + return streamerName.charAt(0).toUpperCase() + streamerName.slice(1) + } + + return t.tv_checking + }, + + getType: () => ActivityType.Watching, + + getLargeImageKey: () => ActivityAssets.Logo, + + getSmallImageKey: () => ActivityAssets.TV, + + getSmallImageText: t => t.media_tv, +} + +export default tvResolver diff --git a/websites/C/Chess.com/sources/variants.ts b/websites/C/Chess.com/sources/variants.ts new file mode 100644 index 000000000000..22688534268b --- /dev/null +++ b/websites/C/Chess.com/sources/variants.ts @@ -0,0 +1,116 @@ +import type { Resolver } from '../util/interfaces.js' +import { ActivityType } from 'premid' +import { ActivityAssets } from '../util/index.js' + +const variantsResolver: Resolver = { + isActive: pathname => pathname.includes('/variants'), + + getDetails: (t, doc) => { + const path = doc.location.pathname + const parts = path.split('/').filter(p => p) + + const variantKey = parts.find(p => p !== 'variants' && p !== 'live' && p !== 'play' && p !== 'game') + + let variantName = t.variants_menu + + if (variantKey) { + const cleanName = variantKey + .split('-') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' ') + + variantName = `${t.variants_menu}: ${cleanName}` + } + + const isUrlAnalysis = parts.length > 4 && parts.includes('game') + if (isUrlAnalysis) { + return variantName + } + + const isGameOverHTML = !!doc.querySelector([ + '.game-over-modal', + '.game-result-component', + '.game-over-message-component', + '.game-rate-sport-message-component', + '.game-buttons-container-component', + 'button[data-cy="rematch-button"]', + ].join(',')) + + if (isGameOverHTML) { + return `${variantName} - ${t.media_finished}` + } + + return variantName + }, + + getState: (t, doc) => { + const parts = doc.location.pathname.split('/').filter(p => p) + if (parts.length > 4 && parts.includes('game')) { + return t.game_reviewing + } + + const resultEl = doc.querySelector('.game-over-modal-header, .game-result-component, .game-over-message-component strong') + if (resultEl) { + return resultEl.textContent?.trim() || t.game_over + } + + const isFinished = !!doc.querySelector([ + '.game-buttons-container-component', + '.game-rate-sport-message-component', + 'button[data-cy="rematch-button"]', + ].join(',')) + + if (isFinished) { + return t.game_over + } + + if (doc.querySelector('.playerbox-clock, .clock-component')) { + return t.play + } + + return t.common_menu + }, + + getType: (t, doc) => { + const parts = doc.location.pathname.split('/').filter(p => p) + + if (parts.length > 4 && parts.includes('game')) { + return ActivityType.Watching + } + + const isGameOver = !!doc.querySelector([ + '.game-over-modal', + '.game-result-component', + '.game-over-message-component', + '.game-rate-sport-message-component', + '.game-buttons-container-component', + 'button[data-cy="rematch-button"]', + ].join(',')) + + const hasClock = !!doc.querySelector('.playerbox-clock, .clock-component') + + if (hasClock && !isGameOver) + return ActivityType.Playing + + return ActivityType.Watching + }, + + getButtons: (t, doc) => { + if (doc.location.pathname.includes('/game/')) { + const parts = doc.location.href.split('/') + const gameIndex = parts.indexOf('game') + + if (gameIndex !== -1 && parts[gameIndex + 1]) { + const cleanUrl = parts.slice(0, gameIndex + 2).join('/') + return [{ label: t.button_view_game, url: cleanUrl }] + } + } + return undefined + }, + + getLargeImageKey: () => ActivityAssets.Variants, + getSmallImageKey: () => ActivityAssets.Logo, + getSmallImageText: t => t.variants_menu, +} + +export default variantsResolver diff --git a/websites/C/Chess.com/sources/video.ts b/websites/C/Chess.com/sources/video.ts new file mode 100644 index 000000000000..dd91369599f4 --- /dev/null +++ b/websites/C/Chess.com/sources/video.ts @@ -0,0 +1,108 @@ +import type { Resolver } from '../util/interfaces.js' +import { ActivityType } from 'premid' +import { ActivityAssets, getText } from '../util/index.js' + +function getMainVideo(doc: Document): HTMLVideoElement | null { + const playerContainer = doc.querySelector('.video-player-player') + + if (playerContainer) { + return playerContainer.querySelector('video') + } + return null +} + +function isPlaying(doc: Document, video: HTMLVideoElement): boolean { + if (video.seeking) + return false + + const pauseIcon = doc.querySelector('.video-player-controls .icon-font-chess.pause') + if (pauseIcon) + return true + + return !video.paused +} + +const videoResolver: Resolver = { + isActive: pathname => pathname.includes('/video'), + + getDetails: (t, doc) => doc.location.pathname.includes('/player/') ? t.media_video : t.video_browsing, + + getState: (t, doc) => { + if (doc.location.pathname.includes('/player/')) { + const ogTitle = doc.querySelector('meta[property="og:title"]') + if (ogTitle) { + const content = ogTitle.getAttribute('content') + if (content) + return content.replace(' - Chess.com', '').trim() + } + return getText(['h1']) || t.video_watching + } + return t.video_library + }, + + getTimestamps: (t, doc) => { + if (doc.location.pathname.includes('/player/')) { + const video = getMainVideo(doc) + + if (!video || !Number.isFinite(video.duration)) + return undefined + + if (isPlaying(doc, video) && video.currentTime > 0) { + const now = Date.now() + const startTimeMs = now - (video.currentTime * 1000) + const endTimeMs = startTimeMs + (video.duration * 1000) + + return { + start: Math.floor(startTimeMs / 1000), + end: Math.floor(endTimeMs / 1000), + } + } + } + return undefined + }, + + getType: (t, doc) => doc.location.pathname.includes('/player/') ? ActivityType.Watching : undefined, + + getButtons: (t, doc) => { + if (doc.location.pathname.includes('/player/')) { + return [{ label: t.button_watch_video, url: doc.location.href }] + } + return undefined + }, + + getLargeImageKey: (t, doc) => { + if (doc.location.pathname.includes('/player/')) { + const ogImage = doc.querySelector('meta[property="og:image"]') + if (ogImage) { + return ogImage.getAttribute('content') || ActivityAssets.Logo + } + } + return ActivityAssets.Logo + }, + + getSmallImageKey: (t, doc) => { + if (doc.location.pathname.includes('/player/')) { + const video = getMainVideo(doc) + + if (video && !isPlaying(doc, video)) { + return ActivityAssets.IconPause + } + + return ActivityAssets.IconPlay + } + return undefined + }, + + getSmallImageText: (t, doc) => { + if (doc.location.pathname.includes('/player/')) { + const video = getMainVideo(doc) + + if (video && !isPlaying(doc, video)) + return t.pause + return t.play + } + return undefined + }, +} + +export default videoResolver diff --git a/websites/C/Chess.com/util/index.ts b/websites/C/Chess.com/util/index.ts new file mode 100644 index 000000000000..654ab1ba9d2b --- /dev/null +++ b/websites/C/Chess.com/util/index.ts @@ -0,0 +1,117 @@ +export const presence = new Presence({ + clientId: '699204548664885279', +}) + +export enum ActivityAssets { + Logo = 'https://i.imgur.com/gvzQPV7.png', + Statistics = 'https://i.imgur.com/Ea3sTPD.png', + GamesArchive = 'https://i.imgur.com/3MhMFdF.png', + Daily = 'https://i.imgur.com/O9GkttW.png', + Computer = 'https://i.imgur.com/v4dGVx2.png', + FourPC = 'https://i.imgur.com/QuY5QRU.png', + Variants = 'https://i.imgur.com/On8TNaJ.png', + Puzzle = 'https://i.imgur.com/mRKagoX.png', + PuzzleRush = 'https://i.imgur.com/A931dHG.png', + Analysis = 'https://i.imgur.com/HeDh1BE.png', + Lessons = 'https://i.imgur.com/4V7r0tB.png', + TV = 'https://i.imgur.com/Xq5IOrC.png', + Bullet = 'https://i.imgur.com/7Lk1sdL.png', + Blitz = 'https://i.imgur.com/BGIwR1E.png', + Rapid = 'https://i.imgur.com/GKFT3rk.png', + IconPlay = 'https://i.imgur.com/DYQRYll.png', + IconPause = 'https://i.imgur.com/FsGA414.png', + WhiteKing = 'https://i.imgur.com/ZP7zJTy.png', + BlackKing = 'https://i.imgur.com/C8AzwmP.png', +} + +export interface PlayerData { + name: string | null + rating: string | null +} + +export function getText(selectors: string[], parent: ParentNode = document): string | null { + for (const selector of selectors) { + const element = parent.querySelector(selector) + if (element && element.textContent) { + return element.textContent.trim() + } + } + return null +} + +export function cleanRating(text: string | null): string { + if (!text) + return '' + return text.replace(/[()]/g, '').trim() +} + +export function getPlayerData(container: ParentNode | null): PlayerData { + if (!container) + return { name: null, rating: null } + + const name = getText([ + '[data-test-element="user-tagline-username"]', + '.user-username-component', + '.cc-user-username-white', + ], container) + + const ratingRaw = getText([ + '[data-cy="user-tagline-rating"]', + '.user-tagline-rating', + '.cc-user-rating-white', + ], container) + + return { name, rating: ratingRaw ? cleanRating(ratingRaw) : null } +} + +export function formatMatch(top: PlayerData, bottom: PlayerData, format: number = 0, hideRating: boolean = false, vsString: string = 'vs'): string | undefined { + const formatPlayer = (p: PlayerData) => { + const name = p.name || undefined + if (!name) + return undefined + return (p.rating && !hideRating) ? `${name} (${p.rating})` : name + } + + if (!top.name && !bottom.name) + return undefined + + if (format === 2) { + return bottom.name ? formatPlayer(bottom) : undefined + } + + if (format === 1) { + if (top.name && bottom.name) { + return `${formatPlayer(bottom)} ${vsString} ${formatPlayer(top)}` + } + } + + if (top.name) { + return `${vsString.charAt(0).toUpperCase() + vsString.slice(1)} ${formatPlayer(top)}` + } + + if (bottom.name) { + return formatPlayer(bottom) + } + + return undefined +} + +export function hasPlayerControls(doc: Document): boolean { + return !!doc.querySelector([ + '.resign-button-component', + '[data-cy="resign-button"]', + '.draw-button-component', + 'button[data-cy="abort-button"]', + '.abort-button-component', + '.daily-game-footer-component', + ].join(',')) +} + +export function getGameMode(doc: Document): string | null { + const modeEl = doc.querySelector('.player-component [rating-type], .cc-user-block-component[rating-type], [rating-type]') + if (!modeEl) + return null + + const raw = modeEl.getAttribute('rating-type') + return raw ? raw.charAt(0).toUpperCase() + raw.slice(1) : null +} diff --git a/websites/C/Chess.com/util/interfaces.ts b/websites/C/Chess.com/util/interfaces.ts new file mode 100644 index 000000000000..7fcd115ceb34 --- /dev/null +++ b/websites/C/Chess.com/util/interfaces.ts @@ -0,0 +1,111 @@ +export interface AppStrings { + // General + play: string + pause: string + browsing: string + menu: string + common_menu: string + overview: string + library: string + vs_separator: string + min: string + survival: string + + // Chess specific + home: string + + // Computer + computer_vs: string + computer_selecting: string + computer_ai: string + computer_name: string + playing_as_white: string + playing_as_black: string + + // Puzzles + puzzle_solving: string + puzzle_rush: string + puzzle_battle: string + puzzle_tactics: string + score: string + level: string + rating: string + + // Play / Game + play_online: string + play_daily: string + play_match: string + play_live: string + play_lobby: string + game_online: string + searching: string + game_finished: string + game_over: string + game_reviewing: string + watching_replay: string + spectating: string + waiting: string + match_in_progress: string + archive: string + + // Media + media_analysis: string + media_learning: string + media_lessons: string + media_tv: string + media_video: string + media_finished: string + tv_checking: string + video_browsing: string + video_watching: string + video_library: string + + // Social + friends_list: string + friends_single: string + friends_plural: string + profile: string + profile_general_alt: string + profile_viewing: string + viewing_profile: string + + // Buttons & Menus + button_view_game: string + button_watch_video: string + variants_menu: string + game_review: string + + // Classroom + classroom_title: string + classroom_session: string + + // Practice + practice_title: string + + // Learn + learn_openings: string + learn_all_lessons: string + + // Insights + insights_title: string + insights_stats: string +} + +export interface Button { + label: string + url: string +} + +export type ButtonTuple = [Button, (Button | undefined)?] + +export interface Resolver { + isActive: (pathname: string) => boolean + getDetails?: (t: AppStrings, doc: Document, lang?: string) => string | undefined + getState?: (t: AppStrings, doc: Document, displayFormat?: number, hideRating?: boolean) => string | undefined + getLargeImageKey?: (t: AppStrings, doc: Document) => string | undefined + getSmallImageKey?: (t: AppStrings, doc: Document) => string | undefined + getSmallImageText?: (t: AppStrings, doc: Document) => string | undefined + getType?: (t: AppStrings, doc: Document) => number | undefined + getButtons?: (t: AppStrings, doc: Document) => ButtonTuple | undefined + getTimestamps?: (t: AppStrings, doc: Document) => { start: number, end: number } | undefined +} diff --git a/websites/C/Chess/presence.ts b/websites/C/Chess/presence.ts deleted file mode 100644 index 87a2cd4b9117..000000000000 --- a/websites/C/Chess/presence.ts +++ /dev/null @@ -1,406 +0,0 @@ -import { Assets } from 'premid' - -const presence = new Presence({ - clientId: '699204548664885279', -}) -const strings = presence.getStrings({ - play: 'general.playing', - pause: 'general.paused', -}) -const browsingTimestamp = Math.floor(Date.now() / 1000) - -enum ActivityAssets { - Logo = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/logo.png', - Statistics = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/0.png', - GamesArchive = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/1.png', - Daily = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/2.png', - Computer = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/3.png', - FourPC = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/4.png', - Variants = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/5.png', - Fog = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/6.png', - Horde = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/7.png', - Koth = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/8.png', - Torpedo = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/9.png', - ThreeCheck = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/10.png', - Giveaway = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/11.png', - Sideways = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/12.png', - Chataranga = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/13.png', - Blindfold = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/14.png', - Nocastle = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/15.png', - Anything = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/16.png', - Atomic = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/17.png', - Automate = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/18.png', - Puzzle = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/19.png', - PuzzleRush = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/20.png', - PuzzleWar = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/21.png', - PuzzleOfDay = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/22.png', - SoloChess = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/23.png', - Drills = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/24.png', - Lessons = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/25.png', - Analysis = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/26.png', - Articles = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/27.png', - Vision = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/28.png', - Openings = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/29.png', - Explorer = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/30.png', - Forum = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/31.png', - Clubs = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/32.png', - Blog = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/33.png', - Members = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/34.png', - Coaches = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/35.png', - ChessToday = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/36.png', - News = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/37.png', - ChessTV = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/38.png', - MasterGames = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/39.png', -} - -presence.on('UpdateData', async () => { - const presenceData: PresenceData = { - largeImageKey: ActivityAssets.Logo, - startTimestamp: browsingTimestamp, - } - - if (document.location.pathname === '/home') { - presenceData.details = 'Viewing home page' - } - else if (document.location.pathname.includes('/messages')) { - presenceData.details = 'Viewing messages' - } - else if (document.location.pathname.includes('/stats')) { - presenceData.details = 'Viewing statistics' - presenceData.smallImageKey = ActivityAssets.Statistics - presenceData.smallImageText = 'Statistics' - } - else if (document.location.pathname.includes('/games/archive')) { - presenceData.details = 'Viewing games archive' - presenceData.smallImageKey = ActivityAssets.GamesArchive - presenceData.smallImageText = 'Games archive' - } - else if (document.location.pathname.includes('/live')) { - presenceData.details = 'Playing Live Chess' - presenceData.smallImageKey = Assets.Live - presenceData.smallImageText = 'Live' - } - else if (document.location.pathname.indexOf('/daily/') === 0) { - presenceData.details = 'Playing Daily Chess' - presenceData.smallImageKey = ActivityAssets.Daily - presenceData.smallImageText = 'Daily' - } - else if (document.location.pathname === '/daily') { - presenceData.details = 'Playing Daily Chess' - presenceData.smallImageKey = ActivityAssets.Daily - presenceData.smallImageText = 'Daily' - } - else if (document.location.pathname.includes('/play/computer')) { - presenceData.details = 'Playing against computer' - presenceData.smallImageKey = ActivityAssets.Computer - presenceData.smallImageText = 'Computer' - } - else if (document.location.pathname.includes('/tournaments')) { - presenceData.details = 'Viewing tournaments' - } - else if (document.location.pathname.includes('/4-player-chess')) { - presenceData.details = 'Playing 4 Player Chess' - presenceData.smallImageKey = ActivityAssets.FourPC - presenceData.smallImageText = '4 Player Chess' - } - else if (document.location.pathname === '/variants') { - presenceData.details = 'Browsing through Chess Variants' - presenceData.smallImageKey = ActivityAssets.Variants - presenceData.smallImageText = 'Variants' - } - else { - switch (0) { - case document.location.pathname.indexOf('/variants/fog-of-war/game/'): { - presenceData.details = 'Playing Fog of War' - presenceData.smallImageKey = ActivityAssets.Fog - presenceData.smallImageText = 'Fog of War' - - break - } - case document.location.pathname.indexOf('/variants/horde/game/'): { - presenceData.details = 'Playing Horde' - presenceData.smallImageKey = ActivityAssets.Horde - presenceData.smallImageText = 'Horde' - - break - } - case document.location.pathname.indexOf( - '/variants/king-of-the-hill/game/', - ): { - presenceData.details = 'Playing King of the Hill' - presenceData.smallImageKey = ActivityAssets.Koth - presenceData.smallImageText = 'King of the Hill' - - break - } - case document.location.pathname.indexOf('/variants/torpedo/game/'): { - presenceData.details = 'Playing Torpedo' - presenceData.smallImageKey = ActivityAssets.Torpedo - presenceData.smallImageText = 'Torpedo' - - break - } - case document.location.pathname.indexOf('/variants/3-check/game/'): { - presenceData.details = 'Playing 3 Check' - presenceData.smallImageKey = ActivityAssets.ThreeCheck - presenceData.smallImageText = '3 Check' - - break - } - case document.location.pathname.indexOf('/variants/giveaway/game/'): { - presenceData.details = 'Playing Giveaway' - presenceData.smallImageKey = ActivityAssets.Giveaway - presenceData.smallImageText = 'Giveaway' - - break - } - case document.location.pathname.indexOf( - '/variants/sideway-pawns/game/', - ): { - presenceData.details = 'Playing Sideway Pawns' - presenceData.smallImageKey = ActivityAssets.Sideways - presenceData.smallImageText = 'Sideways Pawns' - - break - } - case document.location.pathname.indexOf('/variants/chaturanga/game/'): { - presenceData.details = 'Playing Chaturanga' - presenceData.smallImageKey = ActivityAssets.Chataranga - presenceData.smallImageText = 'Chaturanga' - - break - } - case document.location.pathname.indexOf('/variants/blindfold/game/'): { - presenceData.details = 'Playing Blindfold' - presenceData.smallImageKey = ActivityAssets.Blindfold - presenceData.smallImageText = 'Blindfold' - - break - } - case document.location.pathname.indexOf('/variants/no-castling/game/'): { - presenceData.details = 'Playing No Castling' - presenceData.smallImageKey = ActivityAssets.Nocastle - presenceData.smallImageText = 'No Castling' - - break - } - case document.location.pathname.indexOf( - '/variants/capture-anything/game/', - ): { - presenceData.details = 'Playing Capture Anything' - presenceData.smallImageKey = ActivityAssets.Anything - presenceData.smallImageText = 'Capture Anything' - - break - } - case document.location.pathname.indexOf('/variants/atomic/game/'): { - presenceData.details = 'Playing Atomic' - presenceData.smallImageKey = ActivityAssets.Atomic - presenceData.smallImageText = 'Atomic' - - break - } - default: - if (document.location.pathname.includes('/automate')) { - presenceData.details = 'Playing Automate chess' - presenceData.smallImageKey = ActivityAssets.Automate - presenceData.smallImageText = 'Automate' - } - else { - switch (document.location.pathname) { - case '/puzzles/rated': { - presenceData.details = 'Solving puzzles' - presenceData.smallImageKey = ActivityAssets.Puzzle - presenceData.smallImageText = 'Puzzles' - - break - } - case '/puzzles/rush': { - presenceData.details = 'Playing Puzzle Rush' - presenceData.smallImageKey = ActivityAssets.PuzzleRush - presenceData.smallImageText = 'Puzzle Rush' - - break - } - case '/puzzles/battle': { - presenceData.details = 'Playing Puzzle Battle' - presenceData.smallImageKey = ActivityAssets.PuzzleWar - presenceData.smallImageText = 'Puzzle Battle' - - break - } - default: - if ( - document.location.pathname.indexOf( - '/forum/view/daily-puzzles/', - ) === 0 - ) { - presenceData.details = 'Solving Daily Puzzle' - presenceData.smallImageKey = ActivityAssets.PuzzleOfDay - presenceData.smallImageText = 'Daily Puzzle' - } - else if (document.location.pathname.includes('/solo-chess')) { - presenceData.details = 'Playing Solo Chess' - presenceData.smallImageKey = ActivityAssets.SoloChess - presenceData.smallImageText = 'Solo Chess' - } - else if (document.location.pathname.includes('/drills')) { - presenceData.details = 'Playing drills' - presenceData.smallImageKey = ActivityAssets.Drills - presenceData.smallImageText = 'Drills' - } - else if (document.location.pathname.includes('/lessons')) { - presenceData.details = 'Viewing lessons' - presenceData.smallImageKey = ActivityAssets.Lessons - presenceData.smallImageText = 'Lessons' - } - else if (document.location.pathname.includes('/analysis')) { - presenceData.details = 'Analyzing a game' - presenceData.smallImageKey = ActivityAssets.Analysis - presenceData.smallImageText = 'Analysis' - } - else if ( - document.location.pathname.indexOf('/article/view') === 0 - ) { - presenceData.details = 'Reading an article' - presenceData.smallImageKey = ActivityAssets.Articles - presenceData.smallImageText = 'Article' - presenceData.state = document.title - } - else if (document.location.pathname === '/articles') { - presenceData.details = 'Browsing through articles' - presenceData.smallImageKey = ActivityAssets.Articles - presenceData.smallImageText = 'Articles' - } - else if (document.location.pathname === '/videos') { - presenceData.details = 'Browsing through videos' - } - else if (document.location.pathname.includes('/vision')) { - presenceData.details = 'Training vision' - presenceData.smallImageKey = ActivityAssets.Vision - presenceData.smallImageText = 'Vision' - } - else if (document.location.pathname.includes('/openings')) { - presenceData.details = 'Viewing openings' - presenceData.smallImageKey = ActivityAssets.Openings - presenceData.smallImageText = 'Openings' - } - else if (document.location.pathname.includes('/explorer')) { - presenceData.details = 'Using games explorer' - presenceData.smallImageKey = ActivityAssets.Explorer - presenceData.smallImageText = 'Games explorer' - } - else if (document.location.pathname.includes('/forum')) { - presenceData.details = 'Browsing through forum' - presenceData.smallImageKey = ActivityAssets.Forum - presenceData.smallImageText = 'Forum' - } - else if (document.location.pathname.includes('/clubs')) { - presenceData.details = 'Browsing through clubs' - presenceData.smallImageKey = ActivityAssets.Clubs - presenceData.smallImageText = 'Clubs' - } - else if (document.location.pathname === '/blogs') { - presenceData.details = 'Browsing through blogs' - presenceData.smallImageKey = ActivityAssets.Blog - presenceData.smallImageText = 'Blog' - } - else if (document.location.pathname.indexOf('/blog/') === 0) { - presenceData.details = 'Reading a blog post' - presenceData.smallImageKey = ActivityAssets.Blog - presenceData.smallImageText = 'Blog post' - presenceData.state = document.title - } - else if (document.location.pathname.includes('/members')) { - presenceData.details = 'Browsing through members' - presenceData.smallImageKey = ActivityAssets.Members - presenceData.smallImageText = 'Members' - } - else if (document.location.pathname.includes('/coaches')) { - presenceData.details = 'Browsing through coaches' - presenceData.smallImageKey = ActivityAssets.Coaches - presenceData.smallImageText = 'Coaches' - } - else if (document.location.pathname.includes('/today')) { - presenceData.details = 'Viewing Chess Today' - presenceData.smallImageKey = ActivityAssets.ChessToday - presenceData.smallImageText = 'Chess Today' - } - else if ( - document.location.pathname.indexOf('/news/view/') === 0 - ) { - presenceData.details = 'Reading news' - presenceData.smallImageKey = ActivityAssets.News - presenceData.smallImageText = 'News' - presenceData.state = document.title - } - else if (document.location.pathname === '/news') { - presenceData.details = 'Browsing through news' - presenceData.smallImageKey = ActivityAssets.News - presenceData.smallImageText = 'News' - } - else if (document.location.pathname.includes('/tv')) { - presenceData.details = 'Viewing ChessTV' - presenceData.smallImageKey = ActivityAssets.ChessTV - presenceData.smallImageText = 'ChessTV' - } - else if (document.location.pathname === '/games') { - presenceData.details = 'Browsing through master games' - presenceData.smallImageKey = ActivityAssets.MasterGames - presenceData.smallImageText = 'Master Games' - } - else if ( - document.location.pathname.indexOf('/games/view/') === 0 - ) { - presenceData.details = 'Watching a master game' - presenceData.smallImageKey = ActivityAssets.MasterGames - presenceData.smallImageText = 'Master Games' - presenceData.state = document.title.substring( - 0, - document.title.indexOf(')') + 1, - ) - } - else if ( - document.location.pathname.includes( - '/computer-chess-championship', - ) - ) { - presenceData.details = 'Watching Computer Chess Championship' - presenceData.state = document.title.substring( - 0, - document.title.indexOf('-'), - ) - } - else if ( - document.location.pathname.indexOf('/video/player/') === 0 - ) { - const video = document.querySelector('video') - - if (video && !Number.isNaN(video.duration)) { - [presenceData.startTimestamp, presenceData.endTimestamp] = presence.getTimestamps( - Math.floor(video.currentTime), - Math.floor(video.duration), - ) - presenceData.largeImageKey = 'https://cdn.rcd.gg/PreMiD/websites/C/Chess/assets/logo.png' - presenceData.details = 'Watching video' - presenceData.state = document.title - if (video.paused) { - presenceData.smallImageKey = Assets.Pause - presenceData.smallImageText = (await strings).pause - delete presenceData.startTimestamp - delete presenceData.endTimestamp - } - else { - presenceData.smallImageKey = Assets.Play - presenceData.smallImageText = (await strings).play - } - } - } - } - } - } - } - if (presenceData.details) - presence.setActivity(presenceData) - else presence.setActivity() -})