Add cover image fit (#310)

* chore: bump version

* feat: add cover image fit setting

* feat: render cover image

* feat: add cover image to context menu

* refactor: add default fit into description

* fix: only show cover image options for markdown files
This commit is contained in:
DecafDev 2024-08-06 22:00:40 -06:00 committed by GitHub
parent 0c22fe5ca6
commit 2bf5c1302c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 545 additions and 13 deletions

View file

@ -1,7 +1,7 @@
{
"id": "vault-explorer",
"name": "Vault Explorer",
"version": "1.40.2",
"version": "1.41.0",
"minAppVersion": "1.4.13",
"description": "Explore your vault in visual format",
"author": "DecafDev",

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-vault-explorer",
"version": "1.40.2",
"version": "1.41.0",
"description": "Explore your vault in visual format",
"main": "main.js",
"scripts": {

View file

@ -10,6 +10,7 @@ export const DEFAULT_SETTINGS: VaultExplorerPluginSettings = {
favorite: "",
url: "",
image: "",
coverImageFit: "",
createdDate: "",
modifiedDate: "",
custom1: "",
@ -45,6 +46,7 @@ export const DEFAULT_SETTINGS: VaultExplorerPluginSettings = {
},
grid: {
isEnabled: true,
coverImageFit: "cover",
coverImageSources: [
{
type: "image-property",

View file

@ -10,6 +10,7 @@ export enum PluginEvent {
FOLDER_DELETE = "folder-delete",
FOLDER_CREATE = "folder-create",
PAGE_SIZE_SETTING_CHANGE = "page-size-setting-change",
COVER_IMAGE_FIT_SETTING_CHANGE = "cover-image-fit-setting-change",
FEED_CONTENT_SETTING_CHANGE = "feed-content-setting-change",
COVER_IMAGE_SOURCE_SETTING_CHANGE = "cover-image-source-setting-change",
PROPERTY_SETTING_CHANGE = "property-setting-change",

View file

@ -30,6 +30,7 @@ import Migrate_1_37_0 from "./migrate_1_37_0";
import Migrate_1_38_0 from "./migrate_1_38_0";
import Migrate_1_39_0 from "./migrate_1_39_0";
import Migrate_1_40_0 from "./migrate_1_40_0";
import Migrate_1_41_0 from "./migrate_1_41_0";
const migrations: TMigration[] = [
{
@ -177,6 +178,11 @@ const migrations: TMigration[] = [
to: "1.40.0",
migrate: Migrate_1_40_0,
},
{
from: "1.40.2",
to: "1.41.0",
migrate: Migrate_1_41_0,
},
];
export const preformMigrations = (

View file

@ -1,11 +1,11 @@
import MigrationInterface from "./migration_interface";
import { VaultExplorerPluginSettings_1_39_0 } from "src/types/types-1.39.0";
import { VaultExplorerPluginSettings } from "src/types";
import { VaultExplorerPluginSettings_1_40_2 } from "src/types/types-1.40.2";
export default class Migrate_1_40_0 implements MigrationInterface {
migrate(data: Record<string, unknown>) {
const typedData = data as unknown as VaultExplorerPluginSettings_1_39_0;
const newData: VaultExplorerPluginSettings = {
const newData: VaultExplorerPluginSettings_1_40_2 = {
...typedData,
shouldCollapseFilters: false,
};

View file

@ -0,0 +1,25 @@
import { VaultExplorerPluginSettings } from "src/types";
import MigrationInterface from "./migration_interface";
import { VaultExplorerPluginSettings_1_40_2 } from "src/types/types-1.40.2";
export default class Migrate_1_41_0 implements MigrationInterface {
migrate(data: Record<string, unknown>) {
const typedData = data as unknown as VaultExplorerPluginSettings_1_40_2;
const newData: VaultExplorerPluginSettings = {
...typedData,
properties: {
...typedData.properties,
coverImageFit: "",
},
views: {
...typedData.views,
grid: {
...typedData.views.grid,
coverImageFit: "cover",
},
},
};
return newData as unknown as Record<string, unknown>;
}
}

View file

@ -14,7 +14,7 @@ import {
} from "src/logger/constants";
import Logger from "js-logger";
import { stringToLogLevel } from "src/logger";
import { CollapseStyle, TExplorerView } from "src/types";
import { CollapseStyle, CoverImageFit, TExplorerView } from "src/types";
import EventManager from "src/event/event-manager";
import LicenseKeyApp from "../svelte/license-key-app/index.svelte";
import ImageSourceApp from "../svelte/image-source-app/index.svelte";
@ -312,6 +312,26 @@ export default class VaultExplorerSettingsTab extends PluginSettingTab {
})
);
new Setting(containerEl)
.setName("Cover image fit")
.setDesc("Set the default cover image fit")
.addDropdown((cb) =>
cb
.addOptions({
cover: "Cover",
contain: "Contain",
})
.setValue(this.plugin.settings.views.grid.coverImageFit)
.onChange(async (value) => {
this.plugin.settings.views.grid.coverImageFit =
value as CoverImageFit;
await this.plugin.saveSettings();
EventManager.getInstance().emit(
PluginEvent.COVER_IMAGE_FIT_SETTING_CHANGE
);
})
);
new Setting(containerEl).setName("List view").setHeading();
new Setting(containerEl)
@ -443,9 +463,9 @@ export default class VaultExplorerSettingsTab extends PluginSettingTab {
);
new Setting(containerEl)
.setName("Image property")
.setName("Cover image property")
.setDesc(
"Property used to store an image. This must be a text property."
"Property used to store a cover image. This must be a text property."
)
.addDropdown((dropdown) =>
dropdown
@ -460,6 +480,24 @@ export default class VaultExplorerSettingsTab extends PluginSettingTab {
})
);
new Setting(containerEl)
.setName("Cover image fit property")
.setDesc(
"Property used to store the cover image fit. This must be a text property."
)
.addDropdown((dropdown) =>
dropdown
.addOptions(getDropdownOptionsForProperties(textProperties))
.setValue(this.plugin.settings.properties.coverImageFit)
.onChange(async (value) => {
this.plugin.settings.properties.coverImageFit = value;
await this.plugin.saveSettings();
EventManager.getInstance().emit(
PluginEvent.PROPERTY_SETTING_CHANGE
);
})
);
new Setting(containerEl)
.setName("URL property")
.setDesc(

View file

@ -23,6 +23,7 @@
isSocialMediaImageEntryExpired,
putSocialMediaImageUrl,
} from "../services/social-media-image-cache";
import { CoverImageFit } from "src/types";
export let displayName: string;
export let path: string;
@ -35,6 +36,7 @@
export let custom2: string | null;
export let custom3: string | null;
export let isFavorite: boolean | null;
export let coverImageFit: CoverImageFit;
let plugin: VaultExplorerPlugin;
let enableFileIcons: boolean = false;
@ -136,11 +138,21 @@
dispatch("favoritePropertyChange", { filePath, value });
}
function handleCoverImageFitChange(filePath: string, value: CoverImageFit) {
dispatch("coverImageFitChange", { filePath, value });
}
function handleCardContextMenu(e: Event) {
const nativeEvent = e as MouseEvent;
const showCoverImageOptions = path.endsWith(".md");
openContextMenu(plugin, path, nativeEvent, {
isFavorite,
coverImageFit: showCoverImageOptions ? coverImageFit : undefined,
onFavoriteChange: handleFavoriteChange,
onCoverImageFitChange: showCoverImageOptions
? handleCoverImageFitChange
: undefined,
});
}
@ -208,7 +220,9 @@
<img
class="vault-explorer-grid-card__image"
src={imgSrc}
style="display: {isCoverImageLoaded ? 'block' : 'none'};"
style="display: {isCoverImageLoaded
? 'block'
: 'none'}; object-fit: {coverImageFit};"
on:load={handleImageLoad}
on:error={handleImageError}
/>
@ -335,7 +349,6 @@
.vault-explorer-grid-card__image {
width: 100%;
height: 150px;
object-fit: cover;
border-top-left-radius: var(--radius-m);
border-top-right-radius: var(--radius-m);
}

View file

@ -26,6 +26,7 @@
<GridCard
displayName={fileRenderData.displayName}
path={fileRenderData.path}
coverImageFit={fileRenderData.coverImageFit}
baseName={fileRenderData.baseName}
extension={fileRenderData.extension}
imageUrl={fileRenderData.imageUrl}
@ -36,6 +37,7 @@
custom3={fileRenderData.custom3}
isFavorite={fileRenderData.isFavorite}
on:favoritePropertyChange
on:coverImageFitChange
/>
{/each}
</div>

View file

@ -15,6 +15,7 @@
TSortFilter,
TTimestampFilter,
TExplorerView,
CoverImageFit,
} from "src/types";
import store from "../shared/services/store";
import VaultExplorerPlugin from "src/main";
@ -104,6 +105,7 @@
let frontmatterCacheTime: number = Date.now();
let propertySettingsTime: number = Date.now();
let coverImageSourcesTime: number = Date.now();
let coverImageFitTime: number = Date.now();
let loadBodyTagsTime: number = Date.now();
let loadedFiles: LoadedFile[] = [];
@ -471,6 +473,29 @@
};
});
onMount(() => {
function handleCoverImageFitSettingChange() {
Logger.trace({
fileName: "app/index.svelte",
functionName: "handleCoverImageFitSettingChange",
message: "called",
});
coverImageFitTime = Date.now();
}
EventManager.getInstance().on(
PluginEvent.COVER_IMAGE_FIT_SETTING_CHANGE,
handleCoverImageFitSettingChange,
);
return () => {
EventManager.getInstance().off(
PluginEvent.COVER_IMAGE_FIT_SETTING_CHANGE,
handleCoverImageFitSettingChange,
);
};
});
onMount(() => {
function handleCoverImageSourceSettingChange() {
Logger.trace({
@ -759,6 +784,43 @@
debounceFavoriteFilterChange(value);
}
function handleCoverImageFitChange(e: CustomEvent) {
const { filePath, value } = e.detail as {
filePath: string;
value: CoverImageFit;
};
console.log(filePath, value);
const { properties } = plugin.settings;
const { coverImageFit: coverImageFitProperty } = properties;
//If the favorite property is not set, return
if (coverImageFitProperty === "") {
new Notice(
"Vault Explorer: Please set a cover image fit property in the plugin settings to use this feature",
);
return;
}
const file = plugin.app.vault.getFileByPath(filePath);
if (!file) {
Logger.error({
fileName: "app/index.svelte",
functionName: "handleCoverImageFitChange",
message: "file not found. returning...",
});
return;
}
if (file.extension === "md") {
plugin.app.fileManager.processFrontMatter(file, (frontmatter) => {
frontmatter[coverImageFitProperty] = value;
return frontmatter;
});
}
}
function handleFavoritePropertyChange(e: CustomEvent) {
const { filePath, value } = e.detail as {
filePath: string;
@ -771,7 +833,7 @@
//If the favorite property is not set, return
if (favoritePropertyName === "") {
new Notice(
"Please select a favorite property in the Vault Explorer settings to use this feature",
"Vault Explorer: Please set a favorite property in the plugin settings to use this feature",
);
return;
}
@ -827,7 +889,12 @@
}
let formatted: FileRenderData[] = [];
$: if (propertySettingsTime || coverImageSourcesTime || loadBodyTagsTime) {
$: if (
propertySettingsTime ||
coverImageSourcesTime ||
loadBodyTagsTime ||
coverImageFitTime
) {
formatted = filteredCustom.map((loadedFile) => {
const { id, file } = loadedFile;
const frontmatter =
@ -1047,6 +1114,7 @@
{startIndex}
{pageLength}
on:favoritePropertyChange={handleFavoritePropertyChange}
on:coverImageFitChange={handleCoverImageFitChange}
/>
{:else if currentView === "list"}
<ListView

View file

@ -1,5 +1,6 @@
import { Menu } from "obsidian";
import VaultExplorerPlugin from "src/main";
import { CoverImageFit } from "src/types";
export const openContextMenu = (
plugin: VaultExplorerPlugin,
@ -7,10 +8,17 @@ export const openContextMenu = (
e: MouseEvent,
{
isFavorite,
coverImageFit,
onFavoriteChange,
onCoverImageFitChange,
}: {
isFavorite: boolean | null;
coverImageFit?: CoverImageFit;
onFavoriteChange: (filePath: string, value: boolean) => void;
onCoverImageFitChange?: (
filePath: string,
value: CoverImageFit
) => void;
}
) => {
const menu = new Menu();
@ -37,6 +45,19 @@ export const openContextMenu = (
onFavoriteChange(filePath, newValue);
});
});
if (coverImageFit !== undefined && onCoverImageFitChange !== undefined) {
menu.addSeparator();
menu.addItem((item) => {
item.setTitle("Cover image fit: cover");
item.setChecked(coverImageFit === "cover");
item.onClick(() => onCoverImageFitChange(filePath, "cover"));
});
menu.addItem((item) => {
item.setTitle("Cover image fit: contain");
item.setChecked(coverImageFit === "contain");
item.onClick(() => onCoverImageFitChange(filePath, "contain"));
});
}
menu.showAtMouseEvent(e);
};

View file

@ -1,5 +1,9 @@
import { App, FrontMatterCache, TFile } from "obsidian";
import { PropertyType, VaultExplorerPluginSettings } from "src/types";
import {
CoverImageFit,
PropertyType,
VaultExplorerPluginSettings,
} from "src/types";
import { FileRenderData } from "../types";
import Logger from "js-logger";
import {
@ -62,6 +66,7 @@ export const formatFileDataForRender = ({
modifiedDate: modifiedDateProp,
url: urlProp,
image: imageProp,
coverImageFit: coverImageFitProp,
favorite: favoriteProp,
custom1: custom1Prop,
custom2: custom2Prop,
@ -103,6 +108,15 @@ export const formatFileDataForRender = ({
isFavorite = fileFavorite;
}
let coverImageFit: CoverImageFit | null = loadPropertyValue<string>(
fileFrontmatter,
coverImageFitProp,
PropertyType.TEXT
) as CoverImageFit | null;
if (coverImageFit === null) {
coverImageFit = settings.views.grid.coverImageFit;
}
const creationDate: string | null = loadPropertyValue<string>(
fileFrontmatter,
createdDateProp,
@ -230,6 +244,7 @@ export const formatFileDataForRender = ({
displayName,
path,
baseName: basename,
coverImageFit,
basePath,
extension,
url,

View file

@ -1,3 +1,5 @@
import { CoverImageFit } from "src/types";
export interface FileRenderData {
id: string;
displayName: string;
@ -5,6 +7,7 @@ export interface FileRenderData {
basePath: string;
extension: string;
baseName: string;
coverImageFit: CoverImageFit;
content: string | null;
url: string | null;
imageUrl: string | null;

View file

@ -16,6 +16,7 @@ export function isVaultExplorerPluginSettings(obj: unknown): obj is VaultExplore
typeof typedObj["properties"]["favorite"] === "string" &&
typeof typedObj["properties"]["url"] === "string" &&
typeof typedObj["properties"]["image"] === "string" &&
typeof typedObj["properties"]["coverImageFit"] === "string" &&
typeof typedObj["properties"]["createdDate"] === "string" &&
typeof typedObj["properties"]["modifiedDate"] === "string" &&
typeof typedObj["properties"]["custom1"] === "string" &&
@ -606,6 +607,10 @@ export function isVaultExplorerPluginSettings(obj: unknown): obj is VaultExplore
e["type"] === "body") &&
typeof e["isEnabled"] === "boolean"
) &&
(typedObj["views"]["grid"]["coverImageFit"] === "cover" ||
typedObj["views"]["grid"]["coverImageFit"] === "contain" ||
typedObj["views"]["grid"]["coverImageFit"] === "scale-down" ||
typedObj["views"]["grid"]["coverImageFit"] === "none") &&
typeof typedObj["views"]["grid"]["loadSocialMediaImage"] === "boolean" &&
(typedObj["views"]["list"] !== null &&
typeof typedObj["views"]["list"] === "object" ||

View file

@ -4,6 +4,7 @@ export interface VaultExplorerPluginSettings {
favorite: string;
url: string;
image: string;
coverImageFit: string;
createdDate: string;
modifiedDate: string;
custom1: string;
@ -55,6 +56,7 @@ export interface TListView extends BaseView {
export interface TGridView extends BaseView {
coverImageSources: CoverImageSource[];
coverImageFit: CoverImageFit;
loadSocialMediaImage: boolean;
}
@ -63,6 +65,8 @@ export interface CoverImageSource {
isEnabled: boolean;
}
export type CoverImageFit = "cover" | "contain";
export type CoverImageSourceType =
| "image-property"
| "url-property"

328
src/types/types-1.40.2.ts Normal file
View file

@ -0,0 +1,328 @@
export interface VaultExplorerPluginSettings_1_40_2 {
properties: {
favorite: string;
url: string;
image: string;
createdDate: string;
modifiedDate: string;
custom1: string;
custom2: string;
custom3: string;
};
filters: {
search: TSearchFilter;
favorites: TFavoritesFilter;
sort: TSortFilter;
timestamp: TTimestampFilter;
custom: TCustomFilter;
};
views: {
dashboard: TDashboardView;
grid: TGridView;
list: TListView;
table: TTableView;
feed: TFeedView;
recommended: TRecommendedView;
related: TRelatedView;
};
titleWrapping: WordBreak;
enableClockUpdates: boolean;
enableFileIcons: boolean;
loadBodyTags: boolean;
currentView: TExplorerView | null;
pageSize: number;
filterGroupsWidth: string;
shouldWrapFilterGroups: boolean;
shouldCollapseFilters: boolean;
viewOrder: TExplorerView[];
configDir: string;
pluginVersion: string | null;
logLevel: string;
}
type FileInteractionStyle = "title" | "content";
interface BaseView {
isEnabled: boolean;
}
interface TTableView extends BaseView {}
interface TListView extends BaseView {
showTags: boolean;
}
interface TGridView extends BaseView {
coverImageSources: CoverImageSource[];
loadSocialMediaImage: boolean;
}
interface CoverImageSource {
type: CoverImageSourceType;
isEnabled: boolean;
}
type CoverImageSourceType =
| "image-property"
| "url-property"
| "frontmatter"
| "body";
interface TDashboardView extends BaseView {}
type CollapseStyle = "no-new-lines" | "no-extra-new-lines";
interface TFeedView extends BaseView {
collapseStyle: CollapseStyle;
removeH1: boolean;
lineClampSmall: number;
lineClampMedium: number;
lineClampLarge: number;
}
interface TRecommendedView extends BaseView {}
interface TRelatedView extends BaseView {}
interface BaseFilter {
isEnabled: boolean;
}
interface TSearchFilter extends BaseFilter {
value: string;
}
interface TFavoritesFilter extends BaseFilter {
value: boolean;
}
interface TSortFilter extends BaseFilter {
value: SortFilterOption;
}
interface TTimestampFilter extends BaseFilter {
value: TimestampFilterOption;
}
interface TCustomFilter extends BaseFilter {
selectedGroupId: string;
groups: TFilterGroup[];
}
type TimestampFilterOption =
| "created-today"
| "modified-today"
| "created-this-week"
| "modified-this-week"
| "created-2-weeks"
| "modified-2-weeks"
| "all";
type SortFilterOption =
| "file-name-asc"
| "file-name-desc"
| "modified-asc"
| "modified-desc"
| "random";
type WordBreak = "normal" | "break-word";
enum TExplorerView {
DASHBOARD = "dashboard",
GRID = "grid",
LIST = "list",
FEED = "feed",
TABLE = "table",
RECOMMENDED = "recommended",
RELATED = "related",
}
type FilterOperator = "and" | "or";
enum TextFilterCondition {
IS = "is",
IS_NOT = "is-not",
CONTAINS = "contains",
DOES_NOT_CONTAIN = "does-not-contain",
STARTS_WITH = "starts-with",
ENDS_WITH = "ends-with",
EXISTS = "exists",
DOES_NOT_EXIST = "does-not-exist",
}
enum ListFilterCondition {
CONTAINS = "contains",
DOES_NOT_CONTAIN = "does-not-contain",
EXISTS = "exists",
DOES_NOT_EXIST = "does-not-exist",
}
enum NumberFilterCondition {
IS_EQUAL = "is-equal",
IS_NOT_EQUAL = "is-not-equal",
IS_GREATER = "is-greater",
IS_LESS = "is-less",
IS_GREATER_OR_EQUAL = "is-greater-or-equal",
IS_LESS_OR_EQUAL = "is-less-or-equal",
EXISTS = "exists",
DOES_NOT_EXIST = "does-not-exist",
}
enum CheckboxFilterCondition {
IS = "is",
IS_NOT = "is-not",
EXISTS = "exists",
DOES_NOT_EXIST = "does-not-exist",
}
enum DateFilterCondition {
IS = "is",
IS_BEFORE = "is-before",
IS_AFTER = "is-after",
IS_ON_OR_BEFORE = "is-on-or-before",
IS_ON_OR_AFTER = "is-on-or-after",
EXISTS = "exists",
DOES_NOT_EXIST = "does-not-exist",
}
enum ContentFilterCondition {
CONTAINS = "contains",
DOES_NOT_CONTAIN = "does-not-contain",
IS_EMPTY = "is-empty",
IS_NOT_EMPTY = "is-not-empty",
}
enum FolderFilterCondition {
IS = "is",
IS_NOT = "is-not",
}
enum FileNameFilterCondition {
IS = "is",
IS_NOT = "is-not",
CONTAINS = "contains",
DOES_NOT_CONTAIN = "does-not-contain",
STARTS_WITH = "starts-with",
ENDS_WITH = "ends-with",
}
type FilterCondition =
| TextFilterCondition
| NumberFilterCondition
| DateFilterCondition
| CheckboxFilterCondition
| ListFilterCondition
| ContentFilterCondition
| FolderFilterCondition
| FileNameFilterCondition;
//This matches the Obsidian property types
enum PropertyType {
TEXT = "text",
NUMBER = "number",
LIST = "list",
CHECKBOX = "checkbox",
DATE = "date",
DATETIME = "datetime",
}
enum FilterRuleType {
PROPERTY = "property",
FOLDER = "folder",
FILE_NAME = "file-name",
CONTENT = "content",
}
enum DatePropertyFilterValue {
TODAY = "today",
TOMORROW = "tomorrow",
YESTERDAY = "yesterday",
ONE_WEEK_FROM_NOW = "one-week-from-now",
ONE_WEEK_AGO = "one-week-ago",
ONE_MONTH_FROM_NOW = "one-month-from-now",
ONE_MONTH_AGO = "one-month-ago",
CUSTOM = "custom",
}
interface BaseFilterRule {
id: string;
operator: FilterOperator;
type: FilterRuleType;
condition: FilterCondition;
isEnabled: boolean;
value: string;
matchWhenPropertyDNE: boolean;
}
interface TextPropertyFilterRule extends BaseFilterRule {
type: FilterRuleType.PROPERTY;
propertyType: PropertyType.TEXT;
propertyName: string;
condition: TextFilterCondition;
}
interface NumberPropertyFilterRule extends BaseFilterRule {
type: FilterRuleType.PROPERTY;
propertyType: PropertyType.NUMBER;
propertyName: string;
condition: NumberFilterCondition;
}
interface ListPropertyFilterRule extends BaseFilterRule {
type: FilterRuleType.PROPERTY;
propertyType: PropertyType.LIST;
propertyName: string;
condition: ListFilterCondition;
}
interface CheckboxPropertyFilterRule extends BaseFilterRule {
type: FilterRuleType.PROPERTY;
propertyType: PropertyType.CHECKBOX;
propertyName: string;
condition: CheckboxFilterCondition;
}
interface DatePropertyFilterRule extends BaseFilterRule {
type: FilterRuleType.PROPERTY;
propertyType: PropertyType.DATE | PropertyType.DATETIME;
propertyName: string;
condition: DateFilterCondition;
valueData: string;
}
interface FolderFilterRule extends BaseFilterRule {
type: FilterRuleType.FOLDER;
condition: FolderFilterCondition;
includeSubfolders: boolean;
}
interface FileNameFilterRule extends BaseFilterRule {
type: FilterRuleType.FILE_NAME;
condition: FileNameFilterCondition;
}
interface ContentFilterRule extends BaseFilterRule {
type: FilterRuleType.CONTENT;
condition: ContentFilterCondition;
}
type TFilterRule =
| PropertyFilterRule
| FolderFilterRule
| FileNameFilterRule
| ContentFilterRule;
type PropertyFilterRule =
| TextPropertyFilterRule
| NumberPropertyFilterRule
| ListPropertyFilterRule
| CheckboxPropertyFilterRule
| DatePropertyFilterRule;
interface TFilterGroup {
id: string;
name: string;
rules: TFilterRule[];
isEnabled: boolean;
isSticky: boolean;
}

View file

@ -134,5 +134,6 @@
"1.39.0": "1.4.13",
"1.40.0": "1.4.13",
"1.40.1": "1.4.13",
"1.40.2": "1.4.13"
"1.40.2": "1.4.13",
"1.41.0": "1.4.13"
}