Compare commits

...

22 commits

Author SHA1 Message Date
mara.froehlich
53a4f03995 release version 1.1.0 2025-06-25 20:40:55 +02:00
mara.froehlich
253668b983 allow user to position image within container 2025-06-25 20:39:06 +02:00
mara.froehlich
0777249a18 release version 1.0.7 2025-01-27 19:13:06 +01:00
mara.froehlich
6f1d8be50b correctly format avatar view in reading mode 2025-01-27 19:04:29 +01:00
mara.froehlich
06e96c659e fix look of description 2025-01-27 18:56:56 +01:00
mara.froehlich
123a7a2907 fix overlapping text in certain situations 2025-01-27 18:48:09 +01:00
mara.froehlich
2daa4ef222 fix issue with ts build 2025-01-19 23:46:08 +01:00
mara.froehlich
9c74ec76d9 release version 1.0.6 2025-01-19 23:33:56 +01:00
mara.froehlich
ce254f6ab0 allow user to adjust size 2025-01-19 23:18:47 +01:00
mara.froehlich
20f7b30d75 allow user to configure side on which the avatar will be displayed. 2025-01-19 23:00:36 +01:00
froehlichA
410fa34cd0 release version 1.0.5 2023-03-22 22:43:25 +01:00
froehlichA
379781ee84 remove settings 2023-03-22 22:42:43 +01:00
froehlichA
c86808288e release version 1.0.4 2023-03-19 00:41:01 +01:00
froehlichA
cb53cace40 fix issue with view error in certain modes 2023-03-19 00:39:11 +01:00
froehlichA
fb75185a89 release version 1.0.3 2023-03-18 23:40:01 +01:00
froehlichA
3c1eb8b9a3 disable text area resizing 2023-03-18 23:37:12 +01:00
froehlichA
5cb07b20a9 fix placeholder not displaying on empty text 2023-03-18 23:34:44 +01:00
froehlichA
e9f0a8431d bump plugin version 2023-03-18 20:53:47 +01:00
froehlichA
f1167b9c99 fix authorUrl & remove fundingUrl 2023-03-18 20:51:58 +01:00
froehlichA
b99db73d18 add license 2023-03-18 20:35:47 +01:00
froehlichA
1debcb78da change id of plugin before initial release 2023-03-18 20:32:08 +01:00
froehlichA
ab0a7e3217 add installation instructions for the plugin 2023-03-18 20:30:47 +01:00
12 changed files with 207 additions and 80 deletions

21
LICENSE.md Normal file
View file

@ -0,0 +1,21 @@
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,8 +22,39 @@ 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,11 +1,10 @@
{
"id": "obsidian-avatar",
"id": "avatar",
"name": "Avatar",
"version": "1.0.0",
"version": "1.1.0",
"minAppVersion": "0.15.0",
"description": "Display an avatar image in front of your notes.",
"description": "Display an avatar image in your notes.",
"author": "froehlichA",
"authorUrl": "https://github.com/froehlichA/obsidian-avatar",
"fundingUrl": "https://github.com/froehlichA/obsidian-avatar",
"authorUrl": "https://github.com/froehlichA",
"isDesktopOnly": false
}
}

4
package-lock.json generated
View file

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

View file

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

View file

