Add Daily Counts #11

This commit is contained in:
Luke Leppan 2021-07-08 22:20:38 +02:00
parent 7f817bd5bd
commit 5aa51986fe
6 changed files with 262 additions and 114 deletions

View file

@ -2,25 +2,26 @@
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/lukeleppan/better-word-count/Build%20Release?logo=github&style=for-the-badge) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/lukeleppan/better-word-count?style=for-the-badge) ![GitHub All Releases](https://img.shields.io/github/downloads/lukeleppan/better-word-count/total?style=for-the-badge) ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/lukeleppan/better-word-count/Build%20Release?logo=github&style=for-the-badge) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/lukeleppan/better-word-count?style=for-the-badge) ![GitHub All Releases](https://img.shields.io/github/downloads/lukeleppan/better-word-count/total?style=for-the-badge)
This plugin is the same as the built-in **Word Count** plugin, except when you select text, it will count the selected word instead of the whole document. I recommend turning off the built-in **Word Count** because this plugin is designed to replace that. This plugin is the same as the built-in **Word Count** plugin, except when you select text, it will count the selected word instead of the whole document. I recommend turning off the built-in **Word Count** because this plugin is designed to replace that. This plugin also has the ability to store statistics about your vault.
![Better Count Word](https://raw.githubusercontent.com/lukeleppan/better-word-count/master/assets/better-word-count.gif) ![Better Count Word](https://raw.githubusercontent.com/lukeleppan/better-word-count/master/assets/better-word-count.gif)
### Features ### Features
All Optional: - Allows you to store statistics about your vault.
- Works with all languages.
- Show word count (total when no text selected). - Can display a variety of different stats. Including:
- Show character count. - Words, Characters and Sentences in current file.
- Show sentence count. - Total Words, Characters and Sentences in vault.
- Show file count (on non-markdown windows). - Words, Characters and Sentences typed today.
- Total Files in vault.
Works with all languages. - Highly Customizable status bar that can be adapted to your needs.
#### TODO: #### TODO:
- [ ] more statistic display - [ ] add statistic view
- [ ] add more statistics (make suggestions) - [ ] add more statistics (make suggestions)
- [ ] add goals
### Contributors ### Contributors
@ -36,3 +37,6 @@ Works with all languages.
- @aproximate - @aproximate
- @Quorafind - @Quorafind
- @torantine - @torantine
- @lammersma
- @aknighty74
- @dhruvik7

View file

@ -1,6 +1,6 @@
import moment from "moment"; import moment from "moment";
import type { MetadataCache, TFile, Vault } from "obsidian"; import type { MetadataCache, TFile, Vault } from "obsidian";
import { getCharacterCount, getWordCount } from "./stats"; import { getCharacterCount, getSentenceCount, getWordCount } from "./stats";
export class DataCollector { export class DataCollector {
private vault: Vault; private vault: Vault;
@ -42,7 +42,7 @@ export class DataCollector {
const files = this.vault.getFiles(); const files = this.vault.getFiles();
for (const i in files) { for (const i in files) {
const file = files[i]; const file = files[i];
sentence += getCharacterCount(await this.vault.cachedRead(file)); sentence += getSentenceCount(await this.vault.cachedRead(file));
} }
return sentence; return sentence;

View file

@ -1,14 +1,44 @@
import moment from "moment"; import moment from "moment";
import type { MetadataCache, TAbstractFile, Vault } from "obsidian"; import type { MetadataCache, TFile, Vault } from "obsidian";
import { STATS_FILE } from "src/constants"; import { STATS_FILE } from "src/constants";
import { DataCollector } from "./collector"; import { DataCollector } from "./collector";
import { getCharacterCount, getSentenceCount, getWordCount } from "./stats";
type History = Record<string, Day>;
interface Day {
files: number;
modifiedFiles: ModFiles;
words: number;
characters: number;
sentences: number;
totalWords: number;
totalCharacters: number;
totalSentences: number;
}
type ModFiles = Record<string, FileStats>;
type FileStats = Record<number, Count>;
interface Count {
initial: number;
current: number;
}
export interface TodayCounts {
words: number;
characters: number;
sentences: number;
}
export class DataManager { export class DataManager {
private vault: Vault; private vault: Vault;
private metadataCache: MetadataCache; private metadataCache: MetadataCache;
private stats: any; private history: History;
private index: number; private today: string;
private collector: DataCollector; private collector: DataCollector;
private todayCounts: TodayCounts;
constructor(vault: Vault, metadataCache: MetadataCache) { constructor(vault: Vault, metadataCache: MetadataCache) {
this.vault = vault; this.vault = vault;
@ -17,71 +47,127 @@ export class DataManager {
this.vault.adapter.exists(".vault-stats").then(async (exists) => { this.vault.adapter.exists(".vault-stats").then(async (exists) => {
if (!exists) { if (!exists) {
const json: string = JSON.stringify({ this.vault.adapter.write(".vault-stats", "{}");
history: [],
});
this.vault.adapter.write(".vault-stats", json);
} }
this.stats = JSON.parse(await this.vault.adapter.read(".vault-stats")); this.history = Object.assign(
this.getTodayIndex(); JSON.parse(await this.vault.adapter.read(".vault-stats"))
);
this.updateToday();
this.update(); this.update();
}); });
} }
async update(): Promise<void> { async update(): Promise<void> {
this.vault.adapter.write(STATS_FILE, JSON.stringify(this.stats)); this.vault.adapter.write(STATS_FILE, JSON.stringify(this.history));
} }
getTodayIndex(): void { async updateToday(): Promise<void> {
const length: number = this.stats.history.length; const newDay: Day = {
files: this.collector.getTotalFileCount(),
modifiedFiles: {},
words: 0,
characters: 0,
sentences: 0,
totalWords: await this.collector.getTotalWordCount(),
totalCharacters: await this.collector.getTotalCharacterCount(),
totalSentences: await this.collector.getTotalSentenceCount(),
};
if (length === 0) { if (!this.history.hasOwnProperty(moment().format("YYYY-MM-DD"))) {
this.index = this.history[moment().format("YYYY-MM-DD")] = newDay;
this.stats.history.push({ }
date: moment().format("YYYY-MM-DD"),
initFiles: 0, this.today = moment().format("YYYY-MM-DD");
finalFiles: 0,
modifiedFiles: [], this.update();
words: 0, }
characters: 0,
sentences: 0, async setTotalStats() {
totalWords: 0, this.history[this.today].files = this.collector.getTotalFileCount();
totalCharacters: 0, this.history[this.today].totalWords =
totalSentences: 0, await this.collector.getTotalWordCount();
}) - 1; this.history[this.today].totalCharacters =
} else if ( await this.collector.getTotalCharacterCount();
this.stats.history[this.stats.history.length - 1].date === this.history[this.today].totalSentences =
moment().format("YYYY-MM-DD") await this.collector.getTotalSentenceCount();
this.update();
}
change(file: TFile, data: string) {
const currentWords = getWordCount(data);
const currentCharacters = getCharacterCount(data);
const currentSentences = getSentenceCount(data);
if (
this.history.hasOwnProperty(this.today) &&
this.today === moment().format("YYYY-MM-DD")
) { ) {
this.index = this.stats.history.length - 1; if (!this.history[this.today].modifiedFiles.hasOwnProperty(file.path)) {
} else { const newWordCount: Count = {
this.index = initial: currentWords,
this.stats.history.push({ current: currentWords,
date: moment().format("YYYY-MM-DD"), };
initFiles: 0, const newCharacterCount: Count = {
finalFiles: 0, initial: currentCharacters,
modifiedFiles: [], current: currentCharacters,
words: 0, };
characters: 0, const newSentenceCount: Count = {
sentences: 0, initial: currentSentences,
totalWords: 0, current: currentSentences,
totalCharacters: 0, };
totalSentences: 0, const fileStats: FileStats = {
}) - 1; 0: newWordCount,
} 1: newCharacterCount,
} 2: newSentenceCount,
};
onVaultModify(file: TAbstractFile) { this.history[this.today].modifiedFiles[file.path] = fileStats;
if (!this.stats.history[this.index].modifiedFiles.includes(file.name)) { } else {
this.stats.history[this.index].modifiedFiles.push(file.name); this.history[this.today].modifiedFiles[file.path][0].current =
currentWords;
this.history[this.today].modifiedFiles[file.path][1].current =
currentCharacters;
this.history[this.today].modifiedFiles[file.path][2].current =
currentSentences;
}
this.updateTodayCounts();
this.update(); this.update();
} else {
this.updateToday();
} }
} }
change(cm: CodeMirror.Editor) {} updateTodayCounts() {
const words = Object.values(this.history[this.today].modifiedFiles)
.map((counts) => Math.max(0, counts[0].current - counts[0].initial))
.reduce((a, b) => a + b, 0);
const characters = Object.values(this.history[this.today].modifiedFiles)
.map((counts) => Math.max(0, counts[1].current - counts[1].initial))
.reduce((a, b) => a + b, 0);
const sentences = Object.values(this.history[this.today].modifiedFiles)
.map((counts) => Math.max(0, counts[2].current - counts[2].initial))
.reduce((a, b) => a + b, 0);
setTotalStats() {} this.history[this.today].words = words;
this.history[this.today].characters = characters;
this.history[this.today].sentences = sentences;
this.todayCounts = {
words: words,
characters: characters,
sentences: sentences,
};
}
getTodayCounts(): TodayCounts {
return this.todayCounts;
}
async updateFromFile() {
this.history = Object.assign(
JSON.parse(await this.vault.adapter.read(".vault-stats"))
);
}
} }

View file

@ -3,7 +3,7 @@ import { BetterWordCountSettingsTab } from "./settings/settings-tab";
import { BetterWordCountSettings, DEFAULT_SETTINGS } from "./settings/settings"; import { BetterWordCountSettings, DEFAULT_SETTINGS } from "./settings/settings";
import { StatusBar } from "./status/bar"; import { StatusBar } from "./status/bar";
import { STATS_ICON, STATS_ICON_NAME, VIEW_TYPE_STATS } from "./constants"; import { STATS_ICON, STATS_ICON_NAME, VIEW_TYPE_STATS } from "./constants";
import StatsView from "./view/view"; // import StatsView from "./view/view";
import { DataManager } from "./data/manager"; import { DataManager } from "./data/manager";
import { BarManager } from "./status/manager"; import { BarManager } from "./status/manager";
import type CodeMirror from "codemirror"; import type CodeMirror from "codemirror";
@ -12,7 +12,7 @@ export default class BetterWordCount extends Plugin {
public statusBar: StatusBar; public statusBar: StatusBar;
public currentFile: TFile; public currentFile: TFile;
public settings: BetterWordCountSettings; public settings: BetterWordCountSettings;
public view: StatsView; // public view: StatsView;
public dataManager: DataManager; public dataManager: DataManager;
public barManager: BarManager; public barManager: BarManager;
@ -24,13 +24,10 @@ export default class BetterWordCount extends Plugin {
async onload() { async onload() {
this.settings = Object.assign(DEFAULT_SETTINGS, await this.loadData()); this.settings = Object.assign(DEFAULT_SETTINGS, await this.loadData());
this.saveData(this.settings);
this.addSettingTab(new BetterWordCountSettingsTab(this.app, this)); this.addSettingTab(new BetterWordCountSettingsTab(this.app, this));
let statusBarEl = this.addStatusBarItem(); let statusBarEl = this.addStatusBarItem();
this.statusBar = new StatusBar(statusBarEl); this.statusBar = new StatusBar(statusBarEl);
this.dataManager = new DataManager(this.app.vault, this.app.metadataCache);
this.barManager = new BarManager( this.barManager = new BarManager(
this.statusBar, this.statusBar,
this.settings, this.settings,
@ -38,16 +35,15 @@ export default class BetterWordCount extends Plugin {
this.app.metadataCache this.app.metadataCache
); );
this.registerEvent( if (this.settings.collectStats) {
this.app.workspace.on("active-leaf-change", this.activeLeafChange, this) this.dataManager = new DataManager(
); this.app.vault,
this.app.metadataCache
);
}
this.registerEvent( this.registerEvent(
this.app.vault.on( this.app.workspace.on("active-leaf-change", this.activeLeafChange, this)
"modify",
this.dataManager.onVaultModify,
this.dataManager
)
); );
this.registerCodeMirror((cm: CodeMirror.Editor) => { this.registerCodeMirror((cm: CodeMirror.Editor) => {
@ -56,27 +52,45 @@ export default class BetterWordCount extends Plugin {
); );
}); });
addIcon(STATS_ICON_NAME, STATS_ICON); if (this.settings.collectStats) {
this.registerEvent(
this.app.workspace.on(
"quick-preview",
this.dataManager.change,
this.dataManager
)
);
this.addCommand({ this.registerInterval(
id: "show-vault-stats-view", window.setInterval(() => {
name: "Open Statistics", this.dataManager.setTotalStats();
checkCallback: (checking: boolean) => { }, 1000 * 60)
if (checking) { );
return this.app.workspace.getLeavesOfType("vault-stats").length === 0;
}
this.initLeaf();
},
});
this.registerView(
VIEW_TYPE_STATS,
(leaf: WorkspaceLeaf) => (this.view = new StatsView(leaf))
);
if (this.app.workspace.layoutReady) {
this.initLeaf();
} }
// addIcon(STATS_ICON_NAME, STATS_ICON);
// this.addCommand({
// id: "show-vault-stats-view",
// name: "Open Statistics",
// checkCallback: (checking: boolean) => {
// if (checking) {
// return this.app.workspace.getLeavesOfType("vault-stats").length === 0;
// }
// this.initLeaf();
// },
// });
// this.registerView(
// VIEW_TYPE_STATS,
// (leaf: WorkspaceLeaf) => (this.view = new StatsView(leaf))
// );
// if (this.app.workspace.layoutReady) {
// this.initLeaf();
// } else {
// this.app.workspace.onLayoutReady(() => this.initLeaf());
// }
} }
activeLeafChange(leaf: WorkspaceLeaf) { activeLeafChange(leaf: WorkspaceLeaf) {
@ -89,12 +103,12 @@ export default class BetterWordCount extends Plugin {
await this.saveData(this.settings); await this.saveData(this.settings);
} }
initLeaf(): void { // initLeaf(): void {
if (this.app.workspace.getLeavesOfType(VIEW_TYPE_STATS).length) { // if (this.app.workspace.getLeavesOfType(VIEW_TYPE_STATS).length) {
return; // return;
} // }
this.app.workspace.getRightLeaf(false).setViewState({ // this.app.workspace.getRightLeaf(false).setViewState({
type: VIEW_TYPE_STATS, // type: VIEW_TYPE_STATS,
}); // });
} // }
} }

View file

@ -29,7 +29,7 @@ export class BetterWordCountSettingsTab extends PluginSettingTab {
new Setting(containerEl) new Setting(containerEl)
.setName("Collect Statistics") .setName("Collect Statistics")
.setDesc( .setDesc(
"Turn on to start collecting daily statistics of your writing. Stored in the .vault-stats file in the root of your vault." "Reload Required for change to take effect. Turn on to start collecting daily statistics of your writing. Stored in the .vault-stats file in the root of your vault. This is required for counts of the day."
) )
.addToggle((cb: ToggleComponent) => { .addToggle((cb: ToggleComponent) => {
cb.setValue(this.plugin.settings.collectStats); cb.setValue(this.plugin.settings.collectStats);
@ -39,8 +39,8 @@ export class BetterWordCountSettingsTab extends PluginSettingTab {
}); });
}); });
new Setting(containerEl) new Setting(containerEl)
.setName("Count Comments") .setName("Don't Count Comments")
.setDesc("Turn off if you don't want markdown comments to be counted.") .setDesc("Turn on if you don't want markdown comments to be counted.")
.addToggle((cb: ToggleComponent) => { .addToggle((cb: ToggleComponent) => {
cb.setValue(this.plugin.settings.countComments); cb.setValue(this.plugin.settings.countComments);
cb.onChange(async (value: boolean) => { cb.onChange(async (value: boolean) => {
@ -76,6 +76,12 @@ export class BetterWordCountSettingsTab extends PluginSettingTab {
.addTextArea((cb: TextAreaComponent) => { .addTextArea((cb: TextAreaComponent) => {
cb.setPlaceholder("Enter an expression..."); cb.setPlaceholder("Enter an expression...");
cb.setValue(this.plugin.settings.statusBarQuery); cb.setValue(this.plugin.settings.statusBarQuery);
cb.onChange((value: string) => {
let newPreset = PRESETS.find((preset) => preset.name === "custom");
this.plugin.settings.preset = newPreset;
this.plugin.settings.statusBarQuery = value;
this.plugin.saveSettings();
});
}); });
new Setting(containerEl) new Setting(containerEl)
.setName("Alternative Status Bar Text") .setName("Alternative Status Bar Text")
@ -84,6 +90,8 @@ export class BetterWordCountSettingsTab extends PluginSettingTab {
cb.setPlaceholder("Enter an expression..."); cb.setPlaceholder("Enter an expression...");
cb.setValue(this.plugin.settings.statusBarAltQuery); cb.setValue(this.plugin.settings.statusBarAltQuery);
cb.onChange((value: string) => { cb.onChange((value: string) => {
let newPreset = PRESETS.find((preset) => preset.name === "custom");
this.plugin.settings.preset = newPreset;
this.plugin.settings.statusBarAltQuery = value; this.plugin.settings.statusBarAltQuery = value;
this.plugin.saveSettings(); this.plugin.saveSettings();
}); });

View file

@ -1,5 +1,7 @@
import type { MetadataCache, Vault } from "obsidian"; import type { MetadataCache, Vault } from "obsidian";
import { DataCollector } from "src/data/collector"; import { DataCollector } from "src/data/collector";
import { DataManager } from "src/data/manager";
import type { TodayCounts } from "src/data/manager";
import type { BetterWordCountSettings } from "src/settings/settings"; import type { BetterWordCountSettings } from "src/settings/settings";
import { import {
getWordCount, getWordCount,
@ -15,6 +17,7 @@ export class BarManager {
private settings: BetterWordCountSettings; private settings: BetterWordCountSettings;
private vault: Vault; private vault: Vault;
private dataCollector: DataCollector; private dataCollector: DataCollector;
private dataManager: DataManager;
constructor( constructor(
statusBar: StatusBar, statusBar: StatusBar,
@ -26,15 +29,21 @@ export class BarManager {
this.settings = settings; this.settings = settings;
this.vault = vault; this.vault = vault;
this.dataCollector = new DataCollector(vault, metadataCache); this.dataCollector = new DataCollector(vault, metadataCache);
this.dataManager = new DataManager(vault, metadataCache);
} }
async updateStatusBar(text: string): Promise<void> { async updateStatusBar(text: string): Promise<void> {
let newText = ""; let newText = "";
const expression: Expression = parse(this.settings.statusBarQuery); const expression: Expression = parse(this.settings.statusBarQuery);
if (this.settings.collectStats) this.dataManager.updateTodayCounts();
const todayCounts: TodayCounts = this.settings.collectStats
? this.dataManager.getTodayCounts()
: { words: 0, characters: 0, sentences: 0 };
let varsIndex = 0; let varsIndex = 0;
expression.parsed.forEach((value: string) => { for (const i in expression.parsed) {
newText = newText + value; const e = expression.parsed[i];
newText = newText + e;
switch (expression.vars[varsIndex]) { switch (expression.vars[varsIndex]) {
case 0: case 0:
newText = newText + getWordCount(text); newText = newText + getWordCount(text);
@ -46,20 +55,31 @@ export class BarManager {
newText = newText + getSentenceCount(text); newText = newText + getSentenceCount(text);
break; break;
case 3: case 3:
newText = newText + 0; newText = newText + (await this.dataCollector.getTotalWordCount());
break; break;
case 4: case 4:
newText = newText + 0; newText =
newText + (await this.dataCollector.getTotalCharacterCount());
break; break;
case 5: case 5:
newText = newText + 0; newText =
newText + (await this.dataCollector.getTotalCharacterCount());
break; break;
case 6: case 6:
newText = newText + 0; newText = newText + this.dataCollector.getTotalFileCount();
break;
case 7:
newText = newText + todayCounts.words;
break;
case 8:
newText = newText + todayCounts.characters;
break;
case 9:
newText = newText + todayCounts.sentences;
break; break;
} }
varsIndex++; varsIndex++;
}); }
this.statusBar.displayText(newText); this.statusBar.displayText(newText);
} }
@ -67,6 +87,10 @@ export class BarManager {
async updateAltStatusBar(): Promise<void> { async updateAltStatusBar(): Promise<void> {
let newText = ""; let newText = "";
const expression: Expression = parse(this.settings.statusBarAltQuery); const expression: Expression = parse(this.settings.statusBarAltQuery);
if (this.settings.collectStats) this.dataManager.updateTodayCounts();
const todayCounts: TodayCounts = this.settings.collectStats
? this.dataManager.getTodayCounts()
: { words: 0, characters: 0, sentences: 0 };
let varsIndex = 0; let varsIndex = 0;
for (const i in expression.parsed) { for (const i in expression.parsed) {
@ -96,6 +120,15 @@ export class BarManager {
case 6: case 6:
newText = newText + this.dataCollector.getTotalFileCount(); newText = newText + this.dataCollector.getTotalFileCount();
break; break;
case 7:
newText = newText + todayCounts.words;
break;
case 8:
newText = newText + todayCounts.characters;
break;
case 9:
newText = newText + todayCounts.sentences;
break;
} }
varsIndex++; varsIndex++;
} }
@ -111,6 +144,9 @@ export class BarManager {
this.updateStatusBar(cm.getSelection()); this.updateStatusBar(cm.getSelection());
} }
} else { } else {
if (this.settings.collectStats) {
this.dataManager.updateFromFile();
}
if (this.settings.countComments) { if (this.settings.countComments) {
this.updateStatusBar(cleanComments(cm.getValue())); this.updateStatusBar(cleanComments(cm.getValue()));
} else { } else {