mirror of
https://github.com/williamsjack/pairwise-glicko-ranking.git
synced 2026-07-22 06:44:09 +00:00
Add star rating frontmatter property
This commit is contained in:
parent
d2ba407281
commit
92517fd9ba
11 changed files with 686 additions and 150 deletions
|
|
@ -79,13 +79,14 @@ Optionally, the plugin can generate a **post-session report** - a Markdown note
|
|||
|
||||
The plugin can write the following properties to frontmatter (configurable per-cohort, all optional, names customisable):
|
||||
|
||||
- Rating
|
||||
- Glicko rating
|
||||
- Rank (1 = highest within the cohort)
|
||||
- Matches
|
||||
- Wins
|
||||
- Uncertainty (sigma)
|
||||
- Star rating (rank or rating normalised to an n-star scale)
|
||||
|
||||
You can then use the values computed by the plugin however you want. For example, enable the Rank property and then sort a Base by it to see your notes in ranked order, or use the Rating property to filter for your best notes with a rating above a certain threshold.
|
||||
You can then use the values computed by the plugin however you want. For example, enable the Rank property and then sort a Base by it to see your notes in ranked order, or use the Glicko rating property to filter for your best notes with a rating above a certain threshold.
|
||||
|
||||
**Tip:** Configure global defaults in Settings, and (optionally) set per-cohort overrides when creating or editing a cohort.
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ import { ConfirmModal } from '../ui/ConfirmModal';
|
|||
import { ALL_SENTINEL, ResetNoteModal } from '../ui/ResetNoteModal';
|
||||
import {
|
||||
computeRanksForAll,
|
||||
updateCohortFrontmatter,
|
||||
computeStarsForAll,
|
||||
refreshCohortRankAndStars,
|
||||
writeFrontmatterStatsForPlayer,
|
||||
} from '../utils/FrontmatterStats';
|
||||
import { getNoteId } from '../utils/NoteIds';
|
||||
|
|
@ -89,26 +90,16 @@ export async function resetNoteRating(
|
|||
);
|
||||
|
||||
const rankMap = computeRanksForAll(cohort);
|
||||
const wantStars = !!fm.stars.enabled && !!fm.stars.property;
|
||||
const starMap = wantStars ? computeStarsForAll(cohort, fm.stars) : undefined;
|
||||
|
||||
// Write the reset note's own stats (rating, matches, wins, rank)
|
||||
await writeFrontmatterStatsForPlayer(app, fm, cohort, file, noteId, rankMap);
|
||||
|
||||
// Update rank across the whole cohort since relative ordering changed
|
||||
const rankCfg = fm.rank;
|
||||
if (!rankCfg.enabled || !rankCfg.property) continue;
|
||||
// Write the reset note's own stats (rating, matches, wins, rank, stars)
|
||||
await writeFrontmatterStatsForPlayer(app, fm, cohort, file, noteId, rankMap, starMap);
|
||||
|
||||
// Update rank/stars across the whole cohort since relative ordering changed
|
||||
const cohortFiles = await resolveFilesForCohort(app, def, {
|
||||
excludeFolderPath: settings.templatesFolderPath,
|
||||
});
|
||||
if (cohortFiles.length === 0) continue;
|
||||
await updateCohortFrontmatter(
|
||||
app,
|
||||
cohortFiles,
|
||||
rankMap,
|
||||
rankCfg.property,
|
||||
undefined,
|
||||
'Updating ranks...',
|
||||
settings.idPropertyName,
|
||||
);
|
||||
await refreshCohortRankAndStars(app, fm, cohort, cohortFiles, settings.idPropertyName, rankMap);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
24
src/main.ts
24
src/main.ts
|
|
@ -22,7 +22,7 @@ import { CohortPicker } from './ui/CohortPicker';
|
|||
import { ensureBaseCohortTarget } from './utils/EnsureBaseCohort';
|
||||
import { ensureFolderCohortPath } from './utils/EnsureFolderCohort';
|
||||
import { ensureUniqueIds } from './utils/EnsureUniqueIds';
|
||||
import { computeRanksForAll, updateCohortFrontmatter } from './utils/FrontmatterStats';
|
||||
import { refreshCohortRankAndStars } from './utils/FrontmatterStats';
|
||||
import { debugWarn, setDebugLogging } from './utils/logger';
|
||||
|
||||
export default class GlickoPlugin extends Plugin {
|
||||
|
|
@ -266,26 +266,14 @@ export default class GlickoPlugin extends Plugin {
|
|||
this.settings.frontmatterProperties,
|
||||
def.frontmatterOverrides,
|
||||
);
|
||||
const rankCfg = fm.rank;
|
||||
if (!rankCfg.enabled || !rankCfg.property) return;
|
||||
|
||||
const files = await resolveFilesForCohort(this.app, def, {
|
||||
excludeFolderPath: this.settings.templatesFolderPath,
|
||||
});
|
||||
if (files.length === 0) return;
|
||||
|
||||
const rankMap = computeRanksForAll(cohort);
|
||||
|
||||
updateCohortFrontmatter(
|
||||
this.app,
|
||||
files,
|
||||
rankMap,
|
||||
rankCfg.property,
|
||||
undefined,
|
||||
'Updating ranks in frontmatter...',
|
||||
this.settings.idPropertyName,
|
||||
).catch((e) => {
|
||||
console.error('[Glicko] Failed to update ranks in frontmatter', e);
|
||||
});
|
||||
refreshCohortRankAndStars(this.app, fm, cohort, files, this.settings.idPropertyName).catch(
|
||||
(e) => {
|
||||
console.error('[Glicko] Failed to refresh cohort frontmatter', e);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,16 +9,30 @@ import { CohortPicker } from '../ui/CohortPicker';
|
|||
import { ConfirmModal } from '../ui/ConfirmModal';
|
||||
import {
|
||||
computeRanksForAll,
|
||||
computeStarsForAll,
|
||||
previewCohortFrontmatterPropertyUpdates,
|
||||
updateCohortFrontmatter,
|
||||
} from '../utils/FrontmatterStats';
|
||||
import { applyIdTransferPlan, planIdTransfer } from '../utils/IdTransfer';
|
||||
import { withNotice } from '../utils/safe';
|
||||
import type { FrontmatterPropertiesSettings, IdLocation } from './settings';
|
||||
import { DEFAULT_SETTINGS, effectiveFrontmatterProperties } from './settings';
|
||||
import type { FmSimpleKey } from './frontmatterCopy';
|
||||
import {
|
||||
FM_SIMPLE_COPY,
|
||||
FM_SIMPLE_KEYS,
|
||||
FM_STARS_COPY,
|
||||
PROPERTY_NAME_LABEL,
|
||||
STAR_MAPPING_LABELS,
|
||||
STAR_MODE_LABELS,
|
||||
} from './frontmatterCopy';
|
||||
import type { IdLocation } from './settings';
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
effectiveFrontmatterProperties,
|
||||
starScaleConfigEquals,
|
||||
} from './settings';
|
||||
import { migrateIdPropertyName } from './SettingsTabMigration';
|
||||
|
||||
type PropKey = keyof FrontmatterPropertiesSettings;
|
||||
type PropKey = FmSimpleKey;
|
||||
|
||||
function getByPath(root: unknown, path: string): unknown {
|
||||
const parts = path.split('.');
|
||||
|
|
@ -46,50 +60,6 @@ function setByPath(root: Record<string, unknown>, path: string, value: unknown):
|
|||
cur[parts[parts.length - 1]] = value;
|
||||
}
|
||||
|
||||
const FM_KEY_META: Record<
|
||||
PropKey,
|
||||
{ group: string; toggleName: string; toggleDesc: string; nameDesc: string; defaultProp: string }
|
||||
> = {
|
||||
rating: {
|
||||
group: 'Rating',
|
||||
toggleName: 'Write rating to frontmatter',
|
||||
toggleDesc: 'Write the current Glicko rating to a frontmatter property.',
|
||||
nameDesc: 'Frontmatter property used to store the rating.',
|
||||
defaultProp: DEFAULT_SETTINGS.frontmatterProperties.rating.property,
|
||||
},
|
||||
uncertainty: {
|
||||
group: 'Uncertainty',
|
||||
toggleName: 'Write uncertainty to frontmatter',
|
||||
toggleDesc:
|
||||
'Write how uncertain the rating is. Starts high and decreases as more comparisons are made.',
|
||||
nameDesc: 'Frontmatter property used to store the uncertainty.',
|
||||
defaultProp: DEFAULT_SETTINGS.frontmatterProperties.uncertainty.property,
|
||||
},
|
||||
rank: {
|
||||
group: 'Rank',
|
||||
toggleName: 'Write rank to frontmatter',
|
||||
toggleDesc: 'Write the cohort rank (1 = highest) to a frontmatter property.',
|
||||
nameDesc: 'Frontmatter property used to store the rank.',
|
||||
defaultProp: DEFAULT_SETTINGS.frontmatterProperties.rank.property,
|
||||
},
|
||||
matches: {
|
||||
group: 'Matches',
|
||||
toggleName: 'Write match count to frontmatter',
|
||||
toggleDesc: 'Write the number of matches played to a frontmatter property.',
|
||||
nameDesc: 'Frontmatter property used to store the match count.',
|
||||
defaultProp: DEFAULT_SETTINGS.frontmatterProperties.matches.property,
|
||||
},
|
||||
wins: {
|
||||
group: 'Wins',
|
||||
toggleName: 'Write win count to frontmatter',
|
||||
toggleDesc: 'Write the number of wins to a frontmatter property.',
|
||||
nameDesc: 'Frontmatter property used to store the win count.',
|
||||
defaultProp: DEFAULT_SETTINGS.frontmatterProperties.wins.property,
|
||||
},
|
||||
};
|
||||
|
||||
const FM_KEYS_ORDERED: readonly PropKey[] = ['rating', 'uncertainty', 'rank', 'matches', 'wins'];
|
||||
|
||||
export default class GlickoSettingsTab extends PluginSettingTab {
|
||||
icon = 'trophy';
|
||||
plugin: GlickoPlugin;
|
||||
|
|
@ -391,27 +361,27 @@ export default class GlickoSettingsTab extends PluginSettingTab {
|
|||
},
|
||||
];
|
||||
|
||||
for (const key of FM_KEYS_ORDERED) {
|
||||
const meta = FM_KEY_META[key];
|
||||
for (const key of FM_SIMPLE_KEYS) {
|
||||
const copy = FM_SIMPLE_COPY[key];
|
||||
items.push({
|
||||
type: 'group',
|
||||
heading: meta.group,
|
||||
heading: copy.label,
|
||||
items: [
|
||||
{
|
||||
name: meta.toggleName,
|
||||
desc: meta.toggleDesc,
|
||||
name: copy.toggleName,
|
||||
desc: copy.toggleDesc,
|
||||
control: {
|
||||
type: 'toggle',
|
||||
key: `frontmatterProperties.${key}.enabled`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Property name',
|
||||
desc: meta.nameDesc,
|
||||
name: PROPERTY_NAME_LABEL,
|
||||
desc: copy.nameDesc,
|
||||
control: {
|
||||
type: 'text',
|
||||
key: `frontmatterProperties.${key}.property`,
|
||||
placeholder: meta.defaultProp,
|
||||
placeholder: DEFAULT_SETTINGS.frontmatterProperties[key].property,
|
||||
disabled: () => !this.plugin.settings.frontmatterProperties[key].enabled,
|
||||
},
|
||||
},
|
||||
|
|
@ -419,6 +389,87 @@ export default class GlickoSettingsTab extends PluginSettingTab {
|
|||
});
|
||||
}
|
||||
|
||||
const starsEnabled = () => this.plugin.settings.frontmatterProperties.stars.enabled;
|
||||
const starFields = FM_STARS_COPY.fields;
|
||||
items.push({
|
||||
type: 'group',
|
||||
heading: FM_STARS_COPY.label,
|
||||
items: [
|
||||
{
|
||||
name: FM_STARS_COPY.toggleName,
|
||||
desc: FM_STARS_COPY.toggleDesc,
|
||||
control: { type: 'toggle', key: 'frontmatterProperties.stars.enabled' },
|
||||
},
|
||||
{
|
||||
name: PROPERTY_NAME_LABEL,
|
||||
desc: FM_STARS_COPY.nameDesc,
|
||||
control: {
|
||||
type: 'text',
|
||||
key: 'frontmatterProperties.stars.property',
|
||||
placeholder: DEFAULT_SETTINGS.frontmatterProperties.stars.property,
|
||||
disabled: () => !starsEnabled(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: starFields.max.name,
|
||||
desc: starFields.max.desc,
|
||||
control: {
|
||||
type: 'number',
|
||||
key: 'frontmatterProperties.stars.max',
|
||||
min: 2,
|
||||
step: 1,
|
||||
placeholder: String(DEFAULT_SETTINGS.frontmatterProperties.stars.max),
|
||||
disabled: () => !starsEnabled(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: starFields.allowZero.name,
|
||||
desc: starFields.allowZero.desc,
|
||||
control: {
|
||||
type: 'toggle',
|
||||
key: 'frontmatterProperties.stars.allowZero',
|
||||
disabled: () => !starsEnabled(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: starFields.mode.name,
|
||||
desc: starFields.mode.desc,
|
||||
control: {
|
||||
type: 'dropdown',
|
||||
key: 'frontmatterProperties.stars.mode',
|
||||
defaultValue: DEFAULT_SETTINGS.frontmatterProperties.stars.mode,
|
||||
options: STAR_MODE_LABELS,
|
||||
disabled: () => !starsEnabled(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: starFields.decimals.name,
|
||||
desc: starFields.decimals.desc,
|
||||
control: {
|
||||
type: 'number',
|
||||
key: 'frontmatterProperties.stars.decimals',
|
||||
min: 1,
|
||||
max: 2,
|
||||
step: 1,
|
||||
disabled: () => !starsEnabled(),
|
||||
},
|
||||
visible: () =>
|
||||
starsEnabled() && this.plugin.settings.frontmatterProperties.stars.mode === 'float',
|
||||
},
|
||||
{
|
||||
name: starFields.mapping.name,
|
||||
desc: starFields.mapping.desc,
|
||||
control: {
|
||||
type: 'dropdown',
|
||||
key: 'frontmatterProperties.stars.mapping',
|
||||
defaultValue: DEFAULT_SETTINGS.frontmatterProperties.stars.mapping,
|
||||
options: STAR_MAPPING_LABELS,
|
||||
disabled: () => !starsEnabled(),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
|
|
@ -598,7 +649,11 @@ export default class GlickoSettingsTab extends PluginSettingTab {
|
|||
}
|
||||
}
|
||||
|
||||
if (changed.length === 0) return;
|
||||
const starsRelevant =
|
||||
(oldEffective.stars.enabled || newEffective.stars.enabled) &&
|
||||
!starScaleConfigEquals(oldEffective.stars, newEffective.stars);
|
||||
|
||||
if (changed.length === 0 && !starsRelevant) return;
|
||||
|
||||
const files = await resolveFilesForCohort(this.app, def, {
|
||||
excludeFolderPath: this.plugin.settings.templatesFolderPath,
|
||||
|
|
@ -722,5 +777,77 @@ export default class GlickoSettingsTab extends PluginSettingTab {
|
|||
new Notice(`Wrote "${change.newProp}" on ${res.updated} notes.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (starsRelevant) {
|
||||
const oldStars = oldEffective.stars;
|
||||
const newStars = newEffective.stars;
|
||||
|
||||
if (oldStars.enabled && !newStars.enabled && oldStars.property) {
|
||||
const preview = await previewCohortFrontmatterPropertyUpdates(
|
||||
this.app,
|
||||
files,
|
||||
new Map(),
|
||||
'',
|
||||
oldStars.property,
|
||||
idPropName,
|
||||
);
|
||||
if (preview.wouldUpdate > 0) {
|
||||
const ok = await new ConfirmModal(
|
||||
this.app,
|
||||
'Remove star rating?',
|
||||
`Remove frontmatter property "${oldStars.property}" from ${preview.wouldUpdate} notes in this cohort?`,
|
||||
'Yes, remove',
|
||||
"No, don't update",
|
||||
).openAndConfirm();
|
||||
if (ok) {
|
||||
const res = await updateCohortFrontmatter(
|
||||
this.app,
|
||||
files,
|
||||
new Map(),
|
||||
'',
|
||||
oldStars.property,
|
||||
`Removing "${oldStars.property}" from ${preview.wouldUpdate} notes...`,
|
||||
idPropName,
|
||||
);
|
||||
new Notice(`Removed "${oldStars.property}" from ${res.updated} notes.`);
|
||||
}
|
||||
}
|
||||
} else if (newStars.enabled && newStars.property) {
|
||||
const starVals = cohort ? computeStarsForAll(cohort, newStars) : new Map<string, number>();
|
||||
const oldProp =
|
||||
oldStars.enabled && oldStars.property && oldStars.property !== newStars.property
|
||||
? oldStars.property
|
||||
: undefined;
|
||||
const preview = await previewCohortFrontmatterPropertyUpdates(
|
||||
this.app,
|
||||
files,
|
||||
starVals,
|
||||
newStars.property,
|
||||
oldProp,
|
||||
idPropName,
|
||||
);
|
||||
if (preview.wouldUpdate > 0) {
|
||||
const ok = await new ConfirmModal(
|
||||
this.app,
|
||||
'Update star rating?',
|
||||
`Write frontmatter property "${newStars.property}" on ${preview.wouldUpdate} notes in this cohort?`,
|
||||
'Yes, write',
|
||||
"No, don't update",
|
||||
).openAndConfirm();
|
||||
if (ok) {
|
||||
const res = await updateCohortFrontmatter(
|
||||
this.app,
|
||||
files,
|
||||
starVals,
|
||||
newStars.property,
|
||||
oldProp,
|
||||
`Updating "${newStars.property}" on ${preview.wouldUpdate} notes...`,
|
||||
idPropName,
|
||||
);
|
||||
new Notice(`Updated ${res.updated} notes.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
122
src/settings/frontmatterCopy.ts
Normal file
122
src/settings/frontmatterCopy.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import type { StarMapping, StarScaleConfig } from './settings';
|
||||
|
||||
// Single source of truth for the names and descriptions of every frontmatter
|
||||
// property the plugin can write. Both surfaces that render these settings read
|
||||
// from here so their copy cannot drift:
|
||||
// - the global settings page (declarative, in SettingsTab.ts)
|
||||
// - the per-cohort options modal (imperative, via FrontmatterPropertyRow.ts
|
||||
// and StarScaleRow.ts)
|
||||
|
||||
export type FmSimpleKey = 'rating' | 'uncertainty' | 'rank' | 'matches' | 'wins';
|
||||
|
||||
export const FM_SIMPLE_KEYS: readonly FmSimpleKey[] = [
|
||||
'rating',
|
||||
'uncertainty',
|
||||
'rank',
|
||||
'matches',
|
||||
'wins',
|
||||
] as const;
|
||||
|
||||
interface SimpleCopy {
|
||||
// Group heading on the global page; also the single-row name in the cohort modal.
|
||||
label: string;
|
||||
// Name of the enable toggle on the global page (where the toggle is its own row).
|
||||
toggleName: string;
|
||||
// Description of the enable toggle/combined row
|
||||
toggleDesc: string;
|
||||
// Description of the "Property name" row on the global page.
|
||||
nameDesc: string;
|
||||
}
|
||||
|
||||
interface FieldCopy {
|
||||
name: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
interface StarCopy extends SimpleCopy {
|
||||
// Extra controls unique to the star scale.
|
||||
fields: {
|
||||
max: FieldCopy;
|
||||
allowZero: FieldCopy;
|
||||
mode: FieldCopy;
|
||||
decimals: FieldCopy;
|
||||
mapping: FieldCopy;
|
||||
};
|
||||
}
|
||||
|
||||
export const PROPERTY_NAME_LABEL = 'Property name';
|
||||
|
||||
export const FM_SIMPLE_COPY: Record<FmSimpleKey, SimpleCopy> = {
|
||||
rating: {
|
||||
label: 'Rating',
|
||||
toggleName: 'Write rating to frontmatter',
|
||||
toggleDesc: 'Write the current Glicko rating to a frontmatter property.',
|
||||
nameDesc: 'Frontmatter property used to store the rating.',
|
||||
},
|
||||
uncertainty: {
|
||||
label: 'Uncertainty',
|
||||
toggleName: 'Write uncertainty to frontmatter',
|
||||
toggleDesc:
|
||||
'Write how uncertain the rating is. Starts high and decreases as more comparisons are made.',
|
||||
nameDesc: 'Frontmatter property used to store the uncertainty.',
|
||||
},
|
||||
rank: {
|
||||
label: 'Rank',
|
||||
toggleName: 'Write rank to frontmatter',
|
||||
toggleDesc: 'Write the cohort rank (1 = highest) to a frontmatter property.',
|
||||
nameDesc: 'Frontmatter property used to store the rank.',
|
||||
},
|
||||
matches: {
|
||||
label: 'Matches',
|
||||
toggleName: 'Write match count to frontmatter',
|
||||
toggleDesc: 'Write the number of matches played to a frontmatter property.',
|
||||
nameDesc: 'Frontmatter property used to store the match count.',
|
||||
},
|
||||
wins: {
|
||||
label: 'Wins',
|
||||
toggleName: 'Write win count to frontmatter',
|
||||
toggleDesc: 'Write the number of wins to a frontmatter property.',
|
||||
nameDesc: 'Frontmatter property used to store the win count.',
|
||||
},
|
||||
};
|
||||
|
||||
export const FM_STARS_COPY: StarCopy = {
|
||||
label: 'Star rating',
|
||||
toggleName: 'Write a star rating to frontmatter',
|
||||
toggleDesc:
|
||||
"Write a derived n-star rating (e.g. 1-7) to frontmatter, based on each note's position in the cohort.",
|
||||
nameDesc: 'Frontmatter property used to store the star rating.',
|
||||
fields: {
|
||||
max: {
|
||||
name: 'Maximum stars',
|
||||
desc: 'The highest value on the scale (N).',
|
||||
},
|
||||
allowZero: {
|
||||
name: 'Allow zero',
|
||||
desc: 'Let the scale start at 0 instead of 1 (e.g. 0-5 rather than 1-5).',
|
||||
},
|
||||
mode: {
|
||||
name: 'Number type',
|
||||
desc: 'Write whole numbers, or allow fractional star ratings.',
|
||||
},
|
||||
decimals: {
|
||||
name: 'Decimal places',
|
||||
desc: 'Precision used for fractional star ratings.',
|
||||
},
|
||||
mapping: {
|
||||
name: 'Mapping',
|
||||
desc: 'By rating spreads notes according to their rating gaps; by rank spreads them evenly across the scale.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Option labels for the star scale dropdowns.
|
||||
export const STAR_MODE_LABELS: Record<StarScaleConfig['mode'], string> = {
|
||||
integer: 'Whole numbers',
|
||||
float: 'Decimals',
|
||||
};
|
||||
|
||||
export const STAR_MAPPING_LABELS: Record<StarMapping, string> = {
|
||||
rating: 'By rating (spread by score)',
|
||||
rank: 'By rank (even spread)',
|
||||
};
|
||||
|
|
@ -6,12 +6,29 @@ export interface FrontmatterPropertyConfig {
|
|||
enabled: boolean;
|
||||
}
|
||||
|
||||
// How a note's star value is derived from the cohort's pairwise results.
|
||||
// - 'rating': min-max normalise the Glicko rating between the lowest and
|
||||
// highest rated notes (preserves the size of rating gaps).
|
||||
// - 'rank': spread notes evenly across the scale by rank position.
|
||||
export type StarMapping = 'rating' | 'rank';
|
||||
|
||||
export interface StarScaleConfig {
|
||||
enabled: boolean;
|
||||
property: string;
|
||||
max: number;
|
||||
allowZero: boolean;
|
||||
mode: 'integer' | 'float';
|
||||
decimals: number; // decimal places used when mode === 'float'
|
||||
mapping: StarMapping;
|
||||
}
|
||||
|
||||
export interface FrontmatterPropertiesSettings {
|
||||
rating: FrontmatterPropertyConfig;
|
||||
uncertainty: FrontmatterPropertyConfig;
|
||||
rank: FrontmatterPropertyConfig;
|
||||
matches: FrontmatterPropertyConfig;
|
||||
wins: FrontmatterPropertyConfig;
|
||||
stars: StarScaleConfig;
|
||||
}
|
||||
|
||||
export interface SessionReportConfig {
|
||||
|
|
@ -47,6 +64,15 @@ export const DEFAULT_SETTINGS: GlickoSettings = {
|
|||
rank: { property: 'glickoRank', enabled: false },
|
||||
matches: { property: 'glickoMatches', enabled: false },
|
||||
wins: { property: 'glickoWins', enabled: false },
|
||||
stars: {
|
||||
enabled: false,
|
||||
property: 'stars',
|
||||
max: 7,
|
||||
allowZero: false,
|
||||
mode: 'integer',
|
||||
decimals: 1,
|
||||
mapping: 'rating',
|
||||
},
|
||||
},
|
||||
askForOverridesOnCohortCreation: true,
|
||||
askForReportSettingsOnCreation: true,
|
||||
|
|
@ -74,5 +100,19 @@ export function effectiveFrontmatterProperties(
|
|||
rank: overrides?.rank ?? base.rank,
|
||||
matches: overrides?.matches ?? base.matches,
|
||||
wins: overrides?.wins ?? base.wins,
|
||||
stars: overrides?.stars ?? base.stars,
|
||||
};
|
||||
}
|
||||
|
||||
// Used to decide whether a change warrants rewriting cohort frontmatter.
|
||||
export function starScaleConfigEquals(a: StarScaleConfig, b: StarScaleConfig): boolean {
|
||||
return (
|
||||
a.enabled === b.enabled &&
|
||||
a.property.trim() === b.property.trim() &&
|
||||
a.max === b.max &&
|
||||
a.allowZero === b.allowZero &&
|
||||
a.mode === b.mode &&
|
||||
a.decimals === b.decimals &&
|
||||
a.mapping === b.mapping
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,11 @@ function mergeSettings(raw?: Partial<GlickoSettings>): GlickoSettings {
|
|||
...raw.frontmatterProperties,
|
||||
};
|
||||
|
||||
out.frontmatterProperties.stars = {
|
||||
...DEFAULT_SETTINGS.frontmatterProperties.stars,
|
||||
...raw.frontmatterProperties?.stars,
|
||||
};
|
||||
|
||||
if (noIdPropertyName) {
|
||||
out.idPropertyName = 'eloId';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,15 +6,18 @@ import type {
|
|||
FrontmatterPropertiesSettings,
|
||||
FrontmatterPropertyConfig,
|
||||
SessionReportConfig,
|
||||
StarScaleConfig,
|
||||
} from '../settings';
|
||||
import { DEFAULT_SETTINGS } from '../settings';
|
||||
import { DEFAULT_SETTINGS, starScaleConfigEquals } from '../settings';
|
||||
import type { ScrollStartMode } from '../types';
|
||||
import type { FmPropKey } from './FrontmatterPropertyRow';
|
||||
import { FM_PROP_KEYS, renderStandardFmPropertyRow } from './FrontmatterPropertyRow';
|
||||
import { BasePromiseModal } from './PromiseModal';
|
||||
import { renderStarScaleSettings } from './StarScaleRow';
|
||||
|
||||
type Mode = 'create' | 'edit';
|
||||
|
||||
type Key = keyof FrontmatterPropertiesSettings;
|
||||
type Key = FmPropKey;
|
||||
|
||||
type RowState = {
|
||||
key: Key;
|
||||
|
|
@ -54,6 +57,7 @@ export class CohortOptionsModal extends BasePromiseModal<CohortOptionsResult | u
|
|||
private reportTemplatePath = '';
|
||||
|
||||
private working: Record<Key, RowState>;
|
||||
private starWorking: StarScaleConfig;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
|
|
@ -111,6 +115,8 @@ export class CohortOptionsModal extends BasePromiseModal<CohortOptionsResult | u
|
|||
matches: mk('matches'),
|
||||
wins: mk('wins'),
|
||||
};
|
||||
|
||||
this.starWorking = { ...(this.initial?.stars ?? this.base.stars) };
|
||||
}
|
||||
|
||||
async openAndGetOptions(): Promise<CohortOptionsResult | undefined> {
|
||||
|
|
@ -131,6 +137,9 @@ export class CohortOptionsModal extends BasePromiseModal<CohortOptionsResult | u
|
|||
out[key] = { property: row.property.trim(), enabled: !!row.enabled };
|
||||
}
|
||||
}
|
||||
if (!starScaleConfigEquals(this.starWorking, this.base.stars)) {
|
||||
out.stars = { ...this.starWorking, property: this.starWorking.property.trim() };
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
|
@ -141,7 +150,7 @@ export class CohortOptionsModal extends BasePromiseModal<CohortOptionsResult | u
|
|||
const desc =
|
||||
this.mode === 'create'
|
||||
? 'Set an optional name and configure which Glicko statistics to write into frontmatter for this cohort. Global defaults are prefilled.'
|
||||
: 'Rename the cohort and adjust which Glicko statistics to write into frontmatter. Use Reset to revert a property to the global default.';
|
||||
: 'Rename the cohort and adjust which Glicko statistics to write into frontmatter. Use the reset button to revert a property to the global default.';
|
||||
|
||||
contentEl.createEl('h3', { text: 'Cohort options' });
|
||||
contentEl.createEl('p', { text: desc });
|
||||
|
|
@ -234,9 +243,9 @@ export class CohortOptionsModal extends BasePromiseModal<CohortOptionsResult | u
|
|||
this.reportFolderPath = (v ?? '').trim();
|
||||
});
|
||||
})
|
||||
.addButton((b) =>
|
||||
.addExtraButton((b) =>
|
||||
b
|
||||
.setButtonText('Reset')
|
||||
.setIcon('reset')
|
||||
.setTooltip('Reset to global default')
|
||||
.onClick(() => {
|
||||
this.reportFolderPath = this.plugin.settings.sessionReport.folderPath;
|
||||
|
|
@ -256,9 +265,9 @@ export class CohortOptionsModal extends BasePromiseModal<CohortOptionsResult | u
|
|||
this.reportNameTemplate = (v ?? '').trim();
|
||||
});
|
||||
})
|
||||
.addButton((b) =>
|
||||
.addExtraButton((b) =>
|
||||
b
|
||||
.setButtonText('Reset')
|
||||
.setIcon('reset')
|
||||
.setTooltip('Reset to global default')
|
||||
.onClick(() => {
|
||||
this.reportNameTemplate = this.plugin.settings.sessionReport.nameTemplate;
|
||||
|
|
@ -280,9 +289,9 @@ export class CohortOptionsModal extends BasePromiseModal<CohortOptionsResult | u
|
|||
this.reportTemplatePath = (v ?? '').trim();
|
||||
});
|
||||
})
|
||||
.addButton((b) =>
|
||||
.addExtraButton((b) =>
|
||||
b
|
||||
.setButtonText('Reset')
|
||||
.setIcon('reset')
|
||||
.setTooltip('Reset to global default')
|
||||
.onClick(() => {
|
||||
this.reportTemplatePath = this.plugin.settings.sessionReport.reportTemplatePath ?? '';
|
||||
|
|
@ -315,6 +324,15 @@ export class CohortOptionsModal extends BasePromiseModal<CohortOptionsResult | u
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
renderStarScaleSettings(contentEl, {
|
||||
value: this.starWorking,
|
||||
base: this.base.stars,
|
||||
mode: 'cohort',
|
||||
onChange: (next) => {
|
||||
this.starWorking = { ...next };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (this.mode === 'create') {
|
||||
|
|
|
|||
|
|
@ -1,39 +1,11 @@
|
|||
import type { TextComponent, ToggleComponent } from 'obsidian';
|
||||
import { Setting } from 'obsidian';
|
||||
|
||||
export type FmPropKey = 'rating' | 'uncertainty' | 'rank' | 'matches' | 'wins';
|
||||
export const FM_PROP_KEYS: readonly FmPropKey[] = [
|
||||
'rating',
|
||||
'uncertainty',
|
||||
'rank',
|
||||
'matches',
|
||||
'wins',
|
||||
] as const;
|
||||
import type { FmSimpleKey } from '../settings/frontmatterCopy';
|
||||
import { FM_SIMPLE_COPY, FM_SIMPLE_KEYS } from '../settings/frontmatterCopy';
|
||||
|
||||
type Meta = { label: string; desc: string };
|
||||
|
||||
const META: Record<FmPropKey, Meta> = {
|
||||
rating: {
|
||||
label: 'Rating',
|
||||
desc: 'Write the current Glicko rating to this property.',
|
||||
},
|
||||
uncertainty: {
|
||||
label: 'Uncertainty',
|
||||
desc: 'Write how uncertain the rating is. Starts high and decreases as more comparisons are made.',
|
||||
},
|
||||
rank: {
|
||||
label: 'Rank',
|
||||
desc: 'Write the cohort rank (1 = highest) to this property.',
|
||||
},
|
||||
matches: {
|
||||
label: 'Matches',
|
||||
desc: 'Write the number of matches to this property.',
|
||||
},
|
||||
wins: {
|
||||
label: 'Wins',
|
||||
desc: 'Write the number of wins to this property.',
|
||||
},
|
||||
};
|
||||
export type FmPropKey = FmSimpleKey;
|
||||
export const FM_PROP_KEYS = FM_SIMPLE_KEYS;
|
||||
|
||||
export type FmRowValue = { enabled: boolean; property: string };
|
||||
export type FmRowRefs = { setting: Setting; text: TextComponent; toggle: ToggleComponent };
|
||||
|
|
@ -50,7 +22,7 @@ export function renderStandardFmPropertyRow(
|
|||
mode?: RowMode;
|
||||
},
|
||||
): FmRowRefs {
|
||||
const meta = META[key];
|
||||
const copy = FM_SIMPLE_COPY[key];
|
||||
const placeholder = opts.base.property || '';
|
||||
const mode: RowMode = opts.mode ?? 'cohort';
|
||||
|
||||
|
|
@ -63,8 +35,8 @@ export function renderStandardFmPropertyRow(
|
|||
let toggleRef!: ToggleComponent;
|
||||
|
||||
const setting = new Setting(parent)
|
||||
.setName(meta.label)
|
||||
.setDesc(meta.desc)
|
||||
.setName(copy.label)
|
||||
.setDesc(copy.toggleDesc)
|
||||
.addToggle((t) => {
|
||||
toggleRef = t;
|
||||
t.setValue(cur.enabled).onChange((v) => {
|
||||
|
|
@ -88,9 +60,9 @@ export function renderStandardFmPropertyRow(
|
|||
});
|
||||
|
||||
if (mode === 'cohort') {
|
||||
setting.addButton((b) =>
|
||||
setting.addExtraButton((b) =>
|
||||
b
|
||||
.setButtonText('Reset')
|
||||
.setIcon('reset')
|
||||
.setTooltip('Reset to global default')
|
||||
.onClick(() => {
|
||||
cur.enabled = !!opts.base.enabled;
|
||||
|
|
|
|||
167
src/ui/StarScaleRow.ts
Normal file
167
src/ui/StarScaleRow.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import { Setting } from 'obsidian';
|
||||
|
||||
import type { StarScaleConfig } from '../settings';
|
||||
import { DEFAULT_SETTINGS } from '../settings';
|
||||
import {
|
||||
FM_STARS_COPY,
|
||||
PROPERTY_NAME_LABEL,
|
||||
STAR_MAPPING_LABELS,
|
||||
STAR_MODE_LABELS,
|
||||
} from '../settings/frontmatterCopy';
|
||||
|
||||
type RowMode = 'global' | 'cohort';
|
||||
|
||||
// Imperatively render the Star rating configuration for the per-cohort options modal.
|
||||
// The global settings page renders the equivalent controls declaratively.
|
||||
export function renderStarScaleSettings(
|
||||
parent: HTMLElement,
|
||||
opts: {
|
||||
value: StarScaleConfig;
|
||||
base: StarScaleConfig;
|
||||
mode?: RowMode;
|
||||
onChange: (next: StarScaleConfig) => void | Promise<void>;
|
||||
},
|
||||
): void {
|
||||
const mode: RowMode = opts.mode ?? 'cohort';
|
||||
const cur: StarScaleConfig = { ...opts.value };
|
||||
const emit = () => void opts.onChange({ ...cur });
|
||||
|
||||
const defaultProp = opts.base.property || DEFAULT_SETTINGS.frontmatterProperties.stars.property;
|
||||
|
||||
const dependents: Setting[] = [];
|
||||
|
||||
const enableSetting = new Setting(parent)
|
||||
.setName(FM_STARS_COPY.label)
|
||||
.setDesc(FM_STARS_COPY.toggleDesc)
|
||||
.addToggle((t) =>
|
||||
t.setValue(cur.enabled).onChange((v) => {
|
||||
cur.enabled = !!v;
|
||||
refreshControls();
|
||||
emit();
|
||||
}),
|
||||
);
|
||||
|
||||
if (mode === 'cohort') {
|
||||
enableSetting.addExtraButton((b) =>
|
||||
b
|
||||
.setIcon('reset')
|
||||
.setTooltip('Reset to global default')
|
||||
.onClick(() => {
|
||||
Object.assign(cur, opts.base);
|
||||
rerender();
|
||||
emit();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const propSetting = new Setting(parent)
|
||||
.setName(PROPERTY_NAME_LABEL)
|
||||
.setDesc(FM_STARS_COPY.nameDesc)
|
||||
.addText((t) =>
|
||||
t
|
||||
.setPlaceholder(defaultProp)
|
||||
.setValue(cur.property)
|
||||
.onChange((v) => {
|
||||
const trimmed = (v ?? '').trim();
|
||||
cur.property = trimmed.length > 0 ? trimmed : defaultProp;
|
||||
emit();
|
||||
}),
|
||||
);
|
||||
dependents.push(propSetting);
|
||||
|
||||
const maxSetting = new Setting(parent)
|
||||
.setName(FM_STARS_COPY.fields.max.name)
|
||||
.setDesc(FM_STARS_COPY.fields.max.desc)
|
||||
.addText((t) => {
|
||||
t.inputEl.type = 'number';
|
||||
t.inputEl.min = '2';
|
||||
t.setValue(String(cur.max)).onChange((v) => {
|
||||
const n = Math.round(Number(v));
|
||||
if (Number.isFinite(n) && n >= 2) {
|
||||
cur.max = n;
|
||||
emit();
|
||||
}
|
||||
});
|
||||
});
|
||||
dependents.push(maxSetting);
|
||||
|
||||
const allowZeroSetting = new Setting(parent)
|
||||
.setName(FM_STARS_COPY.fields.allowZero.name)
|
||||
.setDesc(FM_STARS_COPY.fields.allowZero.desc)
|
||||
.addToggle((t) =>
|
||||
t.setValue(cur.allowZero).onChange((v) => {
|
||||
cur.allowZero = !!v;
|
||||
emit();
|
||||
}),
|
||||
);
|
||||
dependents.push(allowZeroSetting);
|
||||
|
||||
const modeSetting = new Setting(parent)
|
||||
.setName(FM_STARS_COPY.fields.mode.name)
|
||||
.setDesc(FM_STARS_COPY.fields.mode.desc)
|
||||
.addDropdown((dd) =>
|
||||
dd
|
||||
.addOptions(STAR_MODE_LABELS)
|
||||
.setValue(cur.mode)
|
||||
.onChange((v) => {
|
||||
cur.mode = v === 'float' ? 'float' : 'integer';
|
||||
refreshDecimalsVisibility();
|
||||
emit();
|
||||
}),
|
||||
);
|
||||
dependents.push(modeSetting);
|
||||
|
||||
const decimalsSetting = new Setting(parent)
|
||||
.setName(FM_STARS_COPY.fields.decimals.name)
|
||||
.setDesc(FM_STARS_COPY.fields.decimals.desc)
|
||||
.addText((t) => {
|
||||
t.inputEl.type = 'number';
|
||||
t.inputEl.min = '1';
|
||||
t.inputEl.max = '2';
|
||||
t.setValue(String(cur.decimals === 2 ? 2 : 1)).onChange((v) => {
|
||||
const n = Math.round(Number(v));
|
||||
if (n === 1 || n === 2) {
|
||||
cur.decimals = n;
|
||||
emit();
|
||||
}
|
||||
});
|
||||
});
|
||||
dependents.push(decimalsSetting);
|
||||
|
||||
const mappingSetting = new Setting(parent)
|
||||
.setName(FM_STARS_COPY.fields.mapping.name)
|
||||
.setDesc(FM_STARS_COPY.fields.mapping.desc)
|
||||
.addDropdown((dd) =>
|
||||
dd
|
||||
.addOptions(STAR_MAPPING_LABELS)
|
||||
.setValue(cur.mapping)
|
||||
.onChange((v) => {
|
||||
cur.mapping = v === 'rank' ? 'rank' : 'rating';
|
||||
emit();
|
||||
}),
|
||||
);
|
||||
dependents.push(mappingSetting);
|
||||
|
||||
refreshControls();
|
||||
|
||||
function refreshControls() {
|
||||
for (const s of dependents) {
|
||||
if (mode === 'cohort') {
|
||||
s.settingEl.toggle(cur.enabled);
|
||||
} else {
|
||||
s.setDisabled(!cur.enabled);
|
||||
}
|
||||
}
|
||||
refreshDecimalsVisibility();
|
||||
}
|
||||
|
||||
function refreshDecimalsVisibility() {
|
||||
decimalsSetting.settingEl.toggle(cur.enabled && cur.mode === 'float');
|
||||
}
|
||||
|
||||
function rerender() {
|
||||
enableSetting.settingEl.remove();
|
||||
for (const s of dependents) s.settingEl.remove();
|
||||
renderStarScaleSettings(parent, { ...opts, value: { ...cur } });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import type { App, TFile } from 'obsidian';
|
||||
|
||||
import type { FrontmatterPropertiesSettings } from '../settings';
|
||||
import type { FrontmatterPropertiesSettings, StarScaleConfig } from '../settings';
|
||||
import type { CohortData } from '../types';
|
||||
import { getNoteId } from './NoteIds';
|
||||
import { withNotice } from './safe';
|
||||
|
|
@ -11,6 +11,7 @@ type PlayerStats = {
|
|||
matches: number;
|
||||
wins: number;
|
||||
rank: number;
|
||||
stars?: number;
|
||||
};
|
||||
|
||||
function anyEnabled(fm: FrontmatterPropertiesSettings): boolean {
|
||||
|
|
@ -19,10 +20,79 @@ function anyEnabled(fm: FrontmatterPropertiesSettings): boolean {
|
|||
!!fm.uncertainty.enabled ||
|
||||
!!fm.rank.enabled ||
|
||||
!!fm.matches.enabled ||
|
||||
!!fm.wins.enabled
|
||||
!!fm.wins.enabled ||
|
||||
!!fm.stars.enabled
|
||||
);
|
||||
}
|
||||
|
||||
// Compute a derived n-star value for every rated note in the cohort
|
||||
export function computeStarsForAll(cohort: CohortData, cfg: StarScaleConfig): Map<string, number> {
|
||||
const map = new Map<string, number>();
|
||||
const rated = Object.entries(cohort.players)
|
||||
.filter(([, p]) => p.matches >= 1)
|
||||
.map(([id, p]) => ({ id, rating: p.rating }));
|
||||
if (rated.length === 0) return map;
|
||||
|
||||
const floor = cfg.allowZero ? 0 : 1;
|
||||
const top = cfg.max;
|
||||
const range = top - floor;
|
||||
|
||||
const quantize = (value: number): number => {
|
||||
const clamped = Math.min(top, Math.max(floor, value));
|
||||
if (cfg.mode === 'integer') return Math.round(clamped);
|
||||
const places = Math.max(0, Math.min(6, Math.floor(cfg.decimals ?? 1)));
|
||||
const factor = Math.pow(10, places);
|
||||
return Math.round(clamped * factor) / factor;
|
||||
};
|
||||
|
||||
// Degenerate scale: nothing meaningful to spread across
|
||||
if (range <= 0) {
|
||||
for (const { id } of rated) map.set(id, quantize(floor));
|
||||
return map;
|
||||
}
|
||||
|
||||
const midpoint = floor + range / 2;
|
||||
|
||||
if (cfg.mapping === 'rank') {
|
||||
// Even spread by rank position (1 = best). Standard competition ranking,
|
||||
// so tied ratings share the higher rank.
|
||||
const sorted = [...rated].sort((a, b) => b.rating - a.rating);
|
||||
const m = sorted.length;
|
||||
if (m === 1) {
|
||||
map.set(sorted[0].id, quantize(midpoint));
|
||||
return map;
|
||||
}
|
||||
let lastRating: number | undefined;
|
||||
let rank = 0;
|
||||
for (let i = 0; i < m; i++) {
|
||||
const { id, rating } = sorted[i];
|
||||
if (lastRating === undefined || rating !== lastRating) {
|
||||
rank = i + 1;
|
||||
lastRating = rating;
|
||||
}
|
||||
const t = (m - rank) / (m - 1); // best -> 1, worst -> 0
|
||||
map.set(id, quantize(floor + t * range));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
let min = Infinity;
|
||||
let max = -Infinity;
|
||||
for (const { rating } of rated) {
|
||||
if (rating < min) min = rating;
|
||||
if (rating > max) max = rating;
|
||||
}
|
||||
if (max === min) {
|
||||
for (const { id } of rated) map.set(id, quantize(midpoint));
|
||||
return map;
|
||||
}
|
||||
for (const { id, rating } of rated) {
|
||||
const t = (rating - min) / (max - min);
|
||||
map.set(id, quantize(floor + t * range));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// Standard competition ranking ("1224" style)
|
||||
export function computeRanksForAll(cohort: CohortData): Map<string, number> {
|
||||
const entries = Object.entries(cohort.players);
|
||||
|
|
@ -95,6 +165,9 @@ function buildProps(fm: FrontmatterPropertiesSettings, stats: PlayerStats): Reco
|
|||
if (fm.wins.enabled && fm.wins.property) {
|
||||
out[fm.wins.property] = stats.wins;
|
||||
}
|
||||
if (fm.stars.enabled && fm.stars.property && typeof stats.stars === 'number') {
|
||||
out[fm.stars.property] = stats.stars;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
|
@ -122,13 +195,17 @@ export async function writeFrontmatterStatsForPair(
|
|||
|
||||
const ids = [aId, bId].filter((id): id is string => !!id);
|
||||
const rankMap = computeRanksForSubset(cohort, ids);
|
||||
// Stars depend on the cohort-wide min/max (or full ranking), so compute the
|
||||
// whole map and look up the pair. Other notes are refreshed at session end.
|
||||
const starMap =
|
||||
fm.stars.enabled && fm.stars.property ? computeStarsForAll(cohort, fm.stars) : undefined;
|
||||
const tasks: Promise<void>[] = [];
|
||||
|
||||
if (aFile && aId) {
|
||||
tasks.push(writeFrontmatterStatsForPlayer(app, fm, cohort, aFile, aId, rankMap));
|
||||
tasks.push(writeFrontmatterStatsForPlayer(app, fm, cohort, aFile, aId, rankMap, starMap));
|
||||
}
|
||||
if (bFile && bId) {
|
||||
tasks.push(writeFrontmatterStatsForPlayer(app, fm, cohort, bFile, bId, rankMap));
|
||||
tasks.push(writeFrontmatterStatsForPlayer(app, fm, cohort, bFile, bId, rankMap, starMap));
|
||||
}
|
||||
|
||||
await Promise.all(tasks);
|
||||
|
|
@ -291,15 +368,41 @@ export async function updateCohortFrontmatter(
|
|||
);
|
||||
}
|
||||
|
||||
export async function updateCohortRanksInFrontmatter(
|
||||
// Refresh the rank and star properties across an entire cohort.
|
||||
export async function refreshCohortRankAndStars(
|
||||
app: App,
|
||||
cohort: CohortData | undefined,
|
||||
fm: FrontmatterPropertiesSettings,
|
||||
cohort: CohortData,
|
||||
files: TFile[],
|
||||
newPropName: string,
|
||||
): Promise<{ updated: number }> {
|
||||
if (!cohort) return { updated: 0 };
|
||||
const rankMap = computeRanksForAll(cohort);
|
||||
return updateCohortFrontmatterProperties(app, files, rankMap, newPropName);
|
||||
idPropertyName?: string,
|
||||
rankMap?: Map<string, number>,
|
||||
): Promise<void> {
|
||||
const wantRank = !!fm.rank.enabled && !!fm.rank.property;
|
||||
const wantStars = !!fm.stars.enabled && !!fm.stars.property;
|
||||
if ((!wantRank && !wantStars) || files.length === 0) return;
|
||||
|
||||
if (wantRank) {
|
||||
await updateCohortFrontmatter(
|
||||
app,
|
||||
files,
|
||||
rankMap ?? computeRanksForAll(cohort),
|
||||
fm.rank.property,
|
||||
undefined,
|
||||
'Updating ranks...',
|
||||
idPropertyName,
|
||||
);
|
||||
}
|
||||
if (wantStars) {
|
||||
await updateCohortFrontmatter(
|
||||
app,
|
||||
files,
|
||||
computeStarsForAll(cohort, fm.stars),
|
||||
fm.stars.property,
|
||||
undefined,
|
||||
'Updating star ratings...',
|
||||
idPropertyName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeFrontmatterStatsForPlayer(
|
||||
|
|
@ -309,6 +412,7 @@ export async function writeFrontmatterStatsForPlayer(
|
|||
file: TFile,
|
||||
playerId: string,
|
||||
precomputedRankMap: Map<string, number>,
|
||||
starMap?: Map<string, number>,
|
||||
): Promise<void> {
|
||||
if (!anyEnabled(fm)) return;
|
||||
const p = cohort.players[playerId];
|
||||
|
|
@ -320,6 +424,7 @@ export async function writeFrontmatterStatsForPlayer(
|
|||
matches: p.matches,
|
||||
wins: p.wins,
|
||||
rank: rankMap.get(playerId) ?? rankMap.size,
|
||||
stars: starMap?.get(playerId),
|
||||
});
|
||||
await writeProps(app, file, props);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue