Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ match insights, various visual tweaks throughout the site, and time-saving short
- Display the player's yearly salary next to weekly salary on the player detail page
- Show the likelihood of a player receiving a yellow or red card based on their personality
- Show the likelihood of a team spirit drop when buying or selling a player based on their personality
- List players close to their next birthday in the player list page sidebar

### Transfer
- Save and reuse transfer search filters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ See https://github.com/lucide-icons/lucide/blob/main/LICENSE */
mask: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='m9 11-6 6v3h9l3-3'/><path d='m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4'/></svg>") no-repeat center / contain;
}

.hte-icon-chevron-up::before {
mask: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='m18 15-6-6-6 6'/></svg>") no-repeat center / contain;
}

.hte-icon-chevron-down::before {
mask: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='m6 9 6 6 6-6'/></svg>") no-repeat center / contain;
}

.hte-icon-card-yellow,
.hte-icon-card-red {
&::before {
Expand Down
1 change: 1 addition & 0 deletions src/common/styles/common.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@import url('_colors.css');
@import url('_icons.css');
@import url('_spacing.css');
@import url('_typography.css');
23 changes: 16 additions & 7 deletions src/common/utils/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { allMetadata } from '@/common/utils/metadata'

const settingKey = (moduleId: string, settingId: string) => `${moduleId}:${settingId}`

const buildDefaultSettings = (): Record<string, boolean> => {
const defaults: Record<string, boolean> = {}
const buildDefaultSettings = (): Record<string, boolean | number> => {
const defaults: Record<string, boolean | number> = {}

for (const metadata of allMetadata) {
defaults[settingKey(metadata.id, 'enabled')] = true
Expand All @@ -18,10 +18,13 @@ const buildDefaultSettings = (): Record<string, boolean> => {

const defaultSettings = buildDefaultSettings()

const settingsStorage = storage.defineItem<Record<string, boolean>>('local:settings', { fallback: {}, version: 1 })
const settingsStorage = storage.defineItem<Record<string, boolean | number>>('local:settings', {
fallback: {},
version: 1,
})

const getSettings = (() => {
let promise: Promise<Record<string, boolean>> | null = null
let promise: Promise<Record<string, boolean | number>> | null = null

settingsStorage.watch(() => {
promise = null
Expand All @@ -39,18 +42,24 @@ const getSettings = (() => {
}
})()

export const getSetting = async (moduleId: string, setting: string): Promise<boolean> => {
const getSetting = async <T extends boolean | number>(moduleId: string, setting: string): Promise<T> => {
const key = settingKey(moduleId, setting)
const settings = await getSettings()

if (!(key in settings)) throw new Error(`Setting ${key} does not exist`)

return settings[key]
return settings[key] as T
}

export const getBoolSetting = (moduleId: string, setting: string): Promise<boolean> =>
getSetting<boolean>(moduleId, setting)

export const getIntSetting = (moduleId: string, setting: string): Promise<number> =>
getSetting<number>(moduleId, setting)

let writeQueue = Promise.resolve()

export const setSetting = (moduleId: string, setting: string, value: boolean): Promise<void> => {
export const setSetting = (moduleId: string, setting: string, value: boolean | number): Promise<void> => {
const key = settingKey(moduleId, setting)

if (!(key in defaultSettings)) throw new Error(`Setting ${key} does not exist`)
Expand Down
1 change: 0 additions & 1 deletion src/entrypoints/content/common/styles/common.css
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
@import url('../../../../common/styles/common.css');
@import url('_icons.css');
10 changes: 3 additions & 7 deletions src/entrypoints/content/common/types/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,9 @@ import { Page } from '@/entrypoints/content/common/utils/pages'
/**
* Module-specific setting.
*/
export type ModuleSetting = {
// Label shown in the popup UI
label: string
// Default value for the setting
// Currently only boolean settings are supported
default: boolean
}
export type ModuleSetting =
| { label: string; default: boolean }
| { label: string; default: number; min?: number; max?: number }

export type ModuleGroup = 'general' | 'match' | 'player' | 'transfer'

Expand Down
6 changes: 4 additions & 2 deletions src/entrypoints/content/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import '@/entrypoints/content/common/styles/common.css'

import { defineContentScript } from 'wxt/utils/define-content-script'

import { getSetting } from '@/common/utils/settings'
import { getBoolSetting } from '@/common/utils/settings'
import type { Handler, Module } from '@/entrypoints/content/common/types/module'
import { getCurrentPathname } from '@/entrypoints/content/common/utils/location'
import { logger } from '@/entrypoints/content/common/utils/logger'
Expand All @@ -18,6 +18,7 @@ import playerHtmsPoints from '@/entrypoints/content/modules/player-htms-points'
import playerSalary from '@/entrypoints/content/modules/player-salary'
import playerSkillBonus from '@/entrypoints/content/modules/player-skill-bonus'
import playerTsDropRates from '@/entrypoints/content/modules/player-ts-drop-rates'
import playerUpcomingBirthdays from '@/entrypoints/content/modules/player-upcoming-birthdays'
import transferAge from '@/entrypoints/content/modules/transfer-age'
import weekNumber from '@/entrypoints/content/modules/week-number'

Expand All @@ -30,6 +31,7 @@ const modules: Module[] = [
playerSalary,
playerCardRates,
playerTsDropRates,
playerUpcomingBirthdays,
transferAge,
hteVersion,
matchHatstats,
Expand All @@ -46,7 +48,7 @@ const getHandler = (module: Module): Handler | undefined => {
}

const runModule = async (module: Module): Promise<void> => {
const enabled = await getSetting(module.metadata.id, 'enabled')
const enabled = await getBoolSetting(module.metadata.id, 'enabled')
if (!enabled) {
logger.debug(`Skipping disabled module: ${module.metadata.name}`)
return
Expand Down
6 changes: 3 additions & 3 deletions src/entrypoints/content/modules/denomination/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { describe, expect, it, vi } from 'vitest'

import { getSetting } from '@/common/utils/settings'
import { getBoolSetting } from '@/common/utils/settings'
import { adjustDenominationValue, isDenominationType } from '@/entrypoints/content/modules/denomination/utils'

vi.mock(import('@/common/utils/settings'), async (importOriginal) => {
return {
...(await importOriginal()),
getSetting: vi.fn<typeof getSetting>().mockResolvedValue(true),
getBoolSetting: vi.fn<typeof getBoolSetting>().mockResolvedValue(true),
}
})

Expand All @@ -30,7 +30,7 @@ describe(adjustDenominationValue, () => {
})

it('returns raw aggressiveness value when reverseAggressiveness is false', async () => {
vi.mocked(getSetting).mockResolvedValueOnce(false)
vi.mocked(getBoolSetting).mockResolvedValueOnce(false)

await expect(adjustDenominationValue('aggressiveness', 3)).resolves.toBe(3)
})
Expand Down
4 changes: 2 additions & 2 deletions src/entrypoints/content/modules/denomination/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getSetting } from '@/common/utils/settings'
import { getBoolSetting } from '@/common/utils/settings'
import { DENOMINATION_TYPES, MAX_VALUES } from '@/entrypoints/content/modules/denomination/constants'
import { DenominationType } from '@/entrypoints/content/modules/denomination/types'

Expand All @@ -15,7 +15,7 @@ const getDenominationConfig = async (lt: DenominationType): Promise<Denomination
case 'confidence':
return { offset: -23, max: 9 }
case 'aggressiveness':
return { reverse: await getSetting('denomination', 'reverseAggressiveness') }
return { reverse: await getBoolSetting('denomination', 'reverseAggressiveness') }
default:
return {}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import metadata from '@/entrypoints/content/modules/match-go-to-matches/metadata
const matchGoToMatches: Module = {
metadata,
pages: [pages.matchOrder],
run: async (): Promise<void> => {
run: async () => {
const headerRight = await waitForElement('ht-matchorder .mo-topbar .header-right')
if (!headerRight) return

Expand Down
81 changes: 81 additions & 0 deletions src/entrypoints/content/modules/player-upcoming-birthdays/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { el } from '@/common/utils/dom'
import { getIntSetting } from '@/common/utils/settings'
import type { Module } from '@/entrypoints/content/common/types/module'
import { querySelector, querySelectorAll, querySelectorIn } from '@/entrypoints/content/common/utils/dom'
import { pages } from '@/entrypoints/content/common/utils/pages'
import { PlayerAge } from '@/entrypoints/content/common/utils/player/constants'
import { parsePlayerAge } from '@/entrypoints/content/common/utils/player/utils'
import { createSidebarBox } from '@/entrypoints/content/common/utils/sidebar/box'
import metadata from '@/entrypoints/content/modules/player-upcoming-birthdays/metadata'

type Player = {
name: string
href: string
age: string
parsedAge: PlayerAge
}

const getPlayersWithUpcomingBirthdays = (threshold: number): Player[] => {
const players: Player[] = []

querySelectorAll('#mainBody > .playerList > .teamphoto-player').forEach((el) => {
const nameLink = querySelectorIn<HTMLAnchorElement>(el, ':scope > h3 a')
if (!nameLink) return

const ageCell = querySelectorIn(el, '.transferPlayerInformation table tbody tr:first-child td:nth-child(2)')
if (!ageCell) return

const parsedAge = parsePlayerAge(ageCell)
if (!parsedAge) return

players.push({
name: nameLink.textContent.trim(),
href: nameLink.href,
age: ageCell.textContent.trim(),
parsedAge,
})
})

return players
.filter((player) => player.parsedAge.days > threshold)
.sort((a, b) => b.parsedAge.days - a.parsedAge.days)
}

const renderSidebar = (sidebar: HTMLDivElement, players: Player[]): void => {
const table = el('table')
const tbody = el('tbody')
table.append(tbody)

players.forEach((player) => {
const row = el('tr')

const nameCell = el('td')
nameCell.append(el('a', { href: player.href, textContent: player.name }))

const ageCell = el('td', { textContent: player.age, className: 'right' })

row.append(nameCell, ageCell)
tbody.append(row)
})

const { box, boxBody } = createSidebarBox(i18n.t('player_upcoming_birthdays_title'))
boxBody.append(table)
sidebar.append(box)
}

const playerUpcomingBirthdays: Module = {
metadata,
pages: [pages.playerList.senior.own],
run: async () => {
const sidebar = querySelector<HTMLDivElement>('#sidebar')
if (!sidebar) return

const threshold = await getIntSetting(metadata.id, 'threshold')
const players = getPlayersWithUpcomingBirthdays(threshold)
if (players.length === 0) return

renderSidebar(sidebar, players)
},
}

export default playerUpcomingBirthdays
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { ModuleMetadata } from '@/entrypoints/content/common/types/module'

const metadata = {
id: 'player-upcoming-birthdays',
group: 'player',
name: 'Upcoming Birthdays',
description: 'Show a sidebar box listing players close to their birthday.',
settings: {
threshold: { label: 'Show players older than N days', default: 90, min: 0, max: 111 },
},
} as const satisfies ModuleMetadata

export default metadata
70 changes: 70 additions & 0 deletions src/entrypoints/popup/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,75 @@ body {
accent-color: var(--hte-color-indigo-5);
flex-shrink: 0;
}

& .number-input-wrapper {
display: flex;
height: 27px;
border: 1px solid var(--hte-color-gray-3);
border-radius: 4px;
overflow: hidden;
font-size: 13px;
color: var(--hte-color-gray-9);

&:focus-within {
border-color: var(--hte-color-indigo-5);
}

& input[type="number"] {
width: 32px;
padding: 4px;
border: none;
outline: none;
color: inherit;
font-size: inherit;
background: white;
appearance: textfield;

&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
appearance: none;
}
}

& .number-input-controls {
display: flex;
flex-direction: column;
border-left: 1px solid var(--hte-color-gray-3);
}

& .number-input-up,
& .number-input-down {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
flex: 1;
padding: 0;
background: white;
border: none;
outline: none;
cursor: pointer;
color: var(--hte-color-gray-6);

&:hover {
background: var(--hte-color-gray-1);
color: var(--hte-color-gray-9);
}

& [class*="hte-icon-"] {
display: flex;

&::before {
width: 12px;
height: 12px;
}
}
}

& .number-input-up {
border-bottom: 1px solid var(--hte-color-gray-3);
}
}
}

& .setting {
Expand All @@ -114,6 +183,7 @@ body {
}

/* Enabled toggle — hide native checkbox, style label as pill */

& input[id$="-enabled"] {
position: absolute;
opacity: 0;
Expand Down
Loading