Skip to content

Commit

Permalink
Merge branch 'release/v1.61.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
anisus committed Oct 25, 2024
2 parents 9d1a9de + 34625bb commit 5dabc25
Show file tree
Hide file tree
Showing 51 changed files with 1,358 additions and 266 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mucklet-client",
"version": "1.60.1",
"version": "1.61.0",
"description": "A web client for Mucklet.",
"repository": {
"type": "git",
Expand Down
56 changes: 46 additions & 10 deletions src/client/modules/main/addons/avatar/Avatar.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ class Avatar {
constructor(app, params) {
this.app = app;
let apiFilePath = app.props.apiFilePath;
this.avatarPattern = apiFilePath + 'core/char/avatar/{0}';
this.charImgPattern = apiFilePath + 'core/char/img/{0}';
this.roomImgPattern = apiFilePath + 'core/room/img/{0}';
this.avatarPattern = (v) => apiFilePath + 'core/char/avatar/' + v;
this.charImgPattern = (v) => apiFilePath + 'core/char/img/' + v;
this.roomImgPattern = (v) => apiFilePath + 'core/room/img/' + v;

this.app.require([], this._init.bind(this));
}
Expand All @@ -20,40 +20,76 @@ class Avatar {

/**
* Creates a new avatar component instance
* @param {Model|object} char Char model or object. Should contain properties id, name, surname, and optionally avatar.
* @param {Model|object} char Char or profile model or object.
* @param {object} [opt] Optional parameters.
* @param {object} [opt.char] Character object used to fetch initials in profile is not a character.
* @param {string} [opt.size] Avatar size. May be 'small', 'medium', 'large', or 'xlarge'.
* @param {string} [opt.property] Char property to get the image ID. Defaults to 'avatar'.
* @param {boolean} [opt.initials] Use initials if no image is available. Defaults to true.
* @param {string} [opt.placeholder] Placeholder image to use instead of initials. May be 'avatar', 'room', or 'area'.
* @param {boolean} [opt.modalOnClick] Flag if clicking on the image should show the full image in a modal.
* @param {(v: any) => string} [opt.resolve] Resolves the image href from the image property.
* @returns {Component} Avatar component.
*/
newAvatar(char, opt) {
return new AvatarComponent(char, Object.assign({ pattern: this.avatarPattern }, opt));
return new AvatarComponent(char, Object.assign({
resolve: this.avatarPattern,
placeholder: 'avatar',
initials: true,
}, opt));
}

/**
* Creates a new avatar component instance for char images.
* @param {Model|object} char Char or profile model or object.
* @param {object} [opt] Optional parameters.
* @param {object} [opt.char] Character object used to fetch initials in profile is not a character.
* @param {string} [opt.size] Avatar size. May be 'small', 'medium', 'large', or 'xlarge'.
* @param {string} [opt.property] Char property to get the image ID. Defaults to 'image'.
* @param {boolean} [opt.initials] Use initials if no image is available. Defaults to false.
* @param {string} [opt.placeholder] Placeholder image to use instead of initials. May be 'avatar'. Defaults to 'avatar'.
* @param {boolean} [opt.modalOnClick] Flag if clicking on the image should show the full image in a modal.
* @param {(v: any) => string} [opt.resolve] Resolves the image href from the image property.
* @returns {Component} Avatar component.
*/
newCharImg(char, opt) {
return new AvatarComponent(char, Object.assign({
pattern: this.charImgPattern,
resolve: this.charImgPattern,
placeholder: 'avatar',
property: 'image',
}, opt));
}

/**
* Creates a new avatar component instance for room images.
* @param {Model|object} room Room or profile model or object.
* @param {object} [opt] Optional parameters.
* @param {object} [opt.char] Character object used to fetch initials in profile is not a character.
* @param {string} [opt.size] Avatar size. May be 'small', 'medium', 'large', or 'xlarge'.
* @param {string} [opt.property] Char property to get the image ID. Defaults to 'room'.
* @param {string} [opt.placeholder] Placeholder image to use instead of initials. May be 'avatar'. Defaults to 'avatar'.
* @param {boolean} [opt.modalOnClick] Flag if clicking on the image should show the full image in a modal.
* @param {(v: any) => string} [opt.resolve] Resolves the image href from the image property.
* @returns {Component} Avatar component.
*/
newRoomImg(room, opt) {
return new AvatarComponent(room, Object.assign({
pattern: this.roomImgPattern,
resolve: this.roomImgPattern,
placeholder: 'room',
property: 'image',
}, opt));
}

charImgHref(char) {
return char.image ? this.charImgPattern.replace("{0}", char.image) : null;
return char.image ? this.charImgPattern(char.image) : null;
}

avatarHref(char) {
return char.avatar ? this.avatarPattern.replace("{0}", char.avatar) : null;
return char.avatar ? this.avatarPattern(char.avatar) : null;
}

roomImgHref(room) {
return room.image ? this.roomImgPattern.replace("{0}", room.image) : null;
return room.image ? this.roomImgPattern(room.image) : null;
}

}
Expand Down
103 changes: 66 additions & 37 deletions src/client/modules/main/addons/avatar/AvatarComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import { isResError } from 'resclient';
import { ModelListener, ModelTxt } from 'modapp-resource-component';
import Fader from 'components/Fader';
import Img from 'components/Img';
import FAIcon from 'components/FAIcon';
import ImgModal from 'classes/ImgModal';

let defaultPattern = '/file/core/char/avatar/{0}';
import { relistenResource } from 'utils/listenResource';

// Get character initials.
function getInitials(c) {
Expand All @@ -21,11 +19,15 @@ const sizeMap = {
};

const placeholderMap = {
avatar: '/img/avatar-l.png',
room: '/img/room-l.png',
area: '/img/area-l.png',
avatar: { img: '/img/avatar-l.png', err: '/img/avatar-error-l.png' },
room: { img: '/img/room-l.png', err: '/img/room-error-l.png' },
area: { img: '/img/area-l.png', err: '/img/area-error-l.png' },
};

function getHref(v) {
return v.href;
}

/**
* AvatarComponent is a character avatar component.
*/
Expand All @@ -35,11 +37,11 @@ class AvatarComponent extends Fader {
* Creates an instance of AvatarComponent
* @param {object} profile Character or profile object.
* @param {object} [opt] Optional parameters.
* @param {object} [opt.char] Character object used to fetch initials in profile is not a character.
* @param {object} [opt.char] Character object used to fetch initials if profile is not a character.
* @param {string} [opt.size] Avatar size. May be 'small', 'medium', or 'large.
* @param {string} [opt.pattern] URL pattern for the avatar. Should contain "{0}" to replace with the avatar ID.
* @param {string} [opt.property] Char property to get the image ID. Defaults to 'avatar'.
* @param {string} [opt.resolve] Resolves the image ID from the property. Defaults to v => v.
* @param {string} [opt.property] Char property to get the image object or ID. Defaults to 'avatar'.
* @param {boolean} [opt.initials] Use initials if no image is available. Defaults to false.
* @param {(v: object) => string} [opt.resolve] Resolves the image href from the image property. Defaults to: (v) => v?.href
* @param {string} [opt.placeholder] Placeholder image to use instead of initials. May be 'avatar', 'room', or 'area'.
* @param {boolean} [opt.modalOnClick] Flag if clicking on the image should show the full image in a modal.
*/
Expand All @@ -50,16 +52,17 @@ class AvatarComponent extends Fader {
this.char = opt.char || profile;
this.saturation = opt.saturation || 0.5;
this.lightness = opt.lightness || 0.33;
this.pattern = opt.pattern || defaultPattern;
this.property = opt.property || 'avatar';
this.initials = !!opt.initials;
this.placeholder = (opt.placeholder && placeholderMap[opt.placeholder]) || null;
this.modalOnClick = !!opt.modalOnClick;
this.query = sizeMap[opt.size] || sizeMap['medium'];
this.resolve = opt.resolve || (v => this.pattern.replace("{0}", v));
this.resolve = opt.resolve || getHref;
this.isError = isResError(profile);
this.model = null;

this._update = this._update.bind(this);
this.ml = new ModelListener(profile, this, this._changeHandler.bind(this));
this._setHue(this.char);
}

render(el) {
Expand All @@ -69,46 +72,72 @@ class AvatarComponent extends Fader {
}

unrender() {
this.model = relistenResource(this.model, null, this._update);
this.ml.onUnrender();
super.unrender();
}

setChar(char) {
this.char = char;
this.ml.setModel(char);
this._setHue(char);
return this;
}

_changeHandler(m, c, change) {
if (change && !change.hasOwnProperty(this.property)) return;

let imageId = m ? m[this.property] : null;
let src = imageId
? this.resolve(imageId) + this.query
: this.placeholder;

c.setComponent(m
? this.isError
? new FAIcon('times')
: src
? new Img(src, this.modalOnClick && imageId ? {
className: 'clickable',
events: {
click: c => {
if (!c.hasClass('placeholder')) {
new ImgModal(this.resolve(imageId)).open();
}
},
if (!change || change.hasOwnProperty(this.property)) {
this._update();
}
}

_update() {
let src = null;
let isError = this.isError;
let m = this.ml.getModel();
if (!isError) {
let v = m?.[this.property] || null;
if (v) {
this.model = relistenResource(this.model, v, this._update);
src = this.resolve(v);
isError = !src || v?.deleted;
}
}

this.setComponent(isError || !(src || this.initials)
? this.placeholder
? new Img(isError ? this.placeholder.err : this.placeholder.img)
: null
: src
? new Img(src + this.query, this.modalOnClick ? {
className: 'clickable',
errorPlaceholder: this.placeholder.err,
errorClassName: 'avatar--error',
events: {
click: c => {
if (!c.hasClass('avatar--error')) {
new ImgModal(src).open();
}
},
} : null)
: new ModelTxt(this.char || m, m => getInitials(m), { tagName: 'span' })
: null,
},
} : {
errorPlaceholder: this.placeholder.err,
})
: new ModelTxt(this.char || m, m => getInitials(m), { tagName: 'span' }),
);
if (isError || src || !this.initials) {
this._clearHue();
} else {
this._setHue(this.char);
}
}

_clearHue() {
if (this.initials) return;
this.setStyle('backgroundColor', null);
this.setStyle('color', null);
}

_setHue(char) {
if (this.placeholder) return;
if (!this.initials) return;

let h = 0;
if (!this.isError) {
Expand Down
2 changes: 1 addition & 1 deletion src/client/modules/main/addons/avatar/avatar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
width: 48px;
height: 48px;

&.clickable {
&.clickable:not(.avatar--error) {
cursor: pointer;
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/client/modules/main/addons/charFocus/CharFocus.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,10 @@ class CharFocus {
let f = this.focusChars[c.id]?.props;
if (!f) return null;

let list = Object.keys(f).map(k => f[k]);
list.filter(c => c?.name).sort((a, b) => a.name.localeCompare(b.name) || a.surname.localeCompare(b.surname));
return list;
return Object.keys(f)
.map(k => f[k])
.filter(c => c?.name)
.sort((a, b) => a.name.localeCompare(b.name) || a.surname.localeCompare(b.surname));
});
this.style = document.createElement('style');
document.head.appendChild(this.style);
Expand Down
2 changes: 1 addition & 1 deletion src/client/modules/main/addons/exportLog/htmlTemplate.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const bodyEnd = `</div></body>`;
export const htmlEnd = `</html>`;

export const style = `
body { font-family: "Open Sans", sans-serif; font-size: 16px; color: #7d818c; background-color: #161926; margin: 0; line-height: 125%; padding: 8px 16px; }
body { font-family: "Open Sans", sans-serif; font-size: 16px; color: #7d818c; background-color: #161926; margin: 0; line-height: 1.4em; padding: 8px 16px; }
.cont {
width: 100%;
max-width: 720px;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Elem, Txt, Html } from 'modapp-base-component';
import l10n from 'modapp-l10n';

const imageTxt = {
title: l10n.l('playerEventImageWipe.imageRemoved', "Image removed"),
default: l10n.l('playerEventImageWipe.imageRemovedInfo', "An image has been removed:"),
target: {
char: l10n.l('playerEventImageWipe.charImageRemovedInfo', "An image has been removed from your character:"),
room: l10n.l('playerEventImageWipe.roomImageRemovedInfo', "An image has been removed from your room:"),
area: l10n.l('playerEventImageWipe.areaImageRemovedInfo', "An image has been removed from your area:"),
},
};
const avatarTxt = {
title: l10n.l('playerEventImageWipe.avatarRemoved', "Avatar removed"),
default: l10n.l('playerEventImageWipe.avatarRemovedInfo', "An avatar has been removed:"),
target: {
char: l10n.l('playerEventImageWipe.charAvatarRemovedInfo', "An avatar has been removed from your character:"),
// room: l10n.l('playerEventImageWipe.roomAvatarRemovedInfo', "An avatar has been removed from your room:"),
// area: l10n.l('playerEventImageWipe.areaAvatarRemovedInfo', "An avatar has been removed from your area:"),
},
};

const moreInfoTxt = l10n.l('playerEventImageWipe.moreInfo', `<div class="pad-bottom-m">If you have questions or objections, get in contact with the moderator team. Type:</div>` +
`<div class="charlog--code"><code>help helpme</code></div>`);

/**
* PlayerEventImageWipe registers the imageWipe playerEvent handler.
*/
class PlayerEventImageWipe {
constructor(app, params) {
this.app = app;

// Bind callbacks
this._handleEvent = this._handleEvent.bind(this);

this.app.require([ 'playerEvent', 'toaster' ], this._init.bind(this));
}

_init(module) {
this.module = module;

this.module.playerEvent.addHandler('imageWipe', this._handleEvent);
}

_handleEvent(ev, onClose) {
let txt = ev.avatar ? avatarTxt : imageTxt;
let info = txt.target[ev.target] || txt.default;
return this.module.toaster.open({
title: txt.title,
content: close => new Elem(n => n.elem('div', [
n.component(new Txt(info, { tagName: 'p' })),
n.component(new Txt(ev.name, { tagName: 'p', className: 'dialog--strong dialog--large' })),
n.component(new Html(moreInfoTxt, { tagName: 'div', className: 'common--sectionpadding' })),
])),
closeOn: 'click',
onClose,
});
}

dispose() {
this.module.playerEvent.removeHandler('imageWipe');
}
}

export default PlayerEventImageWipe;
Loading

0 comments on commit 5dabc25

Please sign in to comment.