generated from tpl/obsidian-sample-plugin
			Add A to Z Challenge
This commit is contained in:
		
							parent
							
								
									ffb8cc8d9c
								
							
						
					
					
						commit
						891041c965
					
				| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
{
 | 
			
		||||
	"id": "obsidian-book-tracker",
 | 
			
		||||
	"name": "Book Tracker",
 | 
			
		||||
	"version": "1.1.0",
 | 
			
		||||
	"version": "1.2.0",
 | 
			
		||||
	"minAppVersion": "0.15.0",
 | 
			
		||||
	"description": "Simplifies tracking your reading progress and managing your book collection in Obsidian.",
 | 
			
		||||
	"author": "FiFiTiDo",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
{
 | 
			
		||||
	"name": "obsidian-book-tracker",
 | 
			
		||||
	"version": "1.1.0",
 | 
			
		||||
	"version": "1.2.0",
 | 
			
		||||
	"description": "Simplifies tracking your reading progress and managing your book collection in Obsidian.",
 | 
			
		||||
	"main": "main.js",
 | 
			
		||||
	"scripts": {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,6 +25,7 @@ import { CreateBookFromGoodreadsUrlCommand } from "@commands/CreateBookFromGoodr
 | 
			
		|||
import { registerShelfCodeBlockProcessor } from "@ui/code-blocks/ShelfCodeBlock";
 | 
			
		||||
import { ReloadReadingLogCommand } from "@commands/ReloadReadingLogCommand";
 | 
			
		||||
import { registerReadingCalendarCodeBlockProcessor } from "@ui/code-blocks/ReadingCalendarCodeBlock";
 | 
			
		||||
import { registerAToZChallengeCodeBlockProcessor } from "@ui/code-blocks/AToZChallengeCodeBlock";
 | 
			
		||||
 | 
			
		||||
export default class BookTrackerPlugin extends Plugin {
 | 
			
		||||
	public settings: BookTrackerPluginSettings;
 | 
			
		||||
| 
						 | 
				
			
			@ -91,6 +92,7 @@ export default class BookTrackerPlugin extends Plugin {
 | 
			
		|||
		registerReadingStatsCodeBlockProcessor(this);
 | 
			
		||||
		registerShelfCodeBlockProcessor(this);
 | 
			
		||||
		registerReadingCalendarCodeBlockProcessor(this);
 | 
			
		||||
		registerAToZChallengeCodeBlockProcessor(this);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onunload() {}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
import { registerCodeBlockRenderer } from ".";
 | 
			
		||||
import { SvelteCodeBlockRenderer } from "./SvelteCodeBlockRenderer";
 | 
			
		||||
import AToZChallengeCodeBlockView from "./AToZChallengeCodeBlockView.svelte";
 | 
			
		||||
import type BookTrackerPlugin from "@src/main";
 | 
			
		||||
import z from "zod/v4";
 | 
			
		||||
 | 
			
		||||
export function registerAToZChallengeCodeBlockProcessor(
 | 
			
		||||
	plugin: BookTrackerPlugin
 | 
			
		||||
): void {
 | 
			
		||||
	registerCodeBlockRenderer(
 | 
			
		||||
		plugin,
 | 
			
		||||
		"a-to-z-challenge",
 | 
			
		||||
		(source, el) =>
 | 
			
		||||
			new SvelteCodeBlockRenderer(
 | 
			
		||||
				AToZChallengeCodeBlockView,
 | 
			
		||||
				plugin,
 | 
			
		||||
				source,
 | 
			
		||||
				el
 | 
			
		||||
			)
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AToZChallengeSettingsSchema = z.object({
 | 
			
		||||
	coverProperty: z.string(),
 | 
			
		||||
	titleProperty: z.string(),
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,194 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import {
 | 
			
		||||
		createSettings,
 | 
			
		||||
		setSettingsContext,
 | 
			
		||||
	} from "@ui/stores/settings.svelte";
 | 
			
		||||
	import type { SvelteCodeBlockProps } from "./SvelteCodeBlockRenderer";
 | 
			
		||||
	import { createMetadata } from "@ui/stores/metadata.svelte";
 | 
			
		||||
	import { STATUS_READ } from "@src/const";
 | 
			
		||||
	import { ALL_TIME } from "@ui/stores/date-filter.svelte";
 | 
			
		||||
	import { AToZChallengeSettingsSchema } from "./AToZChallengeCodeBlock";
 | 
			
		||||
	import { parseYaml, TFile } from "obsidian";
 | 
			
		||||
	import OpenFileLink from "@ui/components/OpenFileLink.svelte";
 | 
			
		||||
	import type { Moment } from "moment";
 | 
			
		||||
 | 
			
		||||
	const { plugin, source }: SvelteCodeBlockProps = $props();
 | 
			
		||||
 | 
			
		||||
	const settings = AToZChallengeSettingsSchema.parse(parseYaml(source));
 | 
			
		||||
 | 
			
		||||
	const settingsStore = createSettings(plugin);
 | 
			
		||||
	setSettingsContext(settingsStore);
 | 
			
		||||
 | 
			
		||||
	const metadataStore = createMetadata(plugin, {
 | 
			
		||||
		statusFilter: STATUS_READ,
 | 
			
		||||
		initialYear: true,
 | 
			
		||||
		disableMonthFilter: true,
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const getSortValue = (value: string) => {
 | 
			
		||||
		if (value.startsWith("A ")) {
 | 
			
		||||
			return value.slice(2) + ", A";
 | 
			
		||||
		} else if (value.startsWith("An ")) {
 | 
			
		||||
			return value.slice(3) + ", An";
 | 
			
		||||
		} else if (value.startsWith("The ")) {
 | 
			
		||||
			return value.slice(4) + ", The";
 | 
			
		||||
		} else {
 | 
			
		||||
			return value;
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
 | 
			
		||||
 | 
			
		||||
	const items = $derived(
 | 
			
		||||
		metadataStore.metadata.reduce(
 | 
			
		||||
			(acc, item) => {
 | 
			
		||||
				const title = item.frontmatter[settings.titleProperty];
 | 
			
		||||
				const firstLetter = getSortValue(title).charAt(0).toUpperCase();
 | 
			
		||||
 | 
			
		||||
				if (!firstLetter.match(/[A-Z]/)) {
 | 
			
		||||
					return acc;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if (!acc[firstLetter]) {
 | 
			
		||||
					const coverPath = item.frontmatter[
 | 
			
		||||
						settings.coverProperty
 | 
			
		||||
					] as string;
 | 
			
		||||
					const coverFile = plugin.app.vault.getFileByPath(coverPath);
 | 
			
		||||
 | 
			
		||||
					let coverSrc: string = "";
 | 
			
		||||
					if (coverFile) {
 | 
			
		||||
						coverSrc = plugin.app.vault.getResourcePath(coverFile);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					const coverAlt = item.frontmatter[settings.titleProperty];
 | 
			
		||||
 | 
			
		||||
					acc[firstLetter] = {
 | 
			
		||||
						file: item.file,
 | 
			
		||||
						// @ts-expect-error Moment is provided by Obsidian
 | 
			
		||||
						startDate: moment(
 | 
			
		||||
							item.frontmatter[
 | 
			
		||||
								settingsStore.settings.startDateProperty
 | 
			
		||||
							],
 | 
			
		||||
						),
 | 
			
		||||
						// @ts-expect-error Moment is provided by Obsidian
 | 
			
		||||
						endDate: moment(
 | 
			
		||||
							item.frontmatter[
 | 
			
		||||
								settingsStore.settings.endDateProperty
 | 
			
		||||
							],
 | 
			
		||||
						),
 | 
			
		||||
						coverSrc,
 | 
			
		||||
						coverAlt,
 | 
			
		||||
					};
 | 
			
		||||
				}
 | 
			
		||||
				return acc;
 | 
			
		||||
			},
 | 
			
		||||
			{} as Record<
 | 
			
		||||
				string,
 | 
			
		||||
				{
 | 
			
		||||
					file: TFile;
 | 
			
		||||
					startDate: Moment;
 | 
			
		||||
					endDate: Moment;
 | 
			
		||||
					coverSrc: string;
 | 
			
		||||
					coverAlt: string;
 | 
			
		||||
				}
 | 
			
		||||
			>,
 | 
			
		||||
		),
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	const startDate = $derived(
 | 
			
		||||
		Object.values(items)
 | 
			
		||||
			.map((item) => item.startDate)
 | 
			
		||||
			.sort((a, b) => a.diff(b))[0],
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	const endDate = $derived.by(() => {
 | 
			
		||||
		const dates = Object.values(items)
 | 
			
		||||
			.map((item) => item.endDate)
 | 
			
		||||
			.sort((a, b) => b.diff(a));
 | 
			
		||||
 | 
			
		||||
		if (dates.length !== 26) {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return dates[0];
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="reading-bingo">
 | 
			
		||||
	<div class="top-info">
 | 
			
		||||
		<select class="year-filter" bind:value={metadataStore.filterYear}>
 | 
			
		||||
			{#each metadataStore.filterYears as year}
 | 
			
		||||
				<option value={year}>{year}</option>
 | 
			
		||||
			{/each}
 | 
			
		||||
		</select>
 | 
			
		||||
		<p>Started: {startDate.format("YYYY-MM-DD")}</p>
 | 
			
		||||
		<p>Ended: {endDate?.format("YYYY-MM-DD") ?? "N/A"}</p>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="bingo">
 | 
			
		||||
		{#each alphabet as letter}
 | 
			
		||||
			<div class="bingo-item">
 | 
			
		||||
				{#if items[letter]}
 | 
			
		||||
					{@const item = items[letter]}
 | 
			
		||||
					<OpenFileLink file={item.file}>
 | 
			
		||||
						<img src={item.coverSrc} alt={item.coverAlt} />
 | 
			
		||||
					</OpenFileLink>
 | 
			
		||||
				{:else}
 | 
			
		||||
					<div class="placeholder">{letter}</div>
 | 
			
		||||
				{/if}
 | 
			
		||||
			</div>
 | 
			
		||||
		{/each}
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
	.reading-bingo {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
		gap: var(--size-4-6);
 | 
			
		||||
 | 
			
		||||
		.top-info {
 | 
			
		||||
			display: flex;
 | 
			
		||||
			align-items: center;
 | 
			
		||||
			gap: var(--size-4-4);
 | 
			
		||||
 | 
			
		||||
			p {
 | 
			
		||||
				padding: 0;
 | 
			
		||||
				margin: 0;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.bingo {
 | 
			
		||||
			display: flex;
 | 
			
		||||
			flex-wrap: wrap;
 | 
			
		||||
			justify-content: center;
 | 
			
		||||
			gap: var(--size-4-4);
 | 
			
		||||
 | 
			
		||||
			.bingo-item {
 | 
			
		||||
				min-width: 150px;
 | 
			
		||||
				max-width: 300px;
 | 
			
		||||
				width: calc(20% - var(--size-4-4) * 2);
 | 
			
		||||
				aspect-ratio: 2 / 3;
 | 
			
		||||
				background-color: var(--background-secondary);
 | 
			
		||||
				border-radius: var(--radius-l);
 | 
			
		||||
 | 
			
		||||
				&:has(.placeholder) {
 | 
			
		||||
					display: grid;
 | 
			
		||||
					place-items: center;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				.placeholder {
 | 
			
		||||
					font-size: 2rem;
 | 
			
		||||
					font-weight: bold;
 | 
			
		||||
					font-style: italic;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				img {
 | 
			
		||||
					width: 100%;
 | 
			
		||||
					height: 100%;
 | 
			
		||||
					object-fit: cover;
 | 
			
		||||
					border-radius: var(--radius-l);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ export function registerReadingCalendarCodeBlockProcessor(
 | 
			
		|||
): void {
 | 
			
		||||
	registerCodeBlockRenderer(
 | 
			
		||||
		plugin,
 | 
			
		||||
		"readingcalendar",
 | 
			
		||||
		"reading-calendar",
 | 
			
		||||
		(source, el) =>
 | 
			
		||||
			new SvelteCodeBlockRenderer(
 | 
			
		||||
				ReadingCalendarCodeBlockView,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,7 +24,9 @@
 | 
			
		|||
	const settingsStore = createSettings(plugin);
 | 
			
		||||
	setSettingsContext(settingsStore);
 | 
			
		||||
 | 
			
		||||
	const metadataStore = createMetadata(plugin, null);
 | 
			
		||||
	const metadataStore = createMetadata(plugin, {
 | 
			
		||||
		statusFilter: null,
 | 
			
		||||
	});
 | 
			
		||||
	setMetadataContext(metadataStore);
 | 
			
		||||
 | 
			
		||||
	const readingLog = createReadingLog(plugin.readingLog);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,7 @@ export function registerReadingLogCodeBlockProcessor(
 | 
			
		|||
): void {
 | 
			
		||||
	registerCodeBlockRenderer(
 | 
			
		||||
		plugin,
 | 
			
		||||
		"readinglog",
 | 
			
		||||
		"reading-log",
 | 
			
		||||
		(source, el) =>
 | 
			
		||||
			new SvelteCodeBlockRenderer(
 | 
			
		||||
				ReadingLogCodeBlockView,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,7 @@ export function registerReadingStatsCodeBlockProcessor(
 | 
			
		|||
): void {
 | 
			
		||||
	registerCodeBlockRenderer(
 | 
			
		||||
		plugin,
 | 
			
		||||
		"readingstats",
 | 
			
		||||
		"reading-stats",
 | 
			
		||||
		(source, el) =>
 | 
			
		||||
			new SvelteCodeBlockRenderer(
 | 
			
		||||
				ReadingStatsCodeBlockView,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,7 +26,10 @@
 | 
			
		|||
	const settingsStore = createSettings(plugin);
 | 
			
		||||
	setSettingsContext(settingsStore);
 | 
			
		||||
 | 
			
		||||
	const metadataStore = createMetadata(plugin, settings.statusFilter, true);
 | 
			
		||||
	const metadataStore = createMetadata(plugin, {
 | 
			
		||||
		statusFilter: settings.statusFilter,
 | 
			
		||||
		initialMonth: true,
 | 
			
		||||
	});
 | 
			
		||||
	setMetadataContext(metadataStore);
 | 
			
		||||
 | 
			
		||||
	let view = $state(settings.defaultView);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,7 +23,9 @@
 | 
			
		|||
	const settingsStore = createSettings(plugin);
 | 
			
		||||
	setSettingsContext(settingsStore);
 | 
			
		||||
 | 
			
		||||
	const metadataStore = createMetadata(plugin, null);
 | 
			
		||||
	const metadataStore = createMetadata(plugin, {
 | 
			
		||||
		statusFilter: null,
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	let editMode = $derived(entry !== undefined);
 | 
			
		||||
	let book = $state(entry?.book ?? "");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,17 +9,56 @@ export interface DateFilterStore {
 | 
			
		|||
	get filterMonths(): { label: string; value: number }[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface DateFilterStoreOptions {
 | 
			
		||||
	/**
 | 
			
		||||
	 * If true, the filter month will be set to the current month
 | 
			
		||||
	 * If a number is provided, the filter month will be set to the provided month
 | 
			
		||||
	 *
 | 
			
		||||
	 * @default false
 | 
			
		||||
	 */
 | 
			
		||||
	initialMonth?: boolean | number;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * If true, the filter year will be set to the current year
 | 
			
		||||
	 * If a number is provided, the filter year will be set to the provided year
 | 
			
		||||
	 *
 | 
			
		||||
	 * @default true
 | 
			
		||||
	 */
 | 
			
		||||
	initialYear?: boolean | number;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * If true, the month filter will be disabled
 | 
			
		||||
	 *
 | 
			
		||||
	 * @default false
 | 
			
		||||
	 */
 | 
			
		||||
	disableMonthFilter?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function createDateFilter<T>(
 | 
			
		||||
	data: () => T[],
 | 
			
		||||
	selector: (item: T) => Moment,
 | 
			
		||||
	initialMonth?: boolean
 | 
			
		||||
	{
 | 
			
		||||
		initialMonth = false,
 | 
			
		||||
		initialYear = true,
 | 
			
		||||
		disableMonthFilter = false,
 | 
			
		||||
	}: DateFilterStoreOptions
 | 
			
		||||
): DateFilterStore & {
 | 
			
		||||
	filteredData: T[];
 | 
			
		||||
} {
 | 
			
		||||
	const today = new Date();
 | 
			
		||||
	let filterYear: number | typeof ALL_TIME = $state(today.getFullYear());
 | 
			
		||||
	let filterYear: number | typeof ALL_TIME = $state(
 | 
			
		||||
		typeof initialYear === "number"
 | 
			
		||||
			? initialYear
 | 
			
		||||
			: initialYear
 | 
			
		||||
			? today.getFullYear()
 | 
			
		||||
			: ALL_TIME
 | 
			
		||||
	);
 | 
			
		||||
	let filterMonth: number | typeof ALL_TIME = $state(
 | 
			
		||||
		initialMonth ? today.getMonth() + 1 : ALL_TIME
 | 
			
		||||
		typeof initialMonth === "number"
 | 
			
		||||
			? initialMonth
 | 
			
		||||
			: initialMonth
 | 
			
		||||
			? today.getMonth() + 1
 | 
			
		||||
			: ALL_TIME
 | 
			
		||||
	);
 | 
			
		||||
	const filteredData = $derived.by(() => {
 | 
			
		||||
		return data()
 | 
			
		||||
| 
						 | 
				
			
			@ -30,7 +69,7 @@ export function createDateFilter<T>(
 | 
			
		|||
						return false;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if (filterMonth !== ALL_TIME) {
 | 
			
		||||
					if (filterMonth !== ALL_TIME && !disableMonthFilter) {
 | 
			
		||||
						if (date.month() !== filterMonth - 1) {
 | 
			
		||||
							return false;
 | 
			
		||||
						}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,11 @@ import {
 | 
			
		|||
	setSettingsContext,
 | 
			
		||||
} from "./settings.svelte";
 | 
			
		||||
import type BookTrackerPlugin from "@src/main";
 | 
			
		||||
import { createDateFilter, type DateFilterStore } from "./date-filter.svelte";
 | 
			
		||||
import {
 | 
			
		||||
	createDateFilter,
 | 
			
		||||
	type DateFilterStore,
 | 
			
		||||
	type DateFilterStoreOptions,
 | 
			
		||||
} from "./date-filter.svelte";
 | 
			
		||||
import type { ReadingState } from "@src/types";
 | 
			
		||||
import type { BookTrackerPluginSettings } from "@ui/settings";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -49,10 +53,16 @@ function getMetadata(
 | 
			
		|||
	return metadata;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface MetadataStoreOptions extends DateFilterStoreOptions {
 | 
			
		||||
	/**
 | 
			
		||||
	 * The reading state to filter by
 | 
			
		||||
	 */
 | 
			
		||||
	statusFilter?: ReadingState | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function createMetadata(
 | 
			
		||||
	plugin: BookTrackerPlugin,
 | 
			
		||||
	statusFilter: ReadingState | null = STATUS_READ,
 | 
			
		||||
	initialMonth?: boolean
 | 
			
		||||
	{ statusFilter = STATUS_READ, ...dateFilterOpts }: MetadataStoreOptions = {}
 | 
			
		||||
): MetadataStore {
 | 
			
		||||
	let settingsStore = getSettingsContext();
 | 
			
		||||
	if (!settingsStore) {
 | 
			
		||||
| 
						 | 
				
			
			@ -97,7 +107,7 @@ export function createMetadata(
 | 
			
		|||
				f.frontmatter[settingsStore.settings.endDateProperty]
 | 
			
		||||
			);
 | 
			
		||||
		},
 | 
			
		||||
		initialMonth
 | 
			
		||||
		dateFilterOpts
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	return {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,7 +18,7 @@ export function createReadingLog(readingLog: ReadingLog): ReadingLogStore {
 | 
			
		|||
	const dateFilter = createDateFilter(
 | 
			
		||||
		() => entries,
 | 
			
		||||
		(entry) => entry.createdAt,
 | 
			
		||||
		true
 | 
			
		||||
		{ initialMonth: true }
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	async function addEntry(entry: ReadingLogEntry): Promise<void> {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
{
 | 
			
		||||
	"1.0.0": "0.15.0",
 | 
			
		||||
	"1.1.0": "0.15.0"
 | 
			
		||||
	"1.1.0": "0.15.0",
 | 
			
		||||
	"1.2.0": "0.15.0"
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue