Compare commits

..

No commits in common. "master" and "1.0.0" have entirely different histories.

12 changed files with 80 additions and 207 deletions

View file

@ -1,21 +0,0 @@
MIT License
Copyright © 2023 Alexander Fröhlich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -22,39 +22,8 @@ description: This will be displayed as the description!
```
````
**In live preview mode**, click on the image to change it _(then either select one from your vault, or paste in a URL to a picture online)_, or edit the description by clicking on it _(Don't forget to click the button to save your changes!)_
**In live preview mode**, click on the image to change it *(then either select one from your vault, or paste in a URL to a picture online)*, or edit the description by clicking on it *(Don't forget to click the button to save your changes!)*
![Edit Mode](./docs/avatar_edit.PNG)
The description editor supports all markdown features of Obsidian.
## More options
The following options are available through the YAML codeblock:
- `image?: string`
- A path or URL to an image. Optional, defaults to a picture of the file path initials.
- `description: string`
- The description displayed next to the image.
- `side?: "left" | "right"`
- The side on which the avatar will be displayed. Optional, defaults to `"left"`.
- `size?: "small" | "medium" | "large" | number`
- The size of the avatar. May be either `small` (180x180px), `medium` (240x240px), `large` (320x320px), or any custom width as a number. Optional, defaults to `medium`.
- `objectPosition?: string`
- How to position the image within the square container surrounding it. For example, `"left"` would show the left side of a landscape picture, `"right"` the right side. <small>Refer to https://developer.mozilla.org/en-US/docs/Web/CSS/object-position for more information.</small>
## Installation
### Using Obsidian
1. Open Obsidian
2. Open `Settings` -> `Community Plugins`
3. Click on `Turn on Community plugins` if prompted
4. Click on `Browse`, and search for `Avatar`
5. Install & enable the plugin
### Manual installation
1. Download the latest release .zip from the `Releases` section of this Github repository
2. Extract the .zip to your vault's plugin folder: `<vault>/.obsidian/plugins`
3. Reload Obsidian

View file

@ -1,10 +1,11 @@
{
"id": "avatar",
"id": "obsidian-avatar",
"name": "Avatar",
"version": "1.1.0",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Display an avatar image in your notes.",
"description": "Display an avatar image in front of your notes.",
"author": "froehlichA",
"authorUrl": "https://github.com/froehlichA",
"authorUrl": "https://github.com/froehlichA/obsidian-avatar",
"fundingUrl": "https://github.com/froehlichA/obsidian-avatar",
"isDesktopOnly": false
}
}

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "obsidian-avatar",
"version": "1.0.7",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "obsidian-avatar",
"version": "1.0.7",
"version": "1.0.0",
"license": "MIT",
"devDependencies": {
"@tsconfig/svelte": "^3.0.0",

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-avatar",
"version": "1.1.0",
"version": "1.0.0",
"description": "An obsidian plugin for displaying an avatar image in front of your notes. ",
"main": "main.js",
"scripts": {

View file

@ -4,43 +4,16 @@
MarkdownPostProcessorContext,
MarkdownRenderer,
MarkdownView,
TFile,
TFile
} from "obsidian";
import Fab from "./Fab.svelte";
import ObsidianIcon from "./ObsidianIcon.svelte";
import type { SetState, State } from "../core/stateProviders";
import { SelectImageModal } from "./SelectImageModal";
import { AvatarPlugin } from "../plugin";
import { onMount } from "svelte";
import type {SetState, State} from "../core/stateProviders";
import {SelectImageModal} from "./SelectImageModal";
import {AvatarPlugin} from "../plugin";
interface AvatarViewState {
image?: string;
/**
* The side on which the image will be displayed.
*
* @default "left"
*/
side?: "left" | "right";
/**
* The size of the avatar image.
*
* small: 180x180
* medium: 240x240
* large: 300x300
*
* @default "medium"
*/
size?: "small" | "medium" | "large" | number;
/**
* Alignment of the avatar image within the container.
*
* Corresponds to the `object-position` CSS property.
*
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/object-position
*
* @default "50% 50%"
*/
objectPosition?: string;
image: string;
description: string;
}
@ -56,34 +29,16 @@
let descriptionEditEl: HTMLElement;
let descriptionPreviewEl: HTMLElement;
let inSourceMode = false;
$: inSourceMode = app ? isSourceMode(app) : false;
$: fallbackImage = `https://ui-avatars.com/api/?name=${ctx?.sourcePath.split("/").at(-1) ?? "::"}&size=240`;
$: avatarSize = state.size
? typeof state.size === "string"
? {
small: 180,
medium: 240,
large: 300,
}[state.size]
: state.size
: 240;
$: objectPosition = state.objectPosition;
onMount(() => {
inSourceMode = isSourceMode();
let x = app.workspace.on("layout-change", () => {
inSourceMode = isSourceMode();
});
return () => app.workspace.offref(x);
});
function isSourceMode() {
const view = app.workspace.getActiveViewOfType(MarkdownView);
return view?.getMode?.() === "source";
function isSourceMode(app: App) {
const view = app.workspace.getMostRecentLeaf()?.view as MarkdownView;
return view?.getMode() === "source";
}
function enterEditMode() {
if (inSourceMode && !editMode) {
if(inSourceMode && !editMode) {
editMode = true;
queueMicrotask(() => {
descriptionEditEl.focus();
@ -101,7 +56,7 @@
}
function updateImage() {
if (inSourceMode) {
if(inSourceMode) {
new SelectImageModal(app, (path) => {
setState((fm) => {
fm.image = path;
@ -112,49 +67,33 @@
function normalizeImgPath(src: string): string {
const file = app.vault.getAbstractFileByPath(src);
if (file && file instanceof TFile) {
if(file && file instanceof TFile) {
return app.vault.getResourcePath(file);
}
return src;
}
$: if (descriptionPreviewEl && plugin && state && state?.description) {
descriptionPreviewEl.innerHTML = "";
MarkdownRenderer.renderMarkdown(
state.description,
descriptionPreviewEl,
ctx?.sourcePath ?? "",
plugin,
);
$: if(descriptionPreviewEl && plugin && state && state?.description) {
descriptionPreviewEl.innerHTML = '';
MarkdownRenderer.renderMarkdown(state.description, descriptionPreviewEl, ctx?.sourcePath ?? "", plugin);
}
</script>
<div class="avatar-view flex" class:reverse={state?.side === "right"}>
<div class="flex">
<div
class="avatar-container relative"
class="avatar relative"
on:click={updateImage}
on:mouseenter={() => (hoverOnImage = true)}
on:mouseleave={() => (hoverOnImage = false)}
on:mouseenter={() => hoverOnImage = true}
on:mouseleave={() => hoverOnImage = false}
>
<img
class="avatar"
alt="Avatar"
style:width={`${avatarSize}px`}
style:height={`${avatarSize}px`}
style:object-position={objectPosition}
src={state.image ? normalizeImgPath(state?.image) : fallbackImage}
/>
<img class="avatar" alt="Avatar" src={normalizeImgPath(state?.image) ?? fallbackImage} />
{#if inSourceMode && hoverOnImage}
<Fab>
<ObsidianIcon id="edit"></ObsidianIcon>
</Fab>
{/if}
</div>
<div
class="description relative"
on:click={enterEditMode}
class:editmode={!!editMode}
>
<div class="description" on:click={enterEditMode}>
<textarea
class="textarea"
bind:this={descriptionEditEl}
@ -163,7 +102,7 @@
bind:value={state.description}
></textarea>
<span
class="avatar-plugin--md with-placeholder"
class="avatar-plugin--md-preview"
hidden={editMode}
bind:this={descriptionPreviewEl}
data-placeholder="Write your story..."
@ -184,25 +123,20 @@
justify-content: center;
}
.flex.reverse {
flex-direction: row-reverse;
}
@media (min-width: 992px) {
.flex {
flex-wrap: nowrap;
}
}
.relative {
position: relative;
}
.avatar-container {
flex: 0 0 auto;
}
.avatar {
flex: 0 0 auto;
width: 240px;
height: 240px;
object-fit: cover;
border-radius: 6px;
}
@ -210,20 +144,16 @@
.description {
flex: 1 1 auto;
word-break: break-word;
border-radius: 6px;
}
.description.editmode {
padding: 6px;
border-radius: 6px;
}
.textarea {
width: 100%;
height: 100%;
resize: none;
}
.with-placeholder:empty:before {
[contenteditable=true]:empty:not(:focus):before{
content: attr(data-placeholder);
color: var(--text-faint);
font-style: italic;

View file

@ -1,7 +1,6 @@
import type {App, MarkdownPostProcessorContext, Plugin} from "obsidian";
import type {SvelteComponent} from "svelte";
import type {StateProvider} from "./stateProviders";
import {MarkdownRenderChild} from "obsidian";
export interface CodeBlockProcessorProps extends Record<string, any> {
app: App;
@ -21,7 +20,7 @@ export function renderCodeBlockProcessor<S>(
) {
return (source: string, containerEl: HTMLElement, ctx: MarkdownPostProcessorContext) => {
const node = containerEl.createEl("div");
const svelteComponent = new component({
new component({
target: containerEl,
props: {
...props,
@ -29,13 +28,5 @@ export function renderCodeBlockProcessor<S>(
ctx
}
});
class UnloadSvelteComponent extends MarkdownRenderChild {
onunload() {
svelteComponent.$destroy();
}
}
ctx.addChild(new UnloadSvelteComponent(node));
}
}

View file

@ -1,20 +1,13 @@
import type { App, MarkdownPostProcessorContext } from "obsidian";
import type { CodeBlockProcessorProps } from "./renderCodeBlockProcessor";
import { parseYaml, stringifyYaml } from "obsidian";
import type {MarkdownPostProcessorContext} from "obsidian";
import type {CodeBlockProcessorProps} from "./renderCodeBlockProcessor";
import {parseYaml, stringifyYaml} from "obsidian";
export type State<T> = Partial<T>;
export type SetState<T> = (setter: (state: Partial<T>) => void) => void;
declare let app: App;
export type StateProvider<T> = (
props: CodeBlockProcessorProps,
source: string,
node: HTMLElement,
ctx: MarkdownPostProcessorContext
) => {
state: State<T>;
setState: SetState<T>;
export type StateProvider<T> = (props: CodeBlockProcessorProps, source: string, node: HTMLElement, ctx: MarkdownPostProcessorContext) => {
state: State<T>,
setState: SetState<T>
};
export function withCodeblockState<T>(): StateProvider<T> {
@ -25,12 +18,12 @@ export function withCodeblockState<T>(): StateProvider<T> {
} catch (_) {}
const setState: SetState<T> = (stateSetter) => {
const newState = { ...state };
let newState = { ...state };
stateSetter(newState);
const newStateStr: string = stringifyYaml(newState);
const info = ctx.getSectionInfo(node);
if (info) {
if(info) {
app.workspace.activeEditor?.editor?.replaceRange(
newStateStr + "```",
{ line: info.lineStart + 1, ch: 0 },
@ -41,7 +34,7 @@ export function withCodeblockState<T>(): StateProvider<T> {
return {
state,
setState,
};
setState
}
};
}

View file

@ -1,15 +1,35 @@
import {Plugin} from 'obsidian';
import type {AvatarPluginSettings} from "./settings";
import {DEFAULT_SETTINGS} from "./settings";
import {renderCodeBlockProcessor} from "./core/renderCodeBlockProcessor";
import AvatarView from "./components/AvatarView.svelte";
import {withCodeblockState} from "./core/stateProviders";
export class AvatarPlugin extends Plugin {
settings: AvatarPluginSettings;
async onload() {
await this.loadSettings();
this.registerMarkdownCodeBlockProcessor("avatar", renderCodeBlockProcessor(
AvatarView,
{ app: this.app, plugin: this },
withCodeblockState()
));
}
onunload() {
}
async loadSettings() {
this.settings = {
...DEFAULT_SETTINGS,
...(await this.loadData())
};
}
async saveSettings() {
await this.saveData(this.settings);
}
}

7
src/settings.ts Normal file
View file

@ -0,0 +1,7 @@
export interface AvatarPluginSettings {
mySetting: string;
}
export const DEFAULT_SETTINGS: AvatarPluginSettings = {
mySetting: 'default'
}

View file

@ -2,15 +2,6 @@
float: right;
}
.markdown-preview-view .avatar-view {
margin-block-start: var(--p-spacing);
margin-block-end: var(--p-spacing);
}
.avatar-plugin--md > *:first-child {
margin-block-start: 0;
}
.avatar-plugin--md > *:last-child {
margin-block-end: 0;
.avatar-plugin--md-preview > * {
margin-top: 0;
}

View file

@ -1,11 +1,3 @@
{
"1.0.0": "0.15.0",
"1.0.1": "0.15.0",
"1.0.2": "0.15.0",
"1.0.3": "0.15.0",
"1.0.4": "0.15.0",
"1.0.5": "0.15.0",
"1.0.6": "0.15.0",
"1.0.7": "0.15.0",
"1.1.0": "0.15.0"
}
"1.0.0": "0.15.0"
}