├── .npmrc
├── .eslintignore
├── .husky
└── pre-commit
├── src
├── InternalCounts.ts
├── obsidian.d.ts
├── commands.ts
├── reset-progress-modal
│ ├── ResetProgressModal.svelte
│ └── ResetProgressModal.ts
├── markdownHelpers.ts
├── achievements-view
│ ├── AchievementsView.svelte
│ └── AchievementsView.ts
├── settings-tab
│ ├── SettingsTab.ts
│ └── SettingsTab.svelte
├── array-utils
│ └── sort.ts
├── settings.ts
├── __tests__
│ └── markdownHelpers.test.ts
├── seededAchievements.ts
└── main.ts
├── vitest.config.ts
├── .editorconfig
├── versions.json
├── manifest.json
├── styles.css
├── .gitignore
├── tsconfig.json
├── version-bump.mjs
├── .github
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── FUNDING.yml
└── workflows
│ ├── test.yaml
│ └── release.yaml
├── .eslintrc.js
├── LICENSE
├── esbuild.config.mjs
├── package.json
└── README.md
/.npmrc:
--------------------------------------------------------------------------------
1 | tag-version-prefix=""
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | npm node_modules
2 | build
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npm run typecheck
5 | npm run lint
--------------------------------------------------------------------------------
/src/InternalCounts.ts:
--------------------------------------------------------------------------------
1 | export interface InternalCounts {
2 | noteCount: number;
3 | internalLinkCount: number;
4 | tagCount: number;
5 | }
6 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitest/config";
2 |
3 | export default defineConfig({
4 | test: {
5 | // ...
6 | },
7 | });
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # top-most EditorConfig file
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | insert_final_newline = true
8 | indent_style = tab
9 | indent_size = 4
10 | tab_width = 4
11 |
--------------------------------------------------------------------------------
/versions.json:
--------------------------------------------------------------------------------
1 | {
2 | "0.0.1": "0.15.0",
3 | "0.0.2": "0.15.0",
4 | "0.0.3": "0.15.0",
5 | "0.0.4": "0.15.0",
6 | "0.0.5": "0.15.0",
7 | "0.0.6": "0.15.0",
8 | "0.0.7": "0.15.0",
9 | "0.0.8": "0.15.0",
10 | "0.0.9": "0.15.0",
11 | "0.0.10": "0.15.0"
12 | }
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "obsidian-achievements",
3 | "name": "Achievements",
4 | "version": "0.0.10",
5 | "minAppVersion": "0.15.0",
6 | "description": "Add achievements to Obsidian.",
7 | "author": "Zachatoo",
8 | "authorUrl": "https://zachyoung.dev",
9 | "fundingUrl": "https://github.com/sponsors/Zachatoo",
10 | "isDesktopOnly": false
11 | }
12 |
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | .achievements-plugin__reset-modal__button-container {
2 | display: flex;
3 | justify-content: end;
4 | gap: 0.5rem;
5 | }
6 |
7 | .achievements-plugin__progress {
8 | position: relative;
9 | }
10 |
11 | .achievements-plugin__progress::before {
12 | content: attr(value) "/" attr(max);
13 | position: absolute;
14 | top: -20px;
15 | left: 0px;
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # vscode
2 | .vscode
3 |
4 | # Intellij
5 | *.iml
6 | .idea
7 |
8 | # npm
9 | node_modules
10 |
11 | # Don't include the compiled main.js file in the repo.
12 | # They should be uploaded to GitHub releases instead.
13 | main.js
14 |
15 | # Exclude sourcemaps
16 | *.map
17 |
18 | # obsidian
19 | data.json
20 |
21 | # Exclude macOS Finder (System Explorer) View States
22 | .DS_Store
23 |
--------------------------------------------------------------------------------
/src/obsidian.d.ts:
--------------------------------------------------------------------------------
1 | import "obsidian";
2 |
3 | declare module "obsidian" {
4 | interface App {
5 | commands: Commands;
6 | }
7 |
8 | interface Commands {
9 | executeCommand: (command: Command) => boolean;
10 | }
11 |
12 | interface MetadataCache {
13 | getTags: () => { [key: string]: number };
14 | iterateReferences: (
15 | callback: (sourcePath: string, reference: ReferenceCache) => any
16 | ) => any;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/svelte/tsconfig.json",
3 | "compilerOptions": {
4 | "types": ["svelte", "node"],
5 | "baseUrl": ".",
6 | "inlineSources": true,
7 | "module": "ESNext",
8 | "target": "ES6",
9 | "allowJs": true,
10 | "noImplicitAny": true,
11 | "moduleResolution": "node",
12 | "importHelpers": true,
13 | "isolatedModules": true,
14 | "strictNullChecks": true,
15 | "lib": ["DOM", "ES5", "ES6", "ES7"]
16 | },
17 | "include": ["**/*.ts"]
18 | }
19 |
--------------------------------------------------------------------------------
/src/commands.ts:
--------------------------------------------------------------------------------
1 | import { around } from "monkey-around";
2 | import type { Command } from "obsidian";
3 |
4 | export function onCommandTrigger(id: string, cb: () => void) {
5 | const uninstallCommand = around(this.app.commands, {
6 | executeCommand(originalMethod) {
7 | return function (...args: Command[]) {
8 | if (args[0].id === id) {
9 | cb();
10 | }
11 | const result =
12 | originalMethod && originalMethod.apply(this, args);
13 | return result;
14 | };
15 | },
16 | });
17 | return uninstallCommand;
18 | }
19 |
--------------------------------------------------------------------------------
/src/reset-progress-modal/ResetProgressModal.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 |
Reset Progress
9 |
10 | Resets all achievement progress. THIS CANNOT BE UNDONE.
11 | Are you sure you want to continue?
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/markdownHelpers.ts:
--------------------------------------------------------------------------------
1 | import type { CachedMetadata } from "obsidian";
2 |
3 | export function fileHasCallout(cache: CachedMetadata) {
4 | if (!cache.sections) {
5 | return false;
6 | }
7 | return cache.sections.some((section) => section.type === "callout");
8 | }
9 |
10 | export function getFileHeadingLevelsCount(cache: CachedMetadata) {
11 | if (!cache.headings) {
12 | return 0;
13 | }
14 |
15 | const levels = cache.headings.reduce((obj, { level }) => {
16 | obj[level] = true;
17 | return obj;
18 | }, {} as { [key: string]: boolean });
19 |
20 | return Object.keys(levels).length;
21 | }
22 |
--------------------------------------------------------------------------------
/version-bump.mjs:
--------------------------------------------------------------------------------
1 | import { readFileSync, writeFileSync } from "fs";
2 |
3 | const targetVersion = process.env.npm_package_version;
4 |
5 | // read minAppVersion from manifest.json and bump version to target version
6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
7 | const { minAppVersion } = manifest;
8 | manifest.version = targetVersion;
9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
10 |
11 | // update versions.json with target version and minAppVersion from manifest.json
12 | let versions = JSON.parse(readFileSync("versions.json", "utf8"));
13 | versions[targetVersion] = minAppVersion;
14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/src/achievements-view/AchievementsView.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 | {#each sortAchievements(SEEDED_ACHIEVEMENTS, $settingsStore) as achievement}
9 |
10 |
18 |
19 | {/each}
20 |
--------------------------------------------------------------------------------
/src/reset-progress-modal/ResetProgressModal.ts:
--------------------------------------------------------------------------------
1 | import { App, Modal } from "obsidian";
2 | import settingsStore from "src/settings";
3 | import ResetProgressModalComponent from "./ResetProgressModal.svelte";
4 |
5 | export class ResetProgressModal extends Modal {
6 | component: ResetProgressModalComponent;
7 |
8 | constructor(app: App) {
9 | super(app);
10 | }
11 |
12 | onOpen() {
13 | this.component = new ResetProgressModalComponent({
14 | target: this.contentEl,
15 | props: {
16 | close: () => this.close(),
17 | closeAndReset: () => this.closeAndReset(),
18 | },
19 | });
20 | }
21 |
22 | async closeAndReset() {
23 | settingsStore.reset();
24 | this.close();
25 | }
26 |
27 | onClose() {
28 | const { contentEl } = this;
29 | contentEl.empty();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/achievements-view/AchievementsView.ts:
--------------------------------------------------------------------------------
1 | import { ItemView, WorkspaceLeaf } from "obsidian";
2 | import AchievementsViewComponent from "./AchievementsView.svelte";
3 |
4 | export const VIEW_TYPE_ACHIEVEMENTS = "achievements-view";
5 |
6 | export class AchievementsView extends ItemView {
7 | component: AchievementsViewComponent;
8 |
9 | constructor(leaf: WorkspaceLeaf) {
10 | super(leaf);
11 | }
12 |
13 | getViewType() {
14 | return VIEW_TYPE_ACHIEVEMENTS;
15 | }
16 |
17 | getDisplayText() {
18 | return "Achievements";
19 | }
20 |
21 | getIcon() {
22 | return "trophy";
23 | }
24 |
25 | async onOpen() {
26 | this.component = new AchievementsViewComponent({
27 | target: this.contentEl,
28 | });
29 | }
30 |
31 | async onClose() {
32 | this.component.$destroy();
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Additional context**
32 | Add any other context about the problem here.
33 |
--------------------------------------------------------------------------------
/src/settings-tab/SettingsTab.ts:
--------------------------------------------------------------------------------
1 | import { App, PluginSettingTab } from "obsidian";
2 | import type AchievementsPlugin from "src/main";
3 | import { ResetProgressModal } from "src/reset-progress-modal/ResetProgressModal";
4 | import SettingsTabComponent from "./SettingsTab.svelte";
5 |
6 | export class AchievementsSettingTab extends PluginSettingTab {
7 | component: SettingsTabComponent;
8 |
9 | constructor(app: App, plugin: AchievementsPlugin) {
10 | super(app, plugin);
11 | }
12 |
13 | display(): void {
14 | const { containerEl } = this;
15 | containerEl.empty();
16 |
17 | this.component = new SettingsTabComponent({
18 | target: containerEl,
19 | props: {
20 | openResetModal: () => this.openResetModal(),
21 | },
22 | });
23 | }
24 |
25 | openResetModal() {
26 | new ResetProgressModal(this.app).open();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: "@typescript-eslint/parser",
4 | env: { node: true },
5 | plugins: ["@typescript-eslint", "svelte3"],
6 | overrides: [
7 | {
8 | files: ["*.svelte"],
9 | processor: "svelte3/svelte3",
10 | },
11 | ],
12 | extends: [
13 | "eslint:recommended",
14 | "plugin:@typescript-eslint/eslint-recommended",
15 | "plugin:@typescript-eslint/recommended",
16 | ],
17 | parserOptions: {
18 | sourceType: "module",
19 | },
20 | rules: {
21 | "no-unused-vars": "off",
22 | "@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
23 | "@typescript-eslint/ban-ts-comment": "off",
24 | "no-prototype-builtins": "off",
25 | "@typescript-eslint/no-empty-function": "off",
26 | },
27 | settings: {
28 | "svelte3/typescript": () => require("typescript"), // pass the TypeScript package to the Svelte plugin
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: zachatoo # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | # patreon: # Replace with a single Patreon username
5 | # open_collective: # Replace with a single Open Collective username
6 | ko_fi: zachatoo # Replace with a single Ko-fi username
7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | # liberapay: # Replace with a single Liberapay username
10 | # issuehunt: # Replace with a single IssueHunt username
11 | # otechie: # Replace with a single Otechie username
12 | # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/src/settings-tab/SettingsTab.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 | Achievements List
10 |
11 | {#each SEEDED_ACHIEVEMENTS as achievement}
12 |
13 |
21 |
22 | {/each}
23 |
24 | Danger Zone
25 |
26 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/array-utils/sort.ts:
--------------------------------------------------------------------------------
1 | import type { Achievement } from "src/seededAchievements";
2 | import type { Settings } from "src/settings";
3 |
4 | export function sortAchievements(arr: Achievement[], settings: Settings) {
5 | const result = [...arr];
6 | return result.sort((a, b) => {
7 | const achievedCriteria =
8 | (settings.achievedAchievementIDs.includes(a.id) ? 1 : 0) -
9 | (settings.achievedAchievementIDs.includes(b.id) ? 1 : 0);
10 | if (achievedCriteria !== 0) {
11 | return achievedCriteria;
12 | }
13 |
14 | const aProgress = settings[a.type] / a.requiredOccurenceCount;
15 | const bProgress = settings[b.type] / b.requiredOccurenceCount;
16 | const progressCriteria = bProgress - aProgress;
17 | if (progressCriteria !== 0) {
18 | return progressCriteria;
19 | }
20 |
21 | const requiredOccurenceCountCriteria =
22 | a.requiredOccurenceCount - b.requiredOccurenceCount;
23 | if (requiredOccurenceCountCriteria !== 0) {
24 | return requiredOccurenceCountCriteria;
25 | }
26 |
27 | return a.name.localeCompare(b.name);
28 | });
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Zachary Young
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/settings.ts:
--------------------------------------------------------------------------------
1 | import { writable } from "svelte/store";
2 |
3 | export interface Settings {
4 | achievedAchievementIDs: string[];
5 | notesCreated: number;
6 | notesDeleted: number;
7 | internalLinksCreated: number;
8 | commandPaletteOpened: number;
9 | quickSwitcherOpened: number;
10 | calloutsCreated: number;
11 | headingLevelsCreated: number;
12 | tagsCreated: number;
13 | }
14 |
15 | const DEFAULT_SETTINGS: Settings = {
16 | achievedAchievementIDs: [],
17 | notesCreated: 0,
18 | notesDeleted: 0,
19 | internalLinksCreated: 0,
20 | commandPaletteOpened: 0,
21 | quickSwitcherOpened: 0,
22 | calloutsCreated: 0,
23 | headingLevelsCreated: 0,
24 | tagsCreated: 0,
25 | };
26 |
27 | function createStore() {
28 | const { subscribe, set, update } = writable();
29 |
30 | function init(settings: Settings) {
31 | const newSettings = Object.assign({}, DEFAULT_SETTINGS, settings);
32 | set(newSettings);
33 | }
34 |
35 | function reset() {
36 | update((store) => {
37 | store = { ...DEFAULT_SETTINGS };
38 | return store;
39 | });
40 | }
41 |
42 | return {
43 | subscribe,
44 | set,
45 | init,
46 | reset,
47 | };
48 | }
49 |
50 | const store = createStore();
51 |
52 | export default store;
53 |
--------------------------------------------------------------------------------
/esbuild.config.mjs:
--------------------------------------------------------------------------------
1 | import esbuild from "esbuild";
2 | import process from "process";
3 | import builtins from "builtin-modules";
4 | import esbuildSvelte from "esbuild-svelte";
5 | import sveltePreprocess from "svelte-preprocess";
6 |
7 | const banner = `/*
8 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
9 | if you want to view the source, please visit the github repository of this plugin
10 | */
11 | `;
12 |
13 | const prod = process.argv[2] === "production";
14 |
15 | esbuild
16 | .build({
17 | plugins: [
18 | esbuildSvelte({
19 | compilerOptions: { css: true },
20 | preprocess: sveltePreprocess(),
21 | }),
22 | ],
23 | banner: {
24 | js: banner,
25 | },
26 | entryPoints: ["src/main.ts"],
27 | bundle: true,
28 | external: [
29 | "obsidian",
30 | "electron",
31 | "@codemirror/autocomplete",
32 | "@codemirror/collab",
33 | "@codemirror/commands",
34 | "@codemirror/language",
35 | "@codemirror/lint",
36 | "@codemirror/search",
37 | "@codemirror/state",
38 | "@codemirror/view",
39 | "@lezer/common",
40 | "@lezer/highlight",
41 | "@lezer/lr",
42 | ...builtins,
43 | ],
44 | format: "cjs",
45 | watch: !prod,
46 | target: "es2018",
47 | logLevel: "info",
48 | sourcemap: prod ? false : "inline",
49 | treeShaking: true,
50 | outfile: "main.js",
51 | })
52 | .catch(() => process.exit(1));
53 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "obsidian-achievements",
3 | "version": "0.0.10",
4 | "description": "Add achievements to Obsidian.",
5 | "main": "src/main.js",
6 | "scripts": {
7 | "dev": "node esbuild.config.mjs",
8 | "test": "vitest",
9 | "test:ci": "vitest run",
10 | "typecheck": "tsc -noEmit -skipLibCheck",
11 | "lint": "npx eslint . --ext .ts,.svelte",
12 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
13 | "version": "node version-bump.mjs && git add manifest.json versions.json",
14 | "prepare": "husky install"
15 | },
16 | "keywords": [
17 | "obsidian-plugin"
18 | ],
19 | "author": "Zachatoo",
20 | "license": "MIT",
21 | "devDependencies": {
22 | "@popperjs/core": "^2.11.6",
23 | "@tsconfig/svelte": "^3.0.0",
24 | "@types/node": "^16.11.6",
25 | "@typescript-eslint/eslint-plugin": "^5.40.1",
26 | "@typescript-eslint/parser": "^5.40.1",
27 | "builtin-modules": "3.3.0",
28 | "esbuild": "0.14.47",
29 | "esbuild-svelte": "^0.7.1",
30 | "eslint-plugin-svelte3": "^4.0.0",
31 | "husky": "^7.0.0",
32 | "obsidian": "latest",
33 | "obsidian-svelte": "^0.0.28",
34 | "svelte": "^3.52.0",
35 | "svelte-portal": "^2.2.0",
36 | "svelte-preprocess": "^4.10.7",
37 | "tslib": "2.4.0",
38 | "typescript": "4.7.4",
39 | "vitest": "^0.24.3"
40 | },
41 | "dependencies": {
42 | "monkey-around": "^2.3.0"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | lint:
13 | name: Lint
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v3
18 |
19 | - name: Setup node
20 | uses: actions/setup-node@v3
21 | with:
22 | node-version: 16
23 |
24 | - name: Install dependencies
25 | run: npm i
26 |
27 | - name: Lint
28 | run: npm run lint
29 |
30 | typecheck:
31 | name: Type Check
32 | runs-on: ubuntu-latest
33 | steps:
34 | - name: Checkout
35 | uses: actions/checkout@v3
36 |
37 | - name: Setup node
38 | uses: actions/setup-node@v3
39 | with:
40 | node-version: 16
41 |
42 | - name: Install dependencies
43 | run: npm i
44 |
45 | - name: Type check
46 | run: npm run typecheck
47 |
48 | test:
49 | name: Test
50 | runs-on: ubuntu-latest
51 | steps:
52 | - name: Checkout
53 | uses: actions/checkout@v3
54 |
55 | - name: Setup node
56 | uses: actions/setup-node@v3
57 | with:
58 | node-version: 16
59 |
60 | - name: Install dependencies
61 | run: npm i
62 |
63 | - name: Test
64 | run: npm run test:ci
65 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Obsidian Achievements
4 |
5 | Gamify your note taking by working towards gaining achievements!
6 |
7 | ## Achievements
8 |
9 | ### Your first note
10 |
11 | Create a note.
12 |
13 | ### Note taker
14 |
15 | Create ten notes.
16 |
17 | ### Wordsmith
18 |
19 | Create one hundred notes.
20 |
21 | ### Storyteller
22 |
23 | Create one thousand notes.
24 |
25 | ### Taking out the trash
26 |
27 | Delete ten notes.
28 |
29 | ### Linking your thinking
30 |
31 | Create an internal link. You can type [[ to begin creating an internal link.
32 |
33 | ### Making connections
34 |
35 | Create ten internal links.
36 |
37 | ### Conspiracy theorist
38 |
39 | Create one hundred internal links.
40 |
41 | ### Air traffic controller
42 |
43 | Create one thousand internal links.
44 |
45 | ### Commander
46 |
47 | Open the command palette. You can find the hotkey to open the command palette in Settings - Hotkeys.
48 |
49 | ### Quickly now
50 |
51 | Open the quick switcher. You can find the hotkey to open the quick switcher in Settings - Hotkeys.
52 |
53 | ### Callouts
54 |
55 | Create a callout. You can find the hotkey to create a callout in Settings - Hotkeys.
56 |
57 | ### Headings
58 |
59 | Create a heading. You can create a heading by adding a new line to a note and typing # Heading.
60 |
61 | ### Nested headings
62 |
63 | Create at least three levels of headings in a single note.
64 |
65 | ### Your first tag
66 |
67 | Create a tag. You can create a tag by typing #tag.
68 |
69 | ### Tagging apprentice
70 |
71 | Create five unique tags.
72 |
73 | ### Tagging expert
74 |
75 | Create ten unique tags.
76 |
77 | ## Notice
78 |
79 | There's some known issues on mobile with achievements being achieved much quicker than expected, and some achievements not being achievable.
80 |
81 | ## Attributions
82 |
83 | - Thank you to pjeby for [monkey-around](https://github.com/pjeby/monkey-around) that I used to hook into events in Obsidian.
84 | - Thank you to marcusolsson for [obsidian-svelte](https://github.com/marcusolsson/obsidian-svelte) that I used for creating many of the UI elements.
85 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Release Obsidian Plugin
2 | on:
3 | push:
4 | tags:
5 | - "*"
6 | jobs:
7 | build:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v2
11 | with:
12 | fetch-depth: 0 # otherwise, you will failed to push refs to dest repo
13 | - name: Use Node.js
14 | uses: actions/setup-node@v1
15 | with:
16 | node-version: "14.x"
17 | # Get the version number and put it in a variable
18 | - name: Get Version
19 | id: version
20 | run: |
21 | echo "::set-output name=tag::$(git describe --abbrev=0)"
22 | # Build the plugin
23 | - name: Build
24 | id: build
25 | run: |
26 | npm install
27 | npm run build --if-present
28 | # Package the required files into a zip
29 | - name: Package
30 | run: |
31 | mkdir ${{ github.event.repository.name }}
32 | cp main.js manifest.json styles.css README.md ${{ github.event.repository.name }}
33 | zip -r ${{ github.event.repository.name }}.zip ${{ github.event.repository.name }}
34 | # Create the release on github
35 | - name: Create Release
36 | id: create_release
37 | uses: actions/create-release@v1
38 | env:
39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40 | VERSION: ${{ github.ref }}
41 | with:
42 | tag_name: ${{ github.ref }}
43 | release_name: ${{ github.ref }}
44 | draft: false
45 | prerelease: false
46 | # Upload the packaged release file
47 | - name: Upload zip file
48 | id: upload-zip
49 | uses: actions/upload-release-asset@v1
50 | env:
51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52 | with:
53 | upload_url: ${{ steps.create_release.outputs.upload_url }}
54 | asset_path: ./${{ github.event.repository.name }}.zip
55 | asset_name: ${{ github.event.repository.name }}-${{ steps.version.outputs.tag }}.zip
56 | asset_content_type: application/zip
57 | # Upload the main.js
58 | - name: Upload main.js
59 | id: upload-main
60 | uses: actions/upload-release-asset@v1
61 | env:
62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
63 | with:
64 | upload_url: ${{ steps.create_release.outputs.upload_url }}
65 | asset_path: ./main.js
66 | asset_name: main.js
67 | asset_content_type: text/javascript
68 | # Upload the manifest.json
69 | - name: Upload manifest.json
70 | id: upload-manifest
71 | uses: actions/upload-release-asset@v1
72 | env:
73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
74 | with:
75 | upload_url: ${{ steps.create_release.outputs.upload_url }}
76 | asset_path: ./manifest.json
77 | asset_name: manifest.json
78 | asset_content_type: application/json
79 | # Upload the style.css
80 | - name: Upload styles.css
81 | id: upload-css
82 | uses: actions/upload-release-asset@v1
83 | env:
84 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
85 | with:
86 | upload_url: ${{ steps.create_release.outputs.upload_url }}
87 | asset_path: ./styles.css
88 | asset_name: styles.css
89 | asset_content_type: text/css=
90 |
--------------------------------------------------------------------------------
/src/__tests__/markdownHelpers.test.ts:
--------------------------------------------------------------------------------
1 | import type { CachedMetadata } from "obsidian";
2 | import { fileHasCallout, getFileHeadingLevelsCount } from "src/markdownHelpers";
3 | import { describe, expect, it } from "vitest";
4 |
5 | describe("markdownHelpers", () => {
6 | describe("fileHasCallout", () => {
7 | it("returns true if there is a callout", () => {
8 | const mockCache: CachedMetadata = {
9 | sections: [
10 | {
11 | type: "callout",
12 | position: {
13 | end: { line: 2, col: 10, offset: 27 },
14 | start: { line: 1, col: 0, offset: 1 },
15 | },
16 | },
17 | ],
18 | };
19 |
20 | const result = fileHasCallout(mockCache);
21 |
22 | expect(result).toBe(true);
23 | });
24 |
25 | it("returns false for empty cache", () => {
26 | const mockCache: CachedMetadata = {};
27 |
28 | const result = fileHasCallout(mockCache);
29 |
30 | expect(result).toBe(false);
31 | });
32 |
33 | it("returns false if there are no callouts", () => {
34 | const mockCache: CachedMetadata = {
35 | sections: [
36 | {
37 | position: {
38 | end: { line: 0, col: 9, offset: 9 },
39 | start: { line: 0, col: 0, offset: 0 },
40 | },
41 | type: "heading",
42 | },
43 | {
44 | position: {
45 | end: { line: 2, col: 7, offset: 18 },
46 | start: { line: 2, col: 0, offset: 11 },
47 | },
48 | type: "paragraph",
49 | },
50 | ],
51 | };
52 |
53 | const result = fileHasCallout(mockCache);
54 |
55 | expect(result).toBe(false);
56 | });
57 | });
58 |
59 | describe("getFileHeadingLevelsCount", () => {
60 | it("returns 0 if no headings", () => {
61 | const mockCache: CachedMetadata = {
62 | sections: [
63 | {
64 | type: "callout",
65 | position: {
66 | end: { line: 2, col: 10, offset: 27 },
67 | start: { line: 1, col: 0, offset: 1 },
68 | },
69 | },
70 | ],
71 | };
72 |
73 | const result = getFileHeadingLevelsCount(mockCache);
74 |
75 | expect(result).toBe(0);
76 | });
77 |
78 | it("returns 0 if empty cache", () => {
79 | const mockCache: CachedMetadata = {};
80 |
81 | const result = getFileHeadingLevelsCount(mockCache);
82 |
83 | expect(result).toBe(0);
84 | });
85 |
86 | it("returns 1 if single heading", () => {
87 | const mockCache: CachedMetadata = {
88 | headings: [
89 | {
90 | heading: "Heading",
91 | level: 1,
92 | position: {
93 | end: { line: 0, col: 9, offset: 9 },
94 | start: { line: 0, col: 0, offset: 0 },
95 | },
96 | },
97 | ],
98 | };
99 |
100 | const result = getFileHeadingLevelsCount(mockCache);
101 |
102 | expect(result).toBe(1);
103 | });
104 |
105 | it("returns 1 if 2 headings same level", () => {
106 | const mockCache: CachedMetadata = {
107 | headings: [
108 | {
109 | heading: "Heading",
110 | level: 1,
111 | position: {
112 | end: { line: 0, col: 9, offset: 9 },
113 | start: { line: 0, col: 0, offset: 0 },
114 | },
115 | },
116 | {
117 | heading: "Another heading",
118 | level: 1,
119 | position: {
120 | end: { line: 4, col: 17, offset: 37 },
121 | start: { line: 4, col: 0, offset: 20 },
122 | },
123 | },
124 | ],
125 | };
126 |
127 | const result = getFileHeadingLevelsCount(mockCache);
128 |
129 | expect(result).toBe(1);
130 | });
131 |
132 | it("returns 2 if 2 headings different levels", () => {
133 | const mockCache: CachedMetadata = {
134 | headings: [
135 | {
136 | heading: "Heading",
137 | level: 1,
138 | position: {
139 | end: { line: 0, col: 9, offset: 9 },
140 | start: { line: 0, col: 0, offset: 0 },
141 | },
142 | },
143 | {
144 | heading: "Another heading",
145 | level: 2,
146 | position: {
147 | end: { line: 4, col: 18, offset: 38 },
148 | start: { line: 4, col: 0, offset: 20 },
149 | },
150 | },
151 | ],
152 | };
153 |
154 | const result = getFileHeadingLevelsCount(mockCache);
155 |
156 | expect(result).toBe(2);
157 | });
158 |
159 | it("returns 6 if 6 headings different levels", () => {
160 | const mockCache: CachedMetadata = {
161 | headings: [
162 | {
163 | position: {
164 | start: { line: 0, col: 0, offset: 0 },
165 | end: { line: 0, col: 13, offset: 13 },
166 | },
167 | heading: "First level",
168 | level: 1,
169 | },
170 | {
171 | position: {
172 | start: { line: 2, col: 0, offset: 15 },
173 | end: { line: 2, col: 15, offset: 30 },
174 | },
175 | heading: "Second level",
176 | level: 2,
177 | },
178 | {
179 | position: {
180 | start: { line: 4, col: 0, offset: 32 },
181 | end: { line: 4, col: 15, offset: 47 },
182 | },
183 | heading: "Third level",
184 | level: 3,
185 | },
186 | {
187 | position: {
188 | start: { line: 6, col: 0, offset: 49 },
189 | end: { line: 6, col: 17, offset: 66 },
190 | },
191 | heading: "Fourth level",
192 | level: 4,
193 | },
194 | {
195 | position: {
196 | start: { line: 8, col: 0, offset: 68 },
197 | end: { line: 8, col: 17, offset: 85 },
198 | },
199 | heading: "Fifth level",
200 | level: 5,
201 | },
202 | {
203 | position: {
204 | start: { line: 10, col: 0, offset: 87 },
205 | end: { line: 10, col: 18, offset: 105 },
206 | },
207 | heading: "Sixth level",
208 | level: 6,
209 | },
210 | ],
211 | };
212 |
213 | const result = getFileHeadingLevelsCount(mockCache);
214 |
215 | expect(result).toBe(6);
216 | });
217 | });
218 | });
219 |
--------------------------------------------------------------------------------
/src/seededAchievements.ts:
--------------------------------------------------------------------------------
1 | export type AchievementType =
2 | | "notesCreated"
3 | | "notesDeleted"
4 | | "internalLinksCreated"
5 | | "commandPaletteOpened"
6 | | "quickSwitcherOpened"
7 | | "calloutsCreated"
8 | | "headingLevelsCreated"
9 | | "tagsCreated";
10 |
11 | export interface Achievement {
12 | id: string;
13 | type: AchievementType;
14 | name: string;
15 | description: string;
16 | popupMessage: string; // message that shows when achievement is achieved
17 | requiredOccurenceCount: number;
18 | }
19 |
20 | export const SEEDED_ACHIEVEMENTS: Achievement[] = [
21 | {
22 | id: "notes-created:1",
23 | type: "notesCreated",
24 | name: "Your first note",
25 | description: "Create a note.",
26 | popupMessage:
27 | "You've created your first note. You're off to a great start!",
28 | requiredOccurenceCount: 1,
29 | },
30 | {
31 | id: "notes-created:10",
32 | type: "notesCreated",
33 | name: "Note taker",
34 | description: "Create ten notes.",
35 | popupMessage: "You've created ten notes. Great job!",
36 | requiredOccurenceCount: 10,
37 | },
38 | {
39 | id: "notes-created:100",
40 | type: "notesCreated",
41 | name: "Wordsmith",
42 | description: "Create one hundred notes.",
43 | popupMessage:
44 | "You've created one hundred notes! You're serious, aren't you?",
45 | requiredOccurenceCount: 100,
46 | },
47 | {
48 | id: "notes-created:1000",
49 | type: "notesCreated",
50 | name: "Storyteller",
51 | description: "Create one thousand notes.",
52 | popupMessage: "You've created one thousand notes! That's impressive!",
53 | requiredOccurenceCount: 1000,
54 | },
55 | {
56 | id: "notes-deleted:10",
57 | type: "notesDeleted",
58 | name: "Taking out the trash",
59 | description: "Delete ten notes.",
60 | popupMessage: "You've deleted 10 notes. Way to keep your vault tidy!",
61 | requiredOccurenceCount: 10,
62 | },
63 | {
64 | id: "internal-links-created:1",
65 | type: "internalLinksCreated",
66 | name: "Linking your thinking",
67 | description:
68 | "Create an internal link. You can type [[ to begin creating a internal link.",
69 | popupMessage:
70 | "You've created your first internal link. It's all coming together!",
71 | requiredOccurenceCount: 1,
72 | },
73 | {
74 | id: "internal-links-created:10",
75 | type: "internalLinksCreated",
76 | name: "Making connections",
77 | description: "Create ten internal links.",
78 | popupMessage:
79 | "You've created 10 internal links. Your graph is looking great!",
80 | requiredOccurenceCount: 10,
81 | },
82 | {
83 | id: "internal-links-created:100",
84 | type: "internalLinksCreated",
85 | name: "Conspiracy theorist",
86 | description: "Create one hundred internal links",
87 | popupMessage:
88 | "You've created 100 internal links. Your graph is starting to look like a conspiracy board...",
89 | requiredOccurenceCount: 100,
90 | },
91 | {
92 | id: "internal-links-created:1000",
93 | type: "internalLinksCreated",
94 | name: "Air traffic controller",
95 | description: "Create one thousand internal links",
96 | popupMessage:
97 | "You've created 1000 internal links. If you haven't already, you should post your graph on the official Obsidian discord.",
98 | requiredOccurenceCount: 1000,
99 | },
100 | {
101 | id: "command-palette:open",
102 | type: "commandPaletteOpened",
103 | name: "Commander",
104 | description:
105 | "Open the command palette. You can find the hotkey to open the command palette in Settings - Hotkeys.",
106 | popupMessage:
107 | "You've opened the command palette. Way to take charge of your note taking!",
108 | requiredOccurenceCount: 1,
109 | },
110 | {
111 | id: "switcher:open",
112 | type: "quickSwitcherOpened",
113 | name: "Quickly now",
114 | description:
115 | "Open the quick switcher. You can find the hotkey to open the quick switcher in Settings - Hotkeys.",
116 | popupMessage: "You've opened the quick switcher. Wow that was fast!",
117 | requiredOccurenceCount: 1,
118 | },
119 | {
120 | id: "callouts:1",
121 | type: "calloutsCreated",
122 | name: "Callouts",
123 | description:
124 | "Create a callout. You can find the hotkey to create a callout in Settings - Hotkeys.",
125 | popupMessage:
126 | "You've created a callout. Just felt like that needed to be called out.",
127 | requiredOccurenceCount: 1,
128 | },
129 | {
130 | id: "heading-levels:1",
131 | type: "headingLevelsCreated",
132 | name: "Headings",
133 | description:
134 | "Create a heading. You can create a heading by adding a new line to a note and typing # Heading.",
135 | popupMessage:
136 | "You've created a heading. Your notes are looking more organized already!",
137 | requiredOccurenceCount: 1,
138 | },
139 | {
140 | id: "heading-levels:3",
141 | type: "headingLevelsCreated",
142 | name: "Nested headings",
143 | description:
144 | "Create at least three levels of headings in a single note.",
145 | popupMessage:
146 | "You've created at least 3 levels of nested headings. Your notes look so organized!",
147 | requiredOccurenceCount: 3,
148 | },
149 | {
150 | id: "tags-created:1",
151 | type: "tagsCreated",
152 | name: "Your first tag",
153 | description: "Create a tag. You can create a tag by typing #tag.",
154 | popupMessage: "You've created your first tag!",
155 | requiredOccurenceCount: 1,
156 | },
157 | {
158 | id: "tags-created:5",
159 | type: "tagsCreated",
160 | name: "Tagging apprentice",
161 | description: "Create five unique tags.",
162 | popupMessage: "You've created five unique tags!",
163 | requiredOccurenceCount: 5,
164 | },
165 | {
166 | id: "tags-created:10",
167 | type: "tagsCreated",
168 | name: "Tagging expert",
169 | description: "Create ten unique tags.",
170 | popupMessage: "You've created ten unique tags!",
171 | requiredOccurenceCount: 10,
172 | },
173 | ];
174 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { Notice, Plugin, TAbstractFile, type CachedMetadata } from "obsidian";
2 | import { get } from "svelte/store";
3 | import { AchievementsSettingTab } from "./settings-tab/SettingsTab";
4 | import {
5 | SEEDED_ACHIEVEMENTS,
6 | type AchievementType,
7 | } from "./seededAchievements";
8 | import { onCommandTrigger } from "./commands";
9 | import settingsStore, { type Settings } from "./settings";
10 | import { fileHasCallout, getFileHeadingLevelsCount } from "./markdownHelpers";
11 | import type { InternalCounts } from "./InternalCounts";
12 | import {
13 | AchievementsView,
14 | VIEW_TYPE_ACHIEVEMENTS,
15 | } from "./achievements-view/AchievementsView";
16 |
17 | export default class AchievementsPlugin extends Plugin {
18 | internalCounts: InternalCounts;
19 |
20 | async onload() {
21 | console.log("loading Achievements plugin");
22 |
23 | settingsStore.init(await this.loadData());
24 |
25 | this.register(
26 | settingsStore.subscribe(async (settings) => {
27 | await this.saveData(settings);
28 | })
29 | );
30 |
31 | this.setupInternalCounts();
32 |
33 | this.registerView(
34 | VIEW_TYPE_ACHIEVEMENTS,
35 | (leaf) => new AchievementsView(leaf)
36 | );
37 |
38 | this.registerEvent(
39 | this.app.metadataCache.on("changed", (file, data, cache) => {
40 | this.handleFileCreateUpdateDelete(file, cache);
41 | })
42 | );
43 |
44 | this.registerEvent(
45 | this.app.metadataCache.on("deleted", (file, _prevCache) =>
46 | this.handleFileCreateUpdateDelete(file)
47 | )
48 | );
49 |
50 | this.register(
51 | onCommandTrigger("command-palette:open", async () => {
52 | const settings = this.getSettings();
53 | settings.commandPaletteOpened += 1;
54 | this.getNewAchievementMaybe("commandPaletteOpened", settings);
55 | this.setSettings(settings);
56 | })
57 | );
58 |
59 | this.register(
60 | onCommandTrigger("switcher:open", async () => {
61 | const settings = this.getSettings();
62 | settings.quickSwitcherOpened += 1;
63 | this.getNewAchievementMaybe("quickSwitcherOpened", settings);
64 | this.setSettings(settings);
65 | })
66 | );
67 |
68 | this.addCommand({
69 | id: "show-achievements-view",
70 | name: "Show Achievements Panel",
71 | callback: () => {
72 | this.activateView();
73 | },
74 | });
75 |
76 | this.addSettingTab(new AchievementsSettingTab(this.app, this));
77 | }
78 |
79 | onunload() {
80 | console.log("unloading Achievements plugin");
81 |
82 | this.app.workspace.detachLeavesOfType(VIEW_TYPE_ACHIEVEMENTS);
83 | }
84 |
85 | getSettings() {
86 | return get(settingsStore);
87 | }
88 |
89 | setSettings(settings: Settings) {
90 | settingsStore.set(settings);
91 | }
92 |
93 | setupInternalCounts() {
94 | this.internalCounts = {
95 | noteCount: this.getMarkdownFilesCount(),
96 | internalLinkCount: this.getInternalLinksCount(),
97 | tagCount: this.getTagsCount(),
98 | };
99 | }
100 |
101 | async activateView() {
102 | this.app.workspace.detachLeavesOfType(VIEW_TYPE_ACHIEVEMENTS);
103 |
104 | await this.app.workspace.getRightLeaf(false).setViewState({
105 | type: VIEW_TYPE_ACHIEVEMENTS,
106 | active: true,
107 | });
108 |
109 | this.app.workspace.revealLeaf(
110 | this.app.workspace.getLeavesOfType(VIEW_TYPE_ACHIEVEMENTS)[0]
111 | );
112 | }
113 |
114 | getMarkdownFilesCount() {
115 | return this.app.vault.getMarkdownFiles().length;
116 | }
117 |
118 | getInternalLinksCount() {
119 | let count = 0;
120 | this.app.metadataCache.iterateReferences(() => {
121 | count++;
122 | });
123 | return count;
124 | }
125 |
126 | getTagsCount() {
127 | const tagsObj = this.app.metadataCache.getTags();
128 | const baseTagsArr = Object.entries(tagsObj).filter(
129 | ([key]) => !key.includes("/")
130 | );
131 | return baseTagsArr.reduce((prev, curr) => prev + curr[1], 0);
132 | }
133 |
134 | async handleFileCreateUpdateDelete(
135 | file: TAbstractFile,
136 | cache?: CachedMetadata
137 | ) {
138 | const currNoteCount = this.getMarkdownFilesCount();
139 | const currInternalLinkCount = this.getInternalLinksCount();
140 | const currTagsCount = this.getTagsCount();
141 | const settings = this.getSettings();
142 |
143 | if (currNoteCount > this.internalCounts.noteCount) {
144 | settings.notesCreated +=
145 | currNoteCount - this.internalCounts.noteCount;
146 | this.internalCounts.noteCount = currNoteCount;
147 | this.getNewAchievementMaybe("notesCreated", settings);
148 | }
149 | if (currNoteCount < this.internalCounts.noteCount) {
150 | settings.notesDeleted +=
151 | this.internalCounts.noteCount - currNoteCount;
152 | this.internalCounts.noteCount = currNoteCount;
153 | this.getNewAchievementMaybe("notesDeleted", settings);
154 | }
155 | if (currInternalLinkCount > this.internalCounts.internalLinkCount) {
156 | settings.internalLinksCreated +=
157 | currInternalLinkCount - this.internalCounts.internalLinkCount;
158 | this.internalCounts.internalLinkCount = currInternalLinkCount;
159 | this.getNewAchievementMaybe("internalLinksCreated", settings);
160 | }
161 |
162 | if (currTagsCount > this.internalCounts.tagCount) {
163 | settings.tagsCreated +=
164 | currTagsCount - this.internalCounts.tagCount;
165 | this.internalCounts.tagCount = currTagsCount;
166 | this.getNewAchievementMaybe("tagsCreated", settings);
167 | }
168 |
169 | if (cache) {
170 | if (settings.calloutsCreated === 0 && fileHasCallout(cache)) {
171 | settings.calloutsCreated = 1;
172 | this.getNewAchievementMaybe("calloutsCreated", settings);
173 | }
174 |
175 | const headingLevelsCount = getFileHeadingLevelsCount(cache);
176 | if (headingLevelsCount > settings.headingLevelsCreated) {
177 | settings.headingLevelsCreated = headingLevelsCount;
178 | this.getNewAchievementMaybe("headingLevelsCreated", settings);
179 | }
180 | }
181 |
182 | this.setSettings(settings);
183 | }
184 |
185 | getNewAchievementMaybe(type: AchievementType, settings: Settings) {
186 | const newAchievements = SEEDED_ACHIEVEMENTS.filter(
187 | (achievement) =>
188 | achievement.type === type &&
189 | settings[type] >= achievement.requiredOccurenceCount &&
190 | !settings.achievedAchievementIDs.includes(achievement.id)
191 | );
192 | if (newAchievements.length > 0) {
193 | newAchievements.forEach((achievement) => {
194 | settings.achievedAchievementIDs.push(achievement.id);
195 | new Notice(`${achievement.name}\n${achievement.popupMessage}`);
196 | });
197 | }
198 | }
199 | }
200 |
--------------------------------------------------------------------------------