@ -4,16 +4,43 @@
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 type { SetState, State } from "../core/stateProviders";
import { SelectImageModal } from "./SelectImageModal";
import { AvatarPlugin } from "../plugin";
import { onMount } from "svelte";
interface AvatarViewState {
image: string;
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;
description: string;
}
@ -29,16 +56,34 @@
let descriptionEditEl: HTMLElement;
let descriptionPreviewEl: HTMLElement;
$: inSourceMode = app ? isSourceMode(app) : false;
let inSourceMode = 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;
function isSourceMode(app: App) {
const view = app.workspace.getMostRecentLeaf()?.view as MarkdownView;
return view?.getMode() === "source";
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 enterEditMode() {
if(inSourceMode && !editMode) {
if (inSourceMode && !editMode) {
editMode = true;
queueMicrotask(() => {
descriptionEditEl.focus();
@ -56,7 +101,7 @@
}
function updateImage() {
if(inSourceMode) {
if (inSourceMode) {
new SelectImageModal(app, (path) => {
setState((fm) => {
fm.image = path;
@ -67,33 +112,49 @@
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="flex">
<div class="avatar-view flex" class:reverse={state?.side === "right"}>
<div
class="avatar relative"
class="avatar-container 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" src={normalizeImgPath(state?.image) ?? fallbackImage} />
<img
class="avatar"
alt="Avatar"
style:width={`${avatarSize}px`}
style:height={`${avatarSize}px`}
style:object-position={objectPosition}
src={state.image ? normalizeImgPath(state?.image) : fallbackImage}
/>
{#if inSourceMode && hoverOnImage}
<Fab>
<ObsidianIcon id="edit"></ObsidianIcon>
</Fab>
{/if}
</div>
<div class="description" on:click={enterEditMode}>
<div
class="description relative"
on:click={enterEditMode}
class:editmode={!!editMode}
>
<textarea
class="textarea"
bind:this={descriptionEditEl}
@ -102,7 +163,7 @@
bind:value={state.description}
></textarea>
<span
class="avatar-plugin--md-preview"
class="avatar-plugin--md with-placeholder"
hidden={editMode}
bind:this={descriptionPreviewEl}
data-placeholder="Write your story..."
@ -123,20 +184,25 @@
justify-content: center;
}
.flex.reverse {
flex-direction: row-reverse;
}
@media (min-width: 992px) {
.flex {
flex-wrap: nowrap;
}
}
.relative {
position: relative;
}
.avatar {
.avatar-container {
flex: 0 0 auto;
width: 240px;
height: 240px;
}
.avatar {
object-fit: cover;
border-radius: 6px;
}
@ -144,16 +210,20 @@
.description {
flex: 1 1 auto;
word-break: break-word;
padding: 6px;
border-radius: 6px;
}
.description.editmode {
padding: 6px;
}
.textarea {
width: 100%;
height: 100%;
resize: none;
}
[contenteditable=true]:empty:not(:focus):before{
.with-placeholder:empty:before {
content: attr(data-placeholder);
color: var(--text-faint);
font-style: italic;

View file

@ -1,6 +1,7 @@
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;
@ -20,7 +21,7 @@ export function renderCodeBlockProcessor<S>(
) {
return (source: string, containerEl: HTMLElement, ctx: MarkdownPostProcessorContext) => {
const node = containerEl.createEl("div");
new component({
const svelteComponent = new component({
target: containerEl,
props: {
...props,
@ -28,5 +29,13 @@ export function renderCodeBlockProcessor<S>(
ctx
}
});
class UnloadSvelteComponent extends MarkdownRenderChild {
onunload() {
svelteComponent.$destroy();
}
}
ctx.addChild(new UnloadSvelteComponent(node));
}
}

View file

@ -1,13 +1,20 @@
import type {MarkdownPostProcessorContext} from "obsidian";
import type {CodeBlockProcessorProps} from "./renderCodeBlockProcessor";
import {parseYaml, stringifyYaml} from "obsidian";
import type { App, 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;
export type StateProvider<T> = (props: CodeBlockProcessorProps, source: string, node: HTMLElement, ctx: MarkdownPostProcessorContext) => {
state: State<T>,
setState: SetState<T>
declare let app: App;
export type StateProvider<T> = (
props: CodeBlockProcessorProps,
source: string,
node: HTMLElement,
ctx: MarkdownPostProcessorContext
) => {
state: State<T>;
setState: SetState<T>;
};
export function withCodeblockState<T>(): StateProvider<T> {
@ -18,12 +25,12 @@ export function withCodeblockState<T>(): StateProvider<T> {
} catch (_) {}
const setState: SetState<T> = (stateSetter) => {
let newState = { ...state };
const 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 },
@ -34,7 +41,7 @@ export function withCodeblockState<T>(): StateProvider<T> {
return {
state,
setState
}
setState,
};
};
}

View file

@ -1,35 +1,15 @@
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);
}
}

View file

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

View file

@ -2,6 +2,15 @@
float: right;
}
.avatar-plugin--md-preview > * {
margin-top: 0;
.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;
}

View file

@ -1,3 +1,11 @@
{
"1.0.0": "0.15.0"
}
"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"
}