Revert codestyle changes (#40)

* Revert "7f7efbb94bab8574e42d942ad82d414e007b2970"

Code style changes should probably be part of a PR
This commit is contained in:
Eliot Partridge
2019-08-30 10:53:14 -05:00
committed by GitHub
parent eefd9e9a2c
commit caf70a864f
446 changed files with 70667 additions and 70655 deletions
@@ -8,77 +8,77 @@ import { ACTIONS_LIMIT } from '../../../common/constants';
import { last } from '../../../common/utils';
@Component({
selector: 'action-bar',
templateUrl: 'action-bar.pug',
styleUrls: ['action-bar.scss'],
selector: 'action-bar',
templateUrl: 'action-bar.pug',
styleUrls: ['action-bar.scss'],
})
export class ActionBar {
@ViewChild('scroller', { static: true }) scroller!: ElementRef;
@Input() blurred = false;
activeAction: ButtonAction | undefined = undefined;
shortcuts = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '='];
private _editable = false;
constructor(private game: PonyTownGame, private settings: SettingsService) {
}
@Input() get editable() {
return this._editable;
}
set editable(value) {
if (this._editable !== value) {
this._editable = value;
this.updateFreeSlots();
@ViewChild('scroller', { static: true }) scroller!: ElementRef;
@Input() blurred = false;
activeAction: ButtonAction | undefined = undefined;
shortcuts = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '='];
private _editable = false;
constructor(private game: PonyTownGame, private settings: SettingsService) {
}
@Input() get editable() {
return this._editable;
}
set editable(value) {
if (this._editable !== value) {
this._editable = value;
this.updateFreeSlots();
if (!value) {
this.save();
}
}
}
get actions() {
return this.game.actions;
}
get mobile() {
return isMobile;
}
get hasScroller() {
return this.editable && isMobile;
}
get blurCount() {
const boxWidth = isMobile ? 50 : 40;
const width = 450 + this.scroller.nativeElement.scrollLeft;
return Math.floor(width / boxWidth);
}
use(action: ButtonAction | undefined) {
useAction(this.game, action);
}
drag(index: number) {
this.actions[index].action = undefined;
this.updateFreeSlots();
}
drop(action: ButtonAction | undefined, index: number) {
this.actions[index].action = action;
this.updateFreeSlots();
}
save() {
const settings = { ...this.settings.account, actions: serializeActions(this.actions) };
this.settings.saveAccountSettings(settings);
}
scroll(e: MouseWheelEvent) {
if (e.deltaY) {
const delta = e.deltaY > 0 ? 1 : -1;
this.scroller.nativeElement.scrollLeft += delta * 20;
}
}
private updateFreeSlots() {
const actions = this.actions;
if (!value) {
this.save();
}
}
}
get actions() {
return this.game.actions;
}
get mobile() {
return isMobile;
}
get hasScroller() {
return this.editable && isMobile;
}
get blurCount() {
const boxWidth = isMobile ? 50 : 40;
const width = 450 + this.scroller.nativeElement.scrollLeft;
return Math.floor(width / boxWidth);
}
use(action: ButtonAction | undefined) {
useAction(this.game, action);
}
drag(index: number) {
this.actions[index].action = undefined;
this.updateFreeSlots();
}
drop(action: ButtonAction | undefined, index: number) {
this.actions[index].action = action;
this.updateFreeSlots();
}
save() {
const settings = { ...this.settings.account, actions: serializeActions(this.actions) };
this.settings.saveAccountSettings(settings);
}
scroll(e: MouseWheelEvent) {
if (e.deltaY) {
const delta = e.deltaY > 0 ? 1 : -1;
this.scroller.nativeElement.scrollLeft += delta * 20;
}
}
private updateFreeSlots() {
const actions = this.actions;
if (this.editable) {
while (actions.length < 5 || (last(actions)!.action !== undefined && actions.length < ACTIONS_LIMIT)) {
actions.push({ action: undefined });
}
} else {
while (actions.length > 0 && last(actions)!.action === undefined) {
actions.pop();
}
}
}
if (this.editable) {
while (actions.length < 5 || (last(actions)!.action !== undefined && actions.length < ACTIONS_LIMIT)) {
actions.push({ action: undefined });
}
} else {
while (actions.length > 0 && last(actions)!.action === undefined) {
actions.pop();
}
}
}
}
@@ -5,42 +5,42 @@ import { drawAction } from '../../../client/buttonActions';
import { removeItem } from '../../../common/utils';
@Component({
selector: 'action-button',
templateUrl: 'action-button.pug',
styleUrls: ['action-button.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[class.empty]': '!editable && !action',
},
selector: 'action-button',
templateUrl: 'action-button.pug',
styleUrls: ['action-button.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[class.empty]': '!editable && !action',
},
})
export class ActionButton {
@Input() action?: ButtonAction;
@Input() editable = false;
@Input() active = false;
@Input() shadow = true;
@Input() shortcut = '';
@Output() use = new EventEmitter<ButtonAction>();
@ViewChild('canvas', { static: true }) canvas!: ElementRef;
dirty = true;
private state: any = {};
constructor(private game: PonyTownGame) {
}
ngOnInit() {
actionButtons.push(this);
}
ngOnDestroy() {
removeItem(actionButtons, this);
}
ngOnChanges() {
this.dirty = true;
}
click() {
if (this.action) {
this.use.emit(this.action);
}
}
draw() {
drawAction(this.canvas.nativeElement, this.action, this.state, this.game);
this.dirty = false;
}
@Input() action?: ButtonAction;
@Input() editable = false;
@Input() active = false;
@Input() shadow = true;
@Input() shortcut = '';
@Output() use = new EventEmitter<ButtonAction>();
@ViewChild('canvas', { static: true }) canvas!: ElementRef;
dirty = true;
private state: any = {};
constructor(private game: PonyTownGame) {
}
ngOnInit() {
actionButtons.push(this);
}
ngOnDestroy() {
removeItem(actionButtons, this);
}
ngOnChanges() {
this.dirty = true;
}
click() {
if (this.action) {
this.use.emit(this.action);
}
}
draw() {
drawAction(this.canvas.nativeElement, this.action, this.state, this.game);
this.dirty = false;
}
}
@@ -1,13 +1,13 @@
import { Component, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import {
createButtionActionActions, expressionButtonAction, createButtonCommandActions,
createDefaultButtonActions, actionExpressionDefaultPalette, entityButtonAction
createButtionActionActions, expressionButtonAction, createButtonCommandActions,
createDefaultButtonActions, actionExpressionDefaultPalette, entityButtonAction
} from '../../../client/buttonActions';
import * as sprites from '../../../generated/sprites';
import {
Eye, Muzzle, ColorExtraSet, ColorExtra, Iris, ExpressionExtra, PonyEye, ButtonAction, Action,
ButtonActionSlot, EntityButtonAction
Eye, Muzzle, ColorExtraSet, ColorExtra, Iris, ExpressionExtra, PonyEye, ButtonAction, Action,
ButtonActionSlot, EntityButtonAction
} from '../../../common/interfaces';
import { createExpression } from '../../../client/clientUtils';
import { ACTION_EXPRESSION_BG, ACTION_EXPRESSION_EYE_COLOR, fillToOutline } from '../../../common/colors';
@@ -18,137 +18,137 @@ import { PonyTownGame } from '../../../client/game';
import { getEntityNames } from '../../services/model';
function eyeSprite(e: PonyEye | undefined) {
return createEyeSprite(e, 0, sprites.defaultPalette);
return createEyeSprite(e, 0, sprites.defaultPalette);
}
@Component({
selector: 'actions-modal',
templateUrl: 'actions-modal.pug',
styleUrls: ['actions-modal.scss'],
selector: 'actions-modal',
templateUrl: 'actions-modal.pug',
styleUrls: ['actions-modal.scss'],
})
export class ActionsModal implements OnInit, OnDestroy {
readonly lockIcon = faLock;
readonly actionsIcon = faApple;
readonly expressionsIcon = faLaughBeam;
readonly chatIcon = faComment;
readonly optionsIcon = faCog;
readonly devIcon = faCogs;
readonly dev = BETA;
@Output() close = new EventEmitter();
actions = createButtionActionActions();
commands = createButtonCommandActions();
emoteAction = expressionButtonAction(createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Smile));
entityAction = entityButtonAction('apple');
entityActions: EntityButtonAction[] = [];
entityName = 'apple';
lockEyes = true;
lockIrises = true;
eyesLeft: ColorExtraSet = sprites.eyeLeft.map(e => e && e[0]).map(eyeSprite);
eyesRight: ColorExtraSet = sprites.eyeRight.map(e => e && e[0]).map(eyeSprite);
irisesLeft: ColorExtraSet = times(Iris.COUNT, i => createEyeSprite(sprites.eyeLeft[1]![0]!, i, sprites.defaultPalette));
irisesRight: ColorExtraSet = times(Iris.COUNT, i => createEyeSprite(sprites.eyeRight[1]![0]!, i, sprites.defaultPalette));
muzzles: ColorExtraSet = sprites.noses
.map(n => n[0][0])
.map(({ color, colors, mouth }) => ({
color, colors, extra: mouth, palettes: [actionExpressionDefaultPalette.colors]
} as ColorExtra));
noseFills = [ACTION_EXPRESSION_BG];
noseOutlines = [fillToOutline(ACTION_EXPRESSION_BG)];
coatFill = ACTION_EXPRESSION_BG;
eyeColor = ACTION_EXPRESSION_EYE_COLOR;
muzzle: Muzzle = 0;
eyeLeft: Eye = 1;
eyeRight: Eye = 1;
irisLeft: Iris = 0;
irisRight: Iris = 0;
tabIndex = 0;
blush = false;
sleeping = false;
tears = false;
crying = false;
hearts = false;
activeTab = 'right-eye';
private interval: any = 0;
private subscription?: Subscription;
private actionsToUndo: ButtonActionSlot[][] = [];
constructor(private game: PonyTownGame) {
this.updateEmoteAction();
}
ngOnInit() {
document.body.classList.add('actions-modal-opened');
this.game.editingActions = true;
this.interval = setInterval(() => this.game.send(server => server.action(Action.KeepAlive)), 10000);
this.subscription = this.game.onLeft.subscribe(() => this.ok());
readonly lockIcon = faLock;
readonly actionsIcon = faApple;
readonly expressionsIcon = faLaughBeam;
readonly chatIcon = faComment;
readonly optionsIcon = faCog;
readonly devIcon = faCogs;
readonly dev = BETA;
@Output() close = new EventEmitter();
actions = createButtionActionActions();
commands = createButtonCommandActions();
emoteAction = expressionButtonAction(createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Smile));
entityAction = entityButtonAction('apple');
entityActions: EntityButtonAction[] = [];
entityName = 'apple';
lockEyes = true;
lockIrises = true;
eyesLeft: ColorExtraSet = sprites.eyeLeft.map(e => e && e[0]).map(eyeSprite);
eyesRight: ColorExtraSet = sprites.eyeRight.map(e => e && e[0]).map(eyeSprite);
irisesLeft: ColorExtraSet = times(Iris.COUNT, i => createEyeSprite(sprites.eyeLeft[1]![0]!, i, sprites.defaultPalette));
irisesRight: ColorExtraSet = times(Iris.COUNT, i => createEyeSprite(sprites.eyeRight[1]![0]!, i, sprites.defaultPalette));
muzzles: ColorExtraSet = sprites.noses
.map(n => n[0][0])
.map(({ color, colors, mouth }) => ({
color, colors, extra: mouth, palettes: [actionExpressionDefaultPalette.colors]
} as ColorExtra));
noseFills = [ACTION_EXPRESSION_BG];
noseOutlines = [fillToOutline(ACTION_EXPRESSION_BG)];
coatFill = ACTION_EXPRESSION_BG;
eyeColor = ACTION_EXPRESSION_EYE_COLOR;
muzzle: Muzzle = 0;
eyeLeft: Eye = 1;
eyeRight: Eye = 1;
irisLeft: Iris = 0;
irisRight: Iris = 0;
tabIndex = 0;
blush = false;
sleeping = false;
tears = false;
crying = false;
hearts = false;
activeTab = 'right-eye';
private interval: any = 0;
private subscription?: Subscription;
private actionsToUndo: ButtonActionSlot[][] = [];
constructor(private game: PonyTownGame) {
this.updateEmoteAction();
}
ngOnInit() {
document.body.classList.add('actions-modal-opened');
this.game.editingActions = true;
this.interval = setInterval(() => this.game.send(server => server.action(Action.KeepAlive)), 10000);
this.subscription = this.game.onLeft.subscribe(() => this.ok());
if (BETA) {
this.entityActions = getEntityNames().map(name => entityButtonAction(name));
}
}
ngOnDestroy() {
document.body.classList.remove('actions-modal-opened');
this.game.editingActions = false;
clearInterval(this.interval);
this.subscription && this.subscription.unsubscribe();
}
ok() {
this.close.emit();
}
changed(locked: boolean) {
if (locked) {
this.eyeLeft = this.eyeRight;
}
if (BETA) {
this.entityActions = getEntityNames().map(name => entityButtonAction(name));
}
}
ngOnDestroy() {
document.body.classList.remove('actions-modal-opened');
this.game.editingActions = false;
clearInterval(this.interval);
this.subscription && this.subscription.unsubscribe();
}
ok() {
this.close.emit();
}
changed(locked: boolean) {
if (locked) {
this.eyeLeft = this.eyeRight;
}
if (this.lockIrises) {
this.irisLeft = this.irisRight;
}
if (this.lockIrises) {
this.irisLeft = this.irisRight;
}
this.updateEmoteAction();
}
drop(action: ButtonAction) {
if (action.type === 'expression' && action.expression) {
const e = action.expression;
this.lockEyes = e.right === e.left;
this.lockIrises = e.rightIris === e.leftIris;
this.eyeRight = e.right;
this.eyeLeft = e.left;
this.muzzle = e.muzzle;
this.irisRight = e.rightIris;
this.irisLeft = e.leftIris;
this.blush = hasFlag(e.extra, ExpressionExtra.Blush);
this.sleeping = hasFlag(e.extra, ExpressionExtra.Zzz);
this.tears = hasFlag(e.extra, ExpressionExtra.Tears);
this.crying = hasFlag(e.extra, ExpressionExtra.Cry);
this.hearts = hasFlag(e.extra, ExpressionExtra.Hearts);
this.changed(this.lockEyes);
}
}
updateEmoteAction() {
const extra =
(this.blush ? ExpressionExtra.Blush : 0) |
(this.sleeping ? ExpressionExtra.Zzz : 0) |
(this.tears ? ExpressionExtra.Tears : 0) |
(this.crying ? ExpressionExtra.Cry : 0) |
(this.hearts ? ExpressionExtra.Hearts : 0);
this.updateEmoteAction();
}
drop(action: ButtonAction) {
if (action.type === 'expression' && action.expression) {
const e = action.expression;
this.lockEyes = e.right === e.left;
this.lockIrises = e.rightIris === e.leftIris;
this.eyeRight = e.right;
this.eyeLeft = e.left;
this.muzzle = e.muzzle;
this.irisRight = e.rightIris;
this.irisLeft = e.leftIris;
this.blush = hasFlag(e.extra, ExpressionExtra.Blush);
this.sleeping = hasFlag(e.extra, ExpressionExtra.Zzz);
this.tears = hasFlag(e.extra, ExpressionExtra.Tears);
this.crying = hasFlag(e.extra, ExpressionExtra.Cry);
this.hearts = hasFlag(e.extra, ExpressionExtra.Hearts);
this.changed(this.lockEyes);
}
}
updateEmoteAction() {
const extra =
(this.blush ? ExpressionExtra.Blush : 0) |
(this.sleeping ? ExpressionExtra.Zzz : 0) |
(this.tears ? ExpressionExtra.Tears : 0) |
(this.crying ? ExpressionExtra.Cry : 0) |
(this.hearts ? ExpressionExtra.Hearts : 0);
const expression = createExpression(this.eyeRight, this.eyeLeft, this.muzzle, this.irisRight, this.irisLeft, extra);
this.emoteAction = expressionButtonAction(expression);
}
resetToDefault() {
this.actionsToUndo.push(this.game.actions);
this.game.actions = [...createDefaultButtonActions(), { action: undefined }];
}
clearActionBar() {
this.actionsToUndo.push(this.game.actions);
this.game.actions = this.game.actions.map(() => ({ action: undefined }));
}
undo() {
if (this.actionsToUndo.length) {
this.game.actions = this.actionsToUndo.pop()!;
}
}
updateEntity() {
if (BETA) {
this.entityAction = entityButtonAction(this.entityName);
}
}
const expression = createExpression(this.eyeRight, this.eyeLeft, this.muzzle, this.irisRight, this.irisLeft, extra);
this.emoteAction = expressionButtonAction(expression);
}
resetToDefault() {
this.actionsToUndo.push(this.game.actions);
this.game.actions = [...createDefaultButtonActions(), { action: undefined }];
}
clearActionBar() {
this.actionsToUndo.push(this.game.actions);
this.game.actions = this.game.actions.map(() => ({ action: undefined }));
}
undo() {
if (this.actionsToUndo.length) {
this.game.actions = this.actionsToUndo.pop()!;
}
}
updateEntity() {
if (BETA) {
this.entityAction = entityButtonAction(this.entityName);
}
}
}
@@ -2,44 +2,44 @@ import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges } from
import { parseColor, colorToCSS } from '../../../common/color';
@Component({
selector: 'bitmap-box',
templateUrl: 'bitmap-box.pug',
styleUrls: ['bitmap-box.scss'],
selector: 'bitmap-box',
templateUrl: 'bitmap-box.pug',
styleUrls: ['bitmap-box.scss'],
})
export class BitmapBox implements OnChanges {
@Input() width = 5;
@Input() height = 5;
@Input() bitmap?: string[];
@Input() tool?: string;
@Input() color = 'red';
@Output() colorChange = new EventEmitter<string>();
rows?: number[][];
ngOnChanges(changes: SimpleChanges) {
if (changes.width || changes.height) {
this.rows = [];
@Input() width = 5;
@Input() height = 5;
@Input() bitmap?: string[];
@Input() tool?: string;
@Input() color = 'red';
@Output() colorChange = new EventEmitter<string>();
rows?: number[][];
ngOnChanges(changes: SimpleChanges) {
if (changes.width || changes.height) {
this.rows = [];
for (let y = 0; y < this.height; y++) {
this.rows[y] = [];
for (let y = 0; y < this.height; y++) {
this.rows[y] = [];
for (let x = 0; x < this.width; x++) {
this.rows[y][x] = x + this.width * y;
}
}
}
}
draw(index: number) {
if (this.bitmap) {
if (this.tool === 'eraser') {
this.bitmap[index] = '';
} else if (this.tool === 'brush') {
this.bitmap[index] = parseColor(this.bitmap[index]) === parseColor(this.color) ? '' : this.color;
} else if (this.tool === 'eyedropper') {
this.color = this.bitmap[index];
this.colorChange.emit(this.color);
}
}
}
colorAt(index: number) {
return this.bitmap && this.bitmap[index] ? colorToCSS(parseColor(this.bitmap[index])) : '';
}
for (let x = 0; x < this.width; x++) {
this.rows[y][x] = x + this.width * y;
}
}
}
}
draw(index: number) {
if (this.bitmap) {
if (this.tool === 'eraser') {
this.bitmap[index] = '';
} else if (this.tool === 'brush') {
this.bitmap[index] = parseColor(this.bitmap[index]) === parseColor(this.color) ? '' : this.color;
} else if (this.tool === 'eyedropper') {
this.color = this.bitmap[index];
this.colorChange.emit(this.color);
}
}
}
colorAt(index: number) {
return this.bitmap && this.bitmap[index] ? colorToCSS(parseColor(this.bitmap[index])) : '';
}
}
@@ -5,26 +5,26 @@ import { CM_SIZE } from '../../../common/constants';
import { faTrash, faEraser, faPaintBrush, faEyeDropper } from '../../../client/icons';
export interface ButtMarkEditorState {
brushType: string;
brush: string;
brushType: string;
brush: string;
}
@Component({
selector: 'butt-mark-editor',
templateUrl: 'butt-mark-editor.pug',
selector: 'butt-mark-editor',
templateUrl: 'butt-mark-editor.pug',
})
export class ButtMarkEditor {
readonly trashIcon = faTrash;
readonly eraserIcon = faEraser;
readonly eyeDropperIcon = faEyeDropper;
readonly paintBrushIcon = faPaintBrush;
readonly cmSize = CM_SIZE;
@Input() info!: PonyInfo;
@Input() state = {
brushType: 'brush',
brush: 'orange',
};
clearCM() {
fill(this.info.cm!, '');
}
readonly trashIcon = faTrash;
readonly eraserIcon = faEraser;
readonly eyeDropperIcon = faEyeDropper;
readonly paintBrushIcon = faPaintBrush;
readonly cmSize = CM_SIZE;
@Input() info!: PonyInfo;
@Input() state = {
brushType: 'brush',
brush: 'orange',
};
clearCM() {
fill(this.info.cm!, '');
}
}
@@ -9,164 +9,164 @@ import { LATEST_CHARACTER_LIMIT } from '../../../common/constants';
import { faHashtag } from '../../../client/icons';
function getSortTag(pony: PonyObject) {
const match = pony.desc && /(?:^| )@(top|end|\d+)(?:$| )/.exec(pony.desc);
return match && match[1];
const match = pony.desc && /(?:^| )@(top|end|\d+)(?:$| )/.exec(pony.desc);
return match && match[1];
}
function sortTagToNumber(tag: string) {
if (tag === 'top') {
return -1;
} else if (tag === 'end') {
return 999999999;
} else {
return +tag;
}
if (tag === 'top') {
return -1;
} else if (tag === 'end') {
return 999999999;
} else {
return +tag;
}
}
function fallbackComparePonies(a: PonyObject, b: PonyObject) {
return a.name.localeCompare(b.name) || (a.desc || '').localeCompare(b.desc || '');
return a.name.localeCompare(b.name) || (a.desc || '').localeCompare(b.desc || '');
}
function comparePonies(a: PonyObject, b: PonyObject) {
const aTag = getSortTag(a);
const bTag = getSortTag(b);
const aTag = getSortTag(a);
const bTag = getSortTag(b);
if (aTag && bTag) {
return (sortTagToNumber(aTag) - sortTagToNumber(bTag)) || fallbackComparePonies(a, b);
} else if (aTag) {
return aTag === 'end' ? 1 : -1;
} else if (bTag) {
return bTag === 'end' ? -1 : 1;
} else {
return fallbackComparePonies(a, b);
}
if (aTag && bTag) {
return (sortTagToNumber(aTag) - sortTagToNumber(bTag)) || fallbackComparePonies(a, b);
} else if (aTag) {
return aTag === 'end' ? 1 : -1;
} else if (bTag) {
return bTag === 'end' ? -1 : 1;
} else {
return fallbackComparePonies(a, b);
}
}
@Component({
selector: 'character-list',
templateUrl: 'character-list.pug',
styleUrls: ['character-list.scss'],
selector: 'character-list',
templateUrl: 'character-list.pug',
styleUrls: ['character-list.scss'],
})
export class CharacterList implements OnInit {
readonly hashIcon = faHashtag;
@Input() inGame = false;
@Input() canNew = false;
@Output() close = new EventEmitter<void>();
@Output() newCharacter = new EventEmitter<void>();
@Output() selectCharacter = new EventEmitter<PonyObject>();
@Output() previewCharacter = new EventEmitter<PonyObject | undefined>();
@ViewChild('ariaAnnounce', { static: true }) ariaAnnounce!: ElementRef;
@ViewChild('searchInput', { static: true }) searchInput!: ElementRef;
search?: string;
selectedIndex = -1;
ponies: PonyObject[] = [];
tags: string[] = [];
private previewPony: PonyObject | undefined = undefined;
constructor(private model: Model, private zone: NgZone) {
}
get selectedPony() {
return this.model.pony;
}
get searchable() {
return this.model.ponies.length > LATEST_CHARACTER_LIMIT;
}
get placeholder() {
return `search (${this.model.ponies.length} / ${this.model.characterLimit} ponies)`;
}
ngOnInit() {
this.updatePonies();
readonly hashIcon = faHashtag;
@Input() inGame = false;
@Input() canNew = false;
@Output() close = new EventEmitter<void>();
@Output() newCharacter = new EventEmitter<void>();
@Output() selectCharacter = new EventEmitter<PonyObject>();
@Output() previewCharacter = new EventEmitter<PonyObject | undefined>();
@ViewChild('ariaAnnounce', { static: true }) ariaAnnounce!: ElementRef;
@ViewChild('searchInput', { static: true }) searchInput!: ElementRef;
search?: string;
selectedIndex = -1;
ponies: PonyObject[] = [];
tags: string[] = [];
private previewPony: PonyObject | undefined = undefined;
constructor(private model: Model, private zone: NgZone) {
}
get selectedPony() {
return this.model.pony;
}
get searchable() {
return this.model.ponies.length > LATEST_CHARACTER_LIMIT;
}
get placeholder() {
return `search (${this.model.ponies.length} / ${this.model.characterLimit} ponies)`;
}
ngOnInit() {
this.updatePonies();
this.tags = uniq(flatten(this.ponies.map(p => (p.desc || '').split(/ /g).map(x => x.trim())))
.filter(x => /^#/.test(x)))
.sort();
this.tags = uniq(flatten(this.ponies.map(p => (p.desc || '').split(/ /g).map(x => x.trim())))
.filter(x => /^#/.test(x)))
.sort();
if (!isMobile) {
setTimeout(() => this.searchInput.nativeElement.focus());
}
}
keydown(e: KeyboardEvent) {
if (e.keyCode === Key.ESCAPE) {
if (this.search) {
e.preventDefault();
e.stopPropagation();
this.search = '';
this.updatePonies();
} else {
this.closed();
}
} else if (e.keyCode === Key.ENTER) {
const pony = this.ponies[this.selectedIndex];
if (!isMobile) {
setTimeout(() => this.searchInput.nativeElement.focus());
}
}
keydown(e: KeyboardEvent) {
if (e.keyCode === Key.ESCAPE) {
if (this.search) {
e.preventDefault();
e.stopPropagation();
this.search = '';
this.updatePonies();
} else {
this.closed();
}
} else if (e.keyCode === Key.ENTER) {
const pony = this.ponies[this.selectedIndex];
if (pony) {
this.select(pony);
} else {
this.closed();
}
} else if (e.keyCode === Key.UP) {
this.setSelectedIndex(this.selectedIndex <= 0 ? (this.ponies.length - 1) : (this.selectedIndex - 1));
} else if (e.keyCode === Key.DOWN) {
this.setSelectedIndex(this.selectedIndex === (this.ponies.length - 1) ? 0 : (this.selectedIndex + 1));
}
}
setPreview(pony: PonyObject) {
this.previewPony = pony;
this.previewCharacter.emit(this.model.parsePonyObject(pony));
}
unsetPreview(pony: PonyObject) {
if (this.previewPony && pony && this.previewPony.id === pony.id) {
this.previewPony = undefined;
this.previewCharacter.emit(undefined);
}
}
updatePonies() {
this.zone.run(() => {
const query = this.search && this.search.toLowerCase().trim();
if (pony) {
this.select(pony);
} else {
this.closed();
}
} else if (e.keyCode === Key.UP) {
this.setSelectedIndex(this.selectedIndex <= 0 ? (this.ponies.length - 1) : (this.selectedIndex - 1));
} else if (e.keyCode === Key.DOWN) {
this.setSelectedIndex(this.selectedIndex === (this.ponies.length - 1) ? 0 : (this.selectedIndex + 1));
}
}
setPreview(pony: PonyObject) {
this.previewPony = pony;
this.previewCharacter.emit(this.model.parsePonyObject(pony));
}
unsetPreview(pony: PonyObject) {
if (this.previewPony && pony && this.previewPony.id === pony.id) {
this.previewPony = undefined;
this.previewCharacter.emit(undefined);
}
}
updatePonies() {
this.zone.run(() => {
const query = this.search && this.search.toLowerCase().trim();
function matchesWords(text: string, words: string[]) {
for (const word of words) {
if (text.indexOf(word) === -1) {
return false;
}
}
function matchesWords(text: string, words: string[]) {
for (const word of words) {
if (text.indexOf(word) === -1) {
return false;
}
}
return true;
}
return true;
}
if (query) {
const words = query.split(/ /g).map(x => x.trim());
if (query) {
const words = query.split(/ /g).map(x => x.trim());
this.ponies = this.model.ponies.filter(pony => {
const text = `${pony.name} ${pony.desc || ''}`.toLowerCase();
return matchesWords(text, words);
}).sort(comparePonies);
} else {
this.ponies = this.model.ponies.slice().sort(comparePonies);
}
this.ponies = this.model.ponies.filter(pony => {
const text = `${pony.name} ${pony.desc || ''}`.toLowerCase();
return matchesWords(text, words);
}).sort(comparePonies);
} else {
this.ponies = this.model.ponies.slice().sort(comparePonies);
}
this.setSelectedIndex(this.selectedIndex);
this.previewCharacter.emit(undefined);
});
}
select(pony: PonyObject) {
this.selectCharacter.emit(pony);
}
createNew() {
this.newCharacter.emit();
}
private closed() {
this.zone.run(() => this.close.emit());
}
private setSelectedIndex(index: number) {
this.zone.run(() => {
this.selectedIndex = clamp(index, -1, this.ponies.length - 1);
const pony = this.ponies[index];
this.ariaAnnounce.nativeElement.textContent = pony ? pony.name : '';
this.setSelectedIndex(this.selectedIndex);
this.previewCharacter.emit(undefined);
});
}
select(pony: PonyObject) {
this.selectCharacter.emit(pony);
}
createNew() {
this.newCharacter.emit();
}
private closed() {
this.zone.run(() => this.close.emit());
}
private setSelectedIndex(index: number) {
this.zone.run(() => {
this.selectedIndex = clamp(index, -1, this.ponies.length - 1);
const pony = this.ponies[index];
this.ariaAnnounce.nativeElement.textContent = pony ? pony.name : '';
if (pony) {
this.setPreview(pony);
} else if (this.previewPony) {
this.unsetPreview(this.previewPony);
}
});
}
if (pony) {
this.setPreview(pony);
} else if (this.previewPony) {
this.unsetPreview(this.previewPony);
}
});
}
}
@@ -1,11 +1,11 @@
import {
Component, Input, ElementRef, AfterViewInit, OnDestroy, NgZone, ViewChild, OnChanges, HostListener
Component, Input, ElementRef, AfterViewInit, OnDestroy, NgZone, ViewChild, OnChanges, HostListener
} from '@angular/core';
import { PonyInfo, PonyState } from '../../../common/interfaces';
import { toPalette } from '../../../common/ponyInfo';
import { GRASS_COLOR, TRANSPARENT } from '../../../common/colors';
import {
createCanvas, disableImageSmoothing, getPixelRatio, resizeCanvas, resizeCanvasWithRatio
createCanvas, disableImageSmoothing, getPixelRatio, resizeCanvas, resizeCanvasWithRatio
} from '../../../client/canvasUtils';
import { BLINK_FRAMES } from '../../../client/ponyUtils';
import { defaultPonyState, defaultDrawPonyOptions } from '../../../client/ponyHelpers';
@@ -21,172 +21,172 @@ const DEFAULT_STATE = defaultPonyState();
const DEFAULT_OPTIONS = defaultDrawPonyOptions();
@Component({
selector: 'character-preview',
template: '<canvas class="rounded" #canvas></canvas>',
styles: [`:host { display: block; } canvas { width: 100%; height: 100%; }`],
selector: 'character-preview',
template: '<canvas class="rounded" #canvas></canvas>',
styles: [`:host { display: block; } canvas { width: 100%; height: 100%; }`],
})
export class CharacterPreview implements OnDestroy, OnChanges, AfterViewInit {
@Input() scale = 3;
@Input() name?: string;
@Input() tag?: string;
@Input() pony?: PonyInfo;
@Input() state?: PonyState = defaultPonyState();
@Input() noBackground = false;
@Input() noOutline = false;
@Input() noShadow = false;
@Input() extra = false;
@Input() passive = false;
@Input() blinks = true;
@ViewChild('canvas', { static: true }) canvas!: ElementRef;
private batch?: ContextSpriteBatch;
private nameBatch?: ContextSpriteBatch;
private frame = 0;
private lastFrame = 0;
private initialized = false;
private nextBlink = performance.now() + 2000;
private blinkFrame = -1;
constructor(private zone: NgZone) {
}
ngAfterViewInit() {
return loadAndInitSpriteSheets()
.then(() => this.initialized = true)
.then(() => this.ngOnChanges());
}
ngOnDestroy() {
cancelAnimationFrame(this.frame);
}
ngOnChanges() {
if (!this.frame) {
this.zone.runOutsideAngular(() => this.frame = requestAnimationFrame(this.onFrame));
}
}
@HostListener('window:resize')
redraw() {
this.tryDraw();
}
blink() {
this.nextBlink = performance.now();
}
private onFrame = () => {
if (this.passive && this.initialized) {
this.frame = 0;
this.tryDraw();
return;
}
@Input() scale = 3;
@Input() name?: string;
@Input() tag?: string;
@Input() pony?: PonyInfo;
@Input() state?: PonyState = defaultPonyState();
@Input() noBackground = false;
@Input() noOutline = false;
@Input() noShadow = false;
@Input() extra = false;
@Input() passive = false;
@Input() blinks = true;
@ViewChild('canvas', { static: true }) canvas!: ElementRef;
private batch?: ContextSpriteBatch;
private nameBatch?: ContextSpriteBatch;
private frame = 0;
private lastFrame = 0;
private initialized = false;
private nextBlink = performance.now() + 2000;
private blinkFrame = -1;
constructor(private zone: NgZone) {
}
ngAfterViewInit() {
return loadAndInitSpriteSheets()
.then(() => this.initialized = true)
.then(() => this.ngOnChanges());
}
ngOnDestroy() {
cancelAnimationFrame(this.frame);
}
ngOnChanges() {
if (!this.frame) {
this.zone.runOutsideAngular(() => this.frame = requestAnimationFrame(this.onFrame));
}
}
@HostListener('window:resize')
redraw() {
this.tryDraw();
}
blink() {
this.nextBlink = performance.now();
}
private onFrame = () => {
if (this.passive && this.initialized) {
this.frame = 0;
this.tryDraw();
return;
}
this.frame = requestAnimationFrame(this.onFrame);
this.frame = requestAnimationFrame(this.onFrame);
const now = performance.now();
const now = performance.now();
if ((now - this.lastFrame) > (1000 / 24)) {
if (this.blinks) {
if (this.blinkFrame === -1) {
if (this.nextBlink < now) {
this.blinkFrame = 0;
}
} else {
this.blinkFrame++;
if ((now - this.lastFrame) > (1000 / 24)) {
if (this.blinks) {
if (this.blinkFrame === -1) {
if (this.nextBlink < now) {
this.blinkFrame = 0;
}
} else {
this.blinkFrame++;
if (this.blinkFrame >= BLINK_FRAMES.length) {
this.nextBlink = now + Math.random() * 2000 + 3000;
this.blinkFrame = -1;
}
}
if (this.blinkFrame >= BLINK_FRAMES.length) {
this.nextBlink = now + Math.random() * 2000 + 3000;
this.blinkFrame = -1;
}
}
if (this.state) {
this.state.blinkFrame = this.blinkFrame === -1 ? 1 : BLINK_FRAMES[this.blinkFrame];
}
}
if (this.state) {
this.state.blinkFrame = this.blinkFrame === -1 ? 1 : BLINK_FRAMES[this.blinkFrame];
}
}
this.lastFrame = now;
this.tryDraw();
}
}
private tryDraw() {
try {
this.draw();
} catch { }
}
private draw() {
if (!this.initialized)
return;
this.lastFrame = now;
this.tryDraw();
}
}
private tryDraw() {
try {
this.draw();
} catch { }
}
private draw() {
if (!this.initialized)
return;
const canvas = this.canvas.nativeElement as HTMLCanvasElement;
const canvas = this.canvas.nativeElement as HTMLCanvasElement;
const { width, height } = canvas.getBoundingClientRect();
resizeCanvasWithRatio(canvas, width, height, false);
const { width, height } = canvas.getBoundingClientRect();
resizeCanvasWithRatio(canvas, width, height, false);
const scale = this.scale * getPixelRatio();
const bufferWidth = Math.round(canvas.width / scale);
const bufferHeight = Math.round(canvas.height / scale);
const scale = this.scale * getPixelRatio();
const bufferWidth = Math.round(canvas.width / scale);
const bufferHeight = Math.round(canvas.height / scale);
if (!bufferWidth || !bufferHeight)
return;
if (!bufferWidth || !bufferHeight)
return;
this.batch = this.batch || new ContextSpriteBatch(createCanvas(bufferWidth, bufferHeight));
resizeCanvas(this.batch.canvas, bufferWidth, bufferHeight);
this.batch = this.batch || new ContextSpriteBatch(createCanvas(bufferWidth, bufferHeight));
resizeCanvas(this.batch.canvas, bufferWidth, bufferHeight);
const x = Math.round(bufferWidth / 2);
const y = Math.round(bufferHeight / 2 + 28);
const x = Math.round(bufferWidth / 2);
const y = Math.round(bufferHeight / 2 + 28);
if (this.pony) {
this.batch.start(paletteSpriteSheet, this.noBackground ? TRANSPARENT : GRASS_COLOR);
if (this.pony) {
this.batch.start(paletteSpriteSheet, this.noBackground ? TRANSPARENT : GRASS_COLOR);
try {
const options = { ...DEFAULT_OPTIONS, shadow: !this.noShadow, extra: !!this.extra };
drawPony(this.batch, toPalette(this.pony), this.state || DEFAULT_STATE, x, y, options);
} catch (e) {
console.error(e);
}
try {
const options = { ...DEFAULT_OPTIONS, shadow: !this.noShadow, extra: !!this.extra };
drawPony(this.batch, toPalette(this.pony), this.state || DEFAULT_STATE, x, y, options);
} catch (e) {
console.error(e);
}
this.batch.end();
}
this.batch.end();
}
const viewContext = canvas.getContext('2d');
const viewContext = canvas.getContext('2d');
if (!viewContext)
return;
if (!viewContext)
return;
disableImageSmoothing(viewContext);
disableImageSmoothing(viewContext);
if (this.noBackground) {
viewContext.clearRect(0, 0, canvas.width, canvas.height);
}
if (this.noBackground) {
viewContext.clearRect(0, 0, canvas.width, canvas.height);
}
viewContext.save();
viewContext.scale(scale, scale);
viewContext.save();
viewContext.scale(scale, scale);
// draw outline
if (this.pony && this.noShadow && this.noBackground && !this.noOutline) {
for (let x = -1; x <= 1; x++) {
for (let y = -1; y <= 1; y++) {
viewContext.drawImage(this.batch.canvas, x, y);
}
}
// draw outline
if (this.pony && this.noShadow && this.noBackground && !this.noOutline) {
for (let x = -1; x <= 1; x++) {
for (let y = -1; y <= 1; y++) {
viewContext.drawImage(this.batch.canvas, x, y);
}
}
viewContext.globalCompositeOperation = 'source-in';
viewContext.fillStyle = colorToCSS(GRASS_COLOR);
viewContext.fillRect(0, 0, viewContext.canvas.width, viewContext.canvas.height);
viewContext.globalCompositeOperation = 'source-over';
}
viewContext.globalCompositeOperation = 'source-in';
viewContext.fillStyle = colorToCSS(GRASS_COLOR);
viewContext.fillRect(0, 0, viewContext.canvas.width, viewContext.canvas.height);
viewContext.globalCompositeOperation = 'source-over';
}
viewContext.drawImage(this.batch.canvas, 0, 0);
viewContext.restore();
viewContext.drawImage(this.batch.canvas, 0, 0);
viewContext.restore();
// draw name plate
if (!this.noShadow && this.name) {
const name = replaceEmojis(this.name);
const scale = 2 * getPixelRatio();
const nameBufferWidth = Math.round(canvas.width / scale);
this.nameBatch = this.nameBatch || new ContextSpriteBatch(createCanvas(nameBufferWidth, 25));
resizeCanvas(this.nameBatch.canvas, nameBufferWidth, 25);
this.nameBatch.start(paletteSpriteSheet, TRANSPARENT);
drawNamePlate(this.nameBatch, name, nameBufferWidth / 2, 11, DrawNameFlags.None, commonPalettes, this.tag);
this.nameBatch.end();
viewContext.save();
viewContext.scale(scale, scale);
viewContext.drawImage(this.nameBatch.canvas, 0, 10);
viewContext.restore();
}
}
// draw name plate
if (!this.noShadow && this.name) {
const name = replaceEmojis(this.name);
const scale = 2 * getPixelRatio();
const nameBufferWidth = Math.round(canvas.width / scale);
this.nameBatch = this.nameBatch || new ContextSpriteBatch(createCanvas(nameBufferWidth, 25));
resizeCanvas(this.nameBatch.canvas, nameBufferWidth, 25);
this.nameBatch.start(paletteSpriteSheet, TRANSPARENT);
drawNamePlate(this.nameBatch, name, nameBufferWidth / 2, 11, DrawNameFlags.None, commonPalettes, this.tag);
this.nameBatch.end();
viewContext.save();
viewContext.scale(scale, scale);
viewContext.drawImage(this.nameBatch.canvas, 0, 10);
viewContext.restore();
}
}
}
@@ -12,117 +12,117 @@ import { delay } from '../../../common/utils';
import { isMobile } from '../../../client/data';
@Component({
selector: 'character-select',
templateUrl: 'character-select.pug',
styleUrls: ['character-select.scss'],
selector: 'character-select',
templateUrl: 'character-select.pug',
styleUrls: ['character-select.scss'],
})
export class CharacterSelect {
readonly maxNameLength = PLAYER_NAME_MAX_LENGTH;
readonly spinnerIcon = faSpinner;
readonly deleteIcon = faTrash;
readonly removeIcon = faTimes;
readonly confirmIcon = faCheck;
@Input() newButton = false;
@Input() editButton = false;
@Input() removeButton = false;
@Input() error?: string;
@Output() errorChange = new EventEmitter<string | undefined>();
@Output() change = new EventEmitter<PonyObject>();
@Output() preview = new EventEmitter<PonyObject | undefined>();
@ViewChild('nameInput', { static: true }) nameInput!: ElementRef;
@ViewChild('ariaAnnounce', { static: true }) ariaAnnounce!: ElementRef;
@ViewChild('dropdown', { static: true }) dropdown!: Dropdown;
removing = false;
private locked = false; // TEMP: move to model
constructor(
private element: ElementRef,
private router: Router,
private model: Model,
private gameService: GameService,
) {
}
get joining() {
return this.gameService.joining;
}
get pony() {
return this.model.pony;
}
get canNew() {
return !this.joining && this.model.account && this.model.account.characterCount < this.model.characterLimit;
}
get canEdit() {
return !this.joining;
}
get canRemove() {
return !this.joining && !this.locked && !this.model.pending && !!this.pony
&& !!this.pony.id && this.error !== VERSION_ERROR;
}
get hasPonies() {
return !!this.model.ponies.length;
}
select(pony: PonyObject) {
if (pony) {
this.removing = false;
this.model.selectPony(pony);
this.change.emit(pony);
this.preview.emit(undefined);
}
readonly maxNameLength = PLAYER_NAME_MAX_LENGTH;
readonly spinnerIcon = faSpinner;
readonly deleteIcon = faTrash;
readonly removeIcon = faTimes;
readonly confirmIcon = faCheck;
@Input() newButton = false;
@Input() editButton = false;
@Input() removeButton = false;
@Input() error?: string;
@Output() errorChange = new EventEmitter<string | undefined>();
@Output() change = new EventEmitter<PonyObject>();
@Output() preview = new EventEmitter<PonyObject | undefined>();
@ViewChild('nameInput', { static: true }) nameInput!: ElementRef;
@ViewChild('ariaAnnounce', { static: true }) ariaAnnounce!: ElementRef;
@ViewChild('dropdown', { static: true }) dropdown!: Dropdown;
removing = false;
private locked = false; // TEMP: move to model
constructor(
private element: ElementRef,
private router: Router,
private model: Model,
private gameService: GameService,
) {
}
get joining() {
return this.gameService.joining;
}
get pony() {
return this.model.pony;
}
get canNew() {
return !this.joining && this.model.account && this.model.account.characterCount < this.model.characterLimit;
}
get canEdit() {
return !this.joining;
}
get canRemove() {
return !this.joining && !this.locked && !this.model.pending && !!this.pony
&& !!this.pony.id && this.error !== VERSION_ERROR;
}
get hasPonies() {
return !!this.model.ponies.length;
}
select(pony: PonyObject) {
if (pony) {
this.removing = false;
this.model.selectPony(pony);
this.change.emit(pony);
this.preview.emit(undefined);
}
this.dropdown.close();
this.focusName();
}
createNew() {
if (this.canNew) {
this.removing = false;
this.model.selectPony(createDefaultPonyObject());
this.change.emit(this.pony);
this.router.navigate(['/character']);
this.focusName();
}
}
edit() {
if (this.canEdit) {
this.removing = false;
this.router.navigate(['/character']);
}
}
remove() {
if (this.canRemove) {
this.removing = true;
focusElementAfterTimeout(this.element.nativeElement, '.cancel-remove-button');
}
}
cancelRemove() {
this.removing = false;
focusElementAfterTimeout(this.element.nativeElement, '.remove-button');
}
confirmRemove() {
if (this.canRemove) {
this.setError(undefined);
this.removing = false;
this.locked = true;
this.dropdown.close();
this.focusName();
}
createNew() {
if (this.canNew) {
this.removing = false;
this.model.selectPony(createDefaultPonyObject());
this.change.emit(this.pony);
this.router.navigate(['/character']);
this.focusName();
}
}
edit() {
if (this.canEdit) {
this.removing = false;
this.router.navigate(['/character']);
}
}
remove() {
if (this.canRemove) {
this.removing = true;
focusElementAfterTimeout(this.element.nativeElement, '.cancel-remove-button');
}
}
cancelRemove() {
this.removing = false;
focusElementAfterTimeout(this.element.nativeElement, '.remove-button');
}
confirmRemove() {
if (this.canRemove) {
this.setError(undefined);
this.removing = false;
this.locked = true;
this.model.removePony(this.pony)
.then(() => this.change.emit(this.pony))
.catch((e: Error) => this.setError(e.message))
.then(() => this.ariaAnnounce.nativeElement.textContent = 'Character removed')
.then(() => delay(2000))
.then(() => this.locked = false)
.then(() => this.focusName());
}
}
onToggle(show: boolean) {
if (!show) {
this.preview.emit(undefined);
}
}
private focusName() {
if (!isMobile) {
this.nameInput.nativeElement.focus();
}
}
private setError(error: string | undefined) {
this.error = error;
this.errorChange.emit(error);
}
this.model.removePony(this.pony)
.then(() => this.change.emit(this.pony))
.catch((e: Error) => this.setError(e.message))
.then(() => this.ariaAnnounce.nativeElement.textContent = 'Character removed')
.then(() => delay(2000))
.then(() => this.locked = false)
.then(() => this.focusName());
}
}
onToggle(show: boolean) {
if (!show) {
this.preview.emit(undefined);
}
}
private focusName() {
if (!isMobile) {
this.nameInput.nativeElement.focus();
}
}
private setError(error: string | undefined) {
this.error = error;
this.errorChange.emit(error);
}
}
+309 -309
View File
@@ -18,8 +18,8 @@ const chatTypeNames: string[] = [];
const chatTypeClasses: string[] = [];
function setupChatType(type: ChatType, name: string) {
chatTypeNames[type] = name;
chatTypeClasses[type] = `chat-${name.replace(/ /, '-')}`;
chatTypeNames[type] = name;
chatTypeClasses[type] = `chat-${name.replace(/ /, '-')}`;
}
setupChatType(ChatType.Say, 'say');
@@ -33,356 +33,356 @@ setupChatType(ChatType.Think, 'think');
setupChatType(ChatType.PartyThink, 'party think');
function isActionCommand(message: string) {
return /^\/(yawn|sneeze|achoo|laugh|lol|haha|хаха|jaja)/i.test(message);
return /^\/(yawn|sneeze|achoo|laugh|lol|haha|хаха|jaja)/i.test(message);
}
@Component({
selector: 'chat-box',
templateUrl: 'chat-box.pug',
styleUrls: ['chat-box.scss'],
selector: 'chat-box',
templateUrl: 'chat-box.pug',
styleUrls: ['chat-box.scss'],
})
export class ChatBox implements AfterViewInit, OnDestroy {
readonly maxSayLength = SAY_MAX_LENGTH;
readonly commentIcon = faComment;
readonly sendIcon = faAngleDoubleRight;
@ViewChild('inputElement', { static: true }) inputElement!: ElementRef;
@ViewChild('typeBox', { static: true }) typeBox!: ElementRef;
@ViewChild('typePrefix', { static: true }) typePrefix!: ElementRef;
@ViewChild('typeName', { static: true }) typeName!: ElementRef;
@ViewChild('chatBox', { static: true }) chatBox!: ElementRef;
@ViewChild('chatBoxInput', { static: true }) chatBoxInput!: ElementRef;
isOpen = false;
message: string | undefined = '';
chatType = ChatType.Say;
private pasted = false;
private lastMessages: string[] = [];
private state: AutocompleteState = {};
private subscriptions: Subscription[];
private _disabled = false;
constructor(private game: PonyTownGame, zone: NgZone) {
this.subscriptions = [
this.game.onChat.subscribe(() => zone.run(() => this.chat(undefined))),
this.game.onToggleChat.subscribe(() => zone.run(() => this.toggle())),
this.game.onCommand.subscribe(() => zone.run(() => this.command())),
this.game.onLeft.subscribe(() => {
this.chatType = ChatType.Say;
this.close();
}),
];
readonly maxSayLength = SAY_MAX_LENGTH;
readonly commentIcon = faComment;
readonly sendIcon = faAngleDoubleRight;
@ViewChild('inputElement', { static: true }) inputElement!: ElementRef;
@ViewChild('typeBox', { static: true }) typeBox!: ElementRef;
@ViewChild('typePrefix', { static: true }) typePrefix!: ElementRef;
@ViewChild('typeName', { static: true }) typeName!: ElementRef;
@ViewChild('chatBox', { static: true }) chatBox!: ElementRef;
@ViewChild('chatBoxInput', { static: true }) chatBoxInput!: ElementRef;
isOpen = false;
message: string | undefined = '';
chatType = ChatType.Say;
private pasted = false;
private lastMessages: string[] = [];
private state: AutocompleteState = {};
private subscriptions: Subscription[];
private _disabled = false;
constructor(private game: PonyTownGame, zone: NgZone) {
this.subscriptions = [
this.game.onChat.subscribe(() => zone.run(() => this.chat(undefined))),
this.game.onToggleChat.subscribe(() => zone.run(() => this.toggle())),
this.game.onCommand.subscribe(() => zone.run(() => this.command())),
this.game.onLeft.subscribe(() => {
this.chatType = ChatType.Say;
this.close();
}),
];
this.game.onCancel = () => this.isOpen ? (zone.run(() => this.close()), true) : false;
}
@Input() get disabled() {
return this._disabled;
}
set disabled(value) {
this._disabled = value;
this.game.onCancel = () => this.isOpen ? (zone.run(() => this.close()), true) : false;
}
@Input() get disabled() {
return this._disabled;
}
set disabled(value) {
this._disabled = value;
if (value) {
this.close();
}
}
get input() {
return this.inputElement.nativeElement as HTMLInputElement;
}
ngAfterViewInit() {
this.chatBox.nativeElement.hidden = true;
this.input.addEventListener('paste', () => this.pasted = true);
}
ngOnDestroy() {
this.subscriptions.forEach(s => s.unsubscribe());
}
send(_event: Event | undefined) {
let chatType = this.chatType;
let message = replaceEmojis(cleanMessage(this.message || '')).substr(0, SAY_MAX_LENGTH);
const handled = handleActionCommand(message, this.game);
const spam = this.pasted && chatType !== ChatType.Party && isSpamMessage(message, this.lastMessages);
const empty = !this.game.player || !message;
const ignoreAction = isActionCommand(message) && this.game.player && hasHeadAnimation(this.game.player);
const whisperTo = this.game.whisperTo;
let entityId = whisperTo && whisperTo.id || 0;
if (value) {
this.close();
}
}
get input() {
return this.inputElement.nativeElement as HTMLInputElement;
}
ngAfterViewInit() {
this.chatBox.nativeElement.hidden = true;
this.input.addEventListener('paste', () => this.pasted = true);
}
ngOnDestroy() {
this.subscriptions.forEach(s => s.unsubscribe());
}
send(_event: Event | undefined) {
let chatType = this.chatType;
let message = replaceEmojis(cleanMessage(this.message || '')).substr(0, SAY_MAX_LENGTH);
const handled = handleActionCommand(message, this.game);
const spam = this.pasted && chatType !== ChatType.Party && isSpamMessage(message, this.lastMessages);
const empty = !this.game.player || !message;
const ignoreAction = isActionCommand(message) && this.game.player && hasHeadAnimation(this.game.player);
const whisperTo = this.game.whisperTo;
let entityId = whisperTo && whisperTo.id || 0;
if (/^\/(w|whisper) .+$/i.test(message)) {
chatType = ChatType.Whisper;
message = message.substr(/^\/w /i.test(message) ? 3 : 9);
if (/^\/(w|whisper) .+$/i.test(message)) {
chatType = ChatType.Whisper;
message = message.substr(/^\/w /i.test(message) ? 3 : 9);
let offset = 0;
let entity: Entity | FakeEntity | undefined = undefined;
let offset = 0;
let entity: Entity | FakeEntity | undefined = undefined;
do {
offset = message.indexOf(' ', offset);
do {
offset = message.indexOf(' ', offset);
if (offset === -1)
break;
if (offset === -1)
break;
const name = message.substr(0, offset);
entity = findBestEntityByName(this.game, name);
offset++;
} while (!entity);
const name = message.substr(0, offset);
entity = findBestEntityByName(this.game, name);
offset++;
} while (!entity);
if (entity) {
message = message.substr(offset);
entityId = entity.id;
} else {
entityId = 0;
}
}
if (entity) {
message = message.substr(offset);
entityId = entity.id;
} else {
entityId = 0;
}
}
if (handled || spam || empty || ignoreAction || this.say(message, chatType, entityId)) {
if (message) {
this.lastMessages.push(message);
if (handled || spam || empty || ignoreAction || this.say(message, chatType, entityId)) {
if (message) {
this.lastMessages.push(message);
while (this.lastMessages.length > 5) {
this.lastMessages.shift();
}
}
while (this.lastMessages.length > 5) {
this.lastMessages.shift();
}
}
this.close();
}
}
keydown(e: KeyboardEvent) {
if (e.keyCode !== Key.TAB && e.keyCode !== Key.SHIFT) {
this.state.lastEmoji = undefined;
}
this.close();
}
}
keydown(e: KeyboardEvent) {
if (e.keyCode !== Key.TAB && e.keyCode !== Key.SHIFT) {
this.state.lastEmoji = undefined;
}
if (e.keyCode === Key.TAB) {
if (this.message) {
if (/^\/(w|whisper) .+$/i.test(this.message)) {
const space = this.message.indexOf(' ');
const names = findMatchingEntityNames(this.game, this.message.substr(space + 1));
if (e.keyCode === Key.TAB) {
if (this.message) {
if (/^\/(w|whisper) .+$/i.test(this.message)) {
const space = this.message.indexOf(' ');
const names = findMatchingEntityNames(this.game, this.message.substr(space + 1));
if (names.length === 1) {
this.message = `${this.message.substring(0, space)} ${names[0]}`;
}
} else {
this.message = autocompleteMesssage(this.message, e.shiftKey, this.state);
}
}
if (names.length === 1) {
this.message = `${this.message.substring(0, space)} ${names[0]}`;
}
} else {
this.message = autocompleteMesssage(this.message, e.shiftKey, this.state);
}
}
e.preventDefault();
} else if (e.keyCode === Key.ENTER && this.isOpen) {
this.send(e);
} else if (e.keyCode === Key.ESCAPE) {
this.close();
e.preventDefault();
} else if (e.keyCode === Key.SPACE) {
if (!this.message)
return;
e.preventDefault();
} else if (e.keyCode === Key.ENTER && this.isOpen) {
this.send(e);
} else if (e.keyCode === Key.ESCAPE) {
this.close();
e.preventDefault();
} else if (e.keyCode === Key.SPACE) {
if (!this.message)
return;
const isParty = /^\/(p|party)$/i.test(this.message);
const isSay = /^\/(s|say)$/i.test(this.message);
const isSup = /^\/(ss)$/i.test(this.message);
const isSup1 = /^\/(s1)$/i.test(this.message);
const isSup2 = /^\/(s2)$/i.test(this.message);
const isSup3 = /^\/(s3)$/i.test(this.message);
const isParty = /^\/(p|party)$/i.test(this.message);
const isSay = /^\/(s|say)$/i.test(this.message);
const isSup = /^\/(ss)$/i.test(this.message);
const isSup1 = /^\/(s1)$/i.test(this.message);
const isSup2 = /^\/(s2)$/i.test(this.message);
const isSup3 = /^\/(s3)$/i.test(this.message);
const supporter = this.game.model.supporter;
const isSayOrInvalid = isSay
|| (isParty && !isInParty(this.game))
|| (isSup && supporter === 0)
|| (isSup1 && supporter < 1)
|| (isSup2 && supporter < 2)
|| (isSup3 && supporter < 3);
const supporter = this.game.model.supporter;
const isSayOrInvalid = isSay
|| (isParty && !isInParty(this.game))
|| (isSup && supporter === 0)
|| (isSup1 && supporter < 1)
|| (isSup2 && supporter < 2)
|| (isSup3 && supporter < 3);
if (isSayOrInvalid) {
this.changeChatType(e, ChatType.Say);
} else if (isParty) {
this.changeChatType(e, ChatType.Party);
} else if (isSup) {
this.changeChatType(e, ChatType.Supporter);
} else if (isSup1) {
this.changeChatType(e, ChatType.Supporter1);
} else if (isSup2) {
this.changeChatType(e, ChatType.Supporter2);
} else if (isSup3) {
this.changeChatType(e, ChatType.Supporter3);
} else if (/^\/(t|think)$/i.test(this.message)) {
if (isPartyChat(this.chatType)) {
this.changeChatType(e, ChatType.PartyThink);
} else {
this.changeChatType(e, ChatType.Think);
}
} else if (/^\/(r|reply)$/i.test(this.message)) {
const lastWhisperFrom = this.game.lastWhisperFrom;
const entity = lastWhisperFrom && findEntityOrMockByAnyMeans(this.game, lastWhisperFrom.entityId);
if (isSayOrInvalid) {
this.changeChatType(e, ChatType.Say);
} else if (isParty) {
this.changeChatType(e, ChatType.Party);
} else if (isSup) {
this.changeChatType(e, ChatType.Supporter);
} else if (isSup1) {
this.changeChatType(e, ChatType.Supporter1);
} else if (isSup2) {
this.changeChatType(e, ChatType.Supporter2);
} else if (isSup3) {
this.changeChatType(e, ChatType.Supporter3);
} else if (/^\/(t|think)$/i.test(this.message)) {
if (isPartyChat(this.chatType)) {
this.changeChatType(e, ChatType.PartyThink);
} else {
this.changeChatType(e, ChatType.Think);
}
} else if (/^\/(r|reply)$/i.test(this.message)) {
const lastWhisperFrom = this.game.lastWhisperFrom;
const entity = lastWhisperFrom && findEntityOrMockByAnyMeans(this.game, lastWhisperFrom.entityId);
if (entity) {
this.game.whisperTo = entity;
this.changeChatType(e, ChatType.Whisper);
} else {
this.changeChatType(e, ChatType.Say);
}
} else if (/^\/(w|whisper) .+$/i.test(this.message) && !e.shiftKey) {
const name = this.message.substr(/^\/w /i.test(this.message) ? 3 : 9);
const entity = findBestEntityByName(this.game, name);
if (entity) {
this.game.whisperTo = entity;
this.changeChatType(e, ChatType.Whisper);
} else {
this.changeChatType(e, ChatType.Say);
}
} else if (/^\/(w|whisper) .+$/i.test(this.message) && !e.shiftKey) {
const name = this.message.substr(/^\/w /i.test(this.message) ? 3 : 9);
const entity = findBestEntityByName(this.game, name);
if (entity) {
this.game.whisperTo = entity;
this.changeChatType(e, ChatType.Whisper);
}
}
}
}
private say(message: string, chatType: ChatType, entityId: number): boolean {
this.game.lastChatMessageType = chatType;
return !!this.game.send(server => server.say(entityId, message, chatType));
}
private changeChatType(e: KeyboardEvent, chatType: ChatType) {
this.chatType = chatType;
this.message = '';
this.updateChatType();
e.preventDefault();
}
private chat(event: Event | undefined) {
if (this.isOpen) {
this.send(event);
} else {
this.open();
}
}
private command() {
if (!this.isOpen) {
this.chat(undefined);
this.message = '/';
this.input.selectionStart = this.input.selectionEnd = 10000;
}
}
private open() {
if (!this.isOpen) {
this.isOpen = true;
this.chatBox.nativeElement.hidden = false;
}
if (entity) {
this.game.whisperTo = entity;
this.changeChatType(e, ChatType.Whisper);
}
}
}
}
private say(message: string, chatType: ChatType, entityId: number): boolean {
this.game.lastChatMessageType = chatType;
return !!this.game.send(server => server.say(entityId, message, chatType));
}
private changeChatType(e: KeyboardEvent, chatType: ChatType) {
this.chatType = chatType;
this.message = '';
this.updateChatType();
e.preventDefault();
}
private chat(event: Event | undefined) {
if (this.isOpen) {
this.send(event);
} else {
this.open();
}
}
private command() {
if (!this.isOpen) {
this.chat(undefined);
this.message = '/';
this.input.selectionStart = this.input.selectionEnd = 10000;
}
}
private open() {
if (!this.isOpen) {
this.isOpen = true;
this.chatBox.nativeElement.hidden = false;
}
this.chatType = isValidChatType(this.chatType, this.game) ? this.chatType : ChatType.Say;
this.updateChatType();
this.input.focus();
}
private close() {
if (this.isOpen) {
this.input.blur();
this.isOpen = false;
this.chatBox.nativeElement.hidden = true;
this.message = '';
this.pasted = false;
}
}
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
toggleChatType() {
const chatTypes = getChatTypes(this.game);
this.chatType = chatTypes[(chatTypes.indexOf(this.chatType) + 1) % chatTypes.length];
this.updateChatType();
this.input.focus();
}
setChatType(type: 'say' | 'party' | 'whisper') {
if (type === 'say') {
this.chatType = ChatType.Say;
this.open();
} else if (type === 'party' && isInParty(this.game)) {
this.chatType = ChatType.Party;
this.open();
} else if (type === 'whisper') {
this.chatType = ChatType.Whisper;
this.open();
}
}
private currentTypeClass = '';
private currentTypePrefix = '';
private currentTypeName = '';
private updateChatType() {
let typeName: string;
let typePrefix: string;
let changed = false;
this.chatType = isValidChatType(this.chatType, this.game) ? this.chatType : ChatType.Say;
this.updateChatType();
this.input.focus();
}
private close() {
if (this.isOpen) {
this.input.blur();
this.isOpen = false;
this.chatBox.nativeElement.hidden = true;
this.message = '';
this.pasted = false;
}
}
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
toggleChatType() {
const chatTypes = getChatTypes(this.game);
this.chatType = chatTypes[(chatTypes.indexOf(this.chatType) + 1) % chatTypes.length];
this.updateChatType();
this.input.focus();
}
setChatType(type: 'say' | 'party' | 'whisper') {
if (type === 'say') {
this.chatType = ChatType.Say;
this.open();
} else if (type === 'party' && isInParty(this.game)) {
this.chatType = ChatType.Party;
this.open();
} else if (type === 'whisper') {
this.chatType = ChatType.Whisper;
this.open();
}
}
private currentTypeClass = '';
private currentTypePrefix = '';
private currentTypeName = '';
private updateChatType() {
let typeName: string;
let typePrefix: string;
let changed = false;
const typeClass = chatTypeClass(this.chatType, this.game.model.supporter);
const typeClass = chatTypeClass(this.chatType, this.game.model.supporter);
if (this.currentTypeClass !== typeClass) {
this.currentTypeClass = typeClass;
(this.chatBoxInput.nativeElement as HTMLElement).className = typeClass;
}
if (this.currentTypeClass !== typeClass) {
this.currentTypeClass = typeClass;
(this.chatBoxInput.nativeElement as HTMLElement).className = typeClass;
}
if (this.chatType === ChatType.Whisper) {
typePrefix = 'To ';
typeName = this.game.whisperTo && this.game.whisperTo.name || 'unknown';
} else {
typePrefix = '';
typeName = chatTypeNames[this.chatType];
}
if (this.chatType === ChatType.Whisper) {
typePrefix = 'To ';
typeName = this.game.whisperTo && this.game.whisperTo.name || 'unknown';
} else {
typePrefix = '';
typeName = chatTypeNames[this.chatType];
}
if (this.currentTypePrefix !== typePrefix) {
changed = true;
this.currentTypePrefix = typePrefix;
(this.typePrefix.nativeElement as HTMLElement).textContent = typePrefix;
}
if (this.currentTypePrefix !== typePrefix) {
changed = true;
this.currentTypePrefix = typePrefix;
(this.typePrefix.nativeElement as HTMLElement).textContent = typePrefix;
}
if (this.currentTypeName !== typeName) {
changed = true;
this.currentTypeName = typeName;
replaceNodes(this.typeName.nativeElement, typeName);
}
if (this.currentTypeName !== typeName) {
changed = true;
this.currentTypeName = typeName;
replaceNodes(this.typeName.nativeElement, typeName);
}
if (changed) {
const { width } = (this.typeBox.nativeElement as HTMLElement).getBoundingClientRect();
const padding = 35 + 13 + Math.ceil(width);
(this.inputElement.nativeElement as HTMLElement).style.paddingLeft = `${padding}px`;
}
}
if (changed) {
const { width } = (this.typeBox.nativeElement as HTMLElement).getBoundingClientRect();
const padding = 35 + 13 + Math.ceil(width);
(this.inputElement.nativeElement as HTMLElement).style.paddingLeft = `${padding}px`;
}
}
}
function chatTypeClass(chatType: ChatType, supporter: number) {
if (chatType === ChatType.Supporter) {
switch (supporter) {
case 1: return 'chat-sup chat-sup1';
case 2: return 'chat-sup chat-sup2';
case 3: return 'chat-sup chat-sup3';
}
}
if (chatType === ChatType.Supporter) {
switch (supporter) {
case 1: return 'chat-sup chat-sup1';
case 2: return 'chat-sup chat-sup2';
case 3: return 'chat-sup chat-sup3';
}
}
return chatTypeClasses[chatType];
return chatTypeClasses[chatType];
}
function isValidChatType(type: ChatType, game: PonyTownGame) {
const supporter = game.model.supporter;
const supporter = game.model.supporter;
switch (type) {
case ChatType.Say:
case ChatType.Think:
case ChatType.Whisper:
return true;
case ChatType.Party:
case ChatType.PartyThink:
return isInParty(game);
case ChatType.Supporter:
return supporter > 0;
case ChatType.Supporter1:
return supporter >= 1;
case ChatType.Supporter2:
return supporter >= 2;
case ChatType.Supporter3:
return supporter >= 3;
case ChatType.Dismiss:
return false;
default:
return invalidEnumReturn(type, false);
}
switch (type) {
case ChatType.Say:
case ChatType.Think:
case ChatType.Whisper:
return true;
case ChatType.Party:
case ChatType.PartyThink:
return isInParty(game);
case ChatType.Supporter:
return supporter > 0;
case ChatType.Supporter1:
return supporter >= 1;
case ChatType.Supporter2:
return supporter >= 2;
case ChatType.Supporter3:
return supporter >= 3;
case ChatType.Dismiss:
return false;
default:
return invalidEnumReturn(type, false);
}
}
function getChatTypes(game: PonyTownGame) {
const chatTypes = [ChatType.Say];
const supporter = game.model.supporter;
const chatTypes = [ChatType.Say];
const supporter = game.model.supporter;
if (isInParty(game)) {
chatTypes.push(ChatType.Party);
}
if (isInParty(game)) {
chatTypes.push(ChatType.Party);
}
if (supporter) {
chatTypes.push(ChatType.Supporter);
}
if (supporter) {
chatTypes.push(ChatType.Supporter);
}
return chatTypes;
return chatTypes;
}
File diff suppressed because it is too large Load Diff
+15 -15
View File
@@ -2,21 +2,21 @@ import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from
import { faCheck } from '../../../client/icons';
@Component({
selector: 'check-box',
templateUrl: 'check-box.pug',
styleUrls: ['check-box.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'check-box',
templateUrl: 'check-box.pug',
styleUrls: ['check-box.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CheckBox {
@Input() icon = faCheck;
@Input() label?: string;
@Input() disabled = false;
@Input() checked = false;
@Output() checkedChange = new EventEmitter<boolean>();
toggle() {
if (!this.disabled) {
this.checked = !this.checked;
this.checkedChange.emit(this.checked);
}
}
@Input() icon = faCheck;
@Input() label?: string;
@Input() disabled = false;
@Input() checked = false;
@Output() checkedChange = new EventEmitter<boolean>();
toggle() {
if (!this.disabled) {
this.checked = !this.checked;
this.checkedChange.emit(this.checked);
}
}
}
@@ -7,119 +7,119 @@ import { faChevronDown } from '../../../client/icons';
const SIZE = 175;
@Component({
selector: 'color-picker',
templateUrl: 'color-picker.pug',
styleUrls: ['color-picker.scss'],
selector: 'color-picker',
templateUrl: 'color-picker.pug',
styleUrls: ['color-picker.scss'],
})
export class ColorPicker {
readonly chevronIcon = faChevronDown;
@Input() isOpen = false;
@Input() isDisabled = false;
@Input() disabledColor = '';
@Input() color = '';
@Input() indicatorColor = '';
@Input() label?: string = undefined;
@Input() labelledBy?: string = undefined;
@Output() colorChange = new EventEmitter<string>();
s = 0;
v = 0;
h = 0;
private lastColor = '';
private closeHandler = () => this.close();
get inputColor() {
return this.isDisabled && this.disabledColor ? this.disabledColor : this.color;
}
set inputColor(value) {
if (!this.isDisabled) {
this.color = value;
}
}
get bg() {
return colorToCSS(parseColorFast(this.inputColor));
}
get svLeft() {
this.updateHsv();
return this.s * 100;
}
get svTop() {
this.updateHsv();
return (1 - this.v) * 100;
}
get hueTop() {
this.updateHsv();
return this.h * 100 / 360;
}
get hue() {
this.updateHsv();
return colorToCSS(colorFromHSVA(this.h, 1, 1, 1));
}
focus(e: Event) {
this.isOpen = true;
(e.target as HTMLInputElement).select();
}
dragSV({ event, x, y }: AgDragEvent) {
event.preventDefault();
readonly chevronIcon = faChevronDown;
@Input() isOpen = false;
@Input() isDisabled = false;
@Input() disabledColor = '';
@Input() color = '';
@Input() indicatorColor = '';
@Input() label?: string = undefined;
@Input() labelledBy?: string = undefined;
@Output() colorChange = new EventEmitter<string>();
s = 0;
v = 0;
h = 0;
private lastColor = '';
private closeHandler = () => this.close();
get inputColor() {
return this.isDisabled && this.disabledColor ? this.disabledColor : this.color;
}
set inputColor(value) {
if (!this.isDisabled) {
this.color = value;
}
}
get bg() {
return colorToCSS(parseColorFast(this.inputColor));
}
get svLeft() {
this.updateHsv();
return this.s * 100;
}
get svTop() {
this.updateHsv();
return (1 - this.v) * 100;
}
get hueTop() {
this.updateHsv();
return this.h * 100 / 360;
}
get hue() {
this.updateHsv();
return colorToCSS(colorFromHSVA(this.h, 1, 1, 1));
}
focus(e: Event) {
this.isOpen = true;
(e.target as HTMLInputElement).select();
}
dragSV({ event, x, y }: AgDragEvent) {
event.preventDefault();
this.updateHsv();
this.s = clamp(x / SIZE, 0, 1);
this.v = 1 - clamp(y / SIZE, 0, 1);
this.updateColor();
}
dragHue({ event, y }: AgDragEvent) {
event.preventDefault();
this.updateHsv();
this.s = clamp(x / SIZE, 0, 1);
this.v = 1 - clamp(y / SIZE, 0, 1);
this.updateColor();
}
dragHue({ event, y }: AgDragEvent) {
event.preventDefault();
this.updateHsv();
this.h = clamp(360 * y / SIZE, 0, 360);
this.updateColor();
}
updateHsv() {
if (this.lastColor !== this.color) {
const { h, s, v } = colorToHSVA(parseColorFast(this.color), this.h);
this.h = h;
this.s = s;
this.v = v;
this.lastColor = this.color;
}
}
updateColor() {
const color = colorToHexRGB(colorFromHSVA(this.h, this.s, this.v, 1));
const changed = this.color !== color;
this.lastColor = this.color = color;
this.updateHsv();
this.h = clamp(360 * y / SIZE, 0, 360);
this.updateColor();
}
updateHsv() {
if (this.lastColor !== this.color) {
const { h, s, v } = colorToHSVA(parseColorFast(this.color), this.h);
this.h = h;
this.s = s;
this.v = v;
this.lastColor = this.color;
}
}
updateColor() {
const color = colorToHexRGB(colorFromHSVA(this.h, this.s, this.v, 1));
const changed = this.color !== color;
this.lastColor = this.color = color;
if (changed) {
this.colorChange.emit(color);
}
}
inputChanged(value: string) {
this.color = value;
this.colorChange.emit(this.color);
}
stopEvent(e: Event) {
e.stopPropagation();
e.preventDefault();
}
open() {
if (!this.isOpen) {
this.isOpen = true;
if (changed) {
this.colorChange.emit(color);
}
}
inputChanged(value: string) {
this.color = value;
this.colorChange.emit(this.color);
}
stopEvent(e: Event) {
e.stopPropagation();
e.preventDefault();
}
open() {
if (!this.isOpen) {
this.isOpen = true;
setTimeout(() => {
document.addEventListener('mousedown', this.closeHandler);
document.addEventListener('touchstart', this.closeHandler);
});
}
}
close() {
this.isOpen = false;
document.removeEventListener('mousedown', this.closeHandler);
document.removeEventListener('touchstart', this.closeHandler);
}
toggleOpen() {
if (!this.isDisabled) {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
}
setTimeout(() => {
document.addEventListener('mousedown', this.closeHandler);
document.addEventListener('touchstart', this.closeHandler);
});
}
}
close() {
this.isOpen = false;
document.removeEventListener('mousedown', this.closeHandler);
document.removeEventListener('touchstart', this.closeHandler);
}
toggleOpen() {
if (!this.isDisabled) {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
}
}
@@ -2,15 +2,15 @@ import { Component, ChangeDetectionStrategy, Output, Input, EventEmitter } from
import { uniqueId } from 'lodash';
@Component({
selector: 'custom-checkbox',
templateUrl: 'custom-checkbox.pug',
styleUrls: ['custom-checkbox.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'custom-checkbox',
templateUrl: 'custom-checkbox.pug',
styleUrls: ['custom-checkbox.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomCheckbox {
@Input() disabled = false;
@Input() help = '';
@Input() checked = false;
@Output() checkedChange = new EventEmitter<boolean>();
helpId = uniqueId('custom-checkbox-help-');
@Input() disabled = false;
@Input() help = '';
@Input() checked = false;
@Output() checkedChange = new EventEmitter<boolean>();
helpId = uniqueId('custom-checkbox-help-');
}
@@ -4,52 +4,52 @@ import { MONTH_NAMES_EN } from '../../../common/constants';
import { getLocale } from '../../../client/clientUtils';
@Component({
selector: 'date-picker',
templateUrl: 'date-picker.pug',
selector: 'date-picker',
templateUrl: 'date-picker.pug',
})
export class DatePicker {
readonly days = times(31, i => i + 1);
readonly years: number[] = [];
readonly months = getMonthNames();
day = 0;
month = 0;
year = 0;
@Output() dateChange = new EventEmitter<string | undefined>();
constructor() {
const minYear = 1914;
const maxYear = (new Date()).getFullYear() - 6;
readonly days = times(31, i => i + 1);
readonly years: number[] = [];
readonly months = getMonthNames();
day = 0;
month = 0;
year = 0;
@Output() dateChange = new EventEmitter<string | undefined>();
constructor() {
const minYear = 1914;
const maxYear = (new Date()).getFullYear() - 6;
for (let year = maxYear; year >= minYear; year--) {
this.years.push(year);
}
}
@Input() get date() {
const date = createValidBirthDate(this.day, this.month, this.year);
return date && formatISODate(date);
}
set date(value) {
if (value) {
const { day, month, year } = parseISODate(value);
this.day = day;
this.month = month;
this.year = year;
}
}
change() {
this.dateChange.emit(this.date);
}
for (let year = maxYear; year >= minYear; year--) {
this.years.push(year);
}
}
@Input() get date() {
const date = createValidBirthDate(this.day, this.month, this.year);
return date && formatISODate(date);
}
set date(value) {
if (value) {
const { day, month, year } = parseISODate(value);
this.day = day;
this.month = month;
this.year = year;
}
}
change() {
this.dateChange.emit(this.date);
}
}
function getMonthNames() {
try {
const format = new Intl.DateTimeFormat(getLocale(), { month: 'long' });
try {
const format = new Intl.DateTimeFormat(getLocale(), { month: 'long' });
return times(12, i => {
const date = new Date(523456789);
date.setMonth(i);
return format.format(date);
});
} catch {
return MONTH_NAMES_EN;
}
return times(12, i => {
const date = new Date(523456789);
date.setMonth(i);
return format.format(date);
});
} catch {
return MONTH_NAMES_EN;
}
}
@@ -1,12 +1,12 @@
import { Directive, AfterViewInit, ElementRef } from '@angular/core';
@Directive({
selector: '[agAutoFocus]'
selector: '[agAutoFocus]'
})
export class AgAutoFocus implements AfterViewInit {
constructor(private element: ElementRef) {
}
ngAfterViewInit() {
setTimeout(() => this.element.nativeElement.focus(), 100);
}
constructor(private element: ElementRef) {
}
ngAfterViewInit() {
setTimeout(() => this.element.nativeElement.focus(), 100);
}
}
+113 -113
View File
@@ -3,138 +3,138 @@ import { noop } from 'lodash';
import { getButton, getX, getY, AnyEvent } from '../../../common/utils';
export interface AgDragEvent {
event: AnyEvent;
type: 'start' | 'drag' | 'end';
x: number;
y: number;
dx: number;
dy: number;
event: AnyEvent;
type: 'start' | 'drag' | 'end';
x: number;
y: number;
dx: number;
dy: number;
}
export interface AgDragOptions {
relative?: 'self' | 'parent';
prevent?: boolean;
relative?: 'self' | 'parent';
prevent?: boolean;
}
export function handleDrag(element: HTMLElement, emit: (event: AgDragEvent) => void, options: AgDragOptions = {}) {
// typeof PointerEvent !== 'undefined'
const eventSets = window.navigator.pointerEnabled ? [
{ down: 'pointerdown', move: 'pointermove', up: 'pointerup' }, // , up2: 'pointercancel' },
] : [
{ down: 'mousedown', move: 'mousemove', up: 'mouseup' },
{ down: 'touchstart', move: 'touchmove', up: 'touchend', up2: 'touchcancel' },
];
const emptyRect = { left: 0, top: 0 };
let rect = emptyRect;
let scrollLeft = 0;
let scrollTop = 0;
let startX = 0;
let startY = 0;
let button = 0;
let dragging = false;
let lastEvent: any;
// typeof PointerEvent !== 'undefined'
const eventSets = window.navigator.pointerEnabled ? [
{ down: 'pointerdown', move: 'pointermove', up: 'pointerup' }, // , up2: 'pointercancel' },
] : [
{ down: 'mousedown', move: 'mousemove', up: 'mouseup' },
{ down: 'touchstart', move: 'touchmove', up: 'touchend', up2: 'touchcancel' },
];
const emptyRect = { left: 0, top: 0 };
let rect = emptyRect;
let scrollLeft = 0;
let scrollTop = 0;
let startX = 0;
let startY = 0;
let button = 0;
let dragging = false;
let lastEvent: any;
function setupScrollAndRect() {
// TODO: fix issue with scroll
switch (options.relative) {
case 'self':
rect = element.getBoundingClientRect();
scrollLeft = -(window.scrollX || window.pageXOffset || 0);
scrollTop = -(window.scrollY || window.pageYOffset || 0);
break;
case 'parent':
rect = element.parentElement!.getBoundingClientRect();
scrollLeft = element.parentElement!.scrollLeft;
scrollTop = element.parentElement!.scrollTop;
break;
default:
rect = emptyRect;
scrollLeft = 0;
scrollTop = 0;
}
}
function setupScrollAndRect() {
// TODO: fix issue with scroll
switch (options.relative) {
case 'self':
rect = element.getBoundingClientRect();
scrollLeft = -(window.scrollX || window.pageXOffset || 0);
scrollTop = -(window.scrollY || window.pageYOffset || 0);
break;
case 'parent':
rect = element.parentElement!.getBoundingClientRect();
scrollLeft = element.parentElement!.scrollLeft;
scrollTop = element.parentElement!.scrollTop;
break;
default:
rect = emptyRect;
scrollLeft = 0;
scrollTop = 0;
}
}
function send(event: AnyEvent, type: 'start' | 'drag' | 'end') {
const x = getX(event);
const y = getY(event);
function send(event: AnyEvent, type: 'start' | 'drag' | 'end') {
const x = getX(event);
const y = getY(event);
emit({
event,
type,
x: x - rect.left + scrollLeft,
y: y - rect.top + scrollTop,
dx: x - startX,
dy: y - startY,
});
}
emit({
event,
type,
x: x - rect.left + scrollLeft,
y: y - rect.top + scrollTop,
dx: x - startX,
dy: y - startY,
});
}
const handlers = eventSets.map(events => {
function move(e: any) {
lastEvent = e;
e.preventDefault();
send(e, 'drag');
}
const handlers = eventSets.map(events => {
function move(e: any) {
lastEvent = e;
e.preventDefault();
send(e, 'drag');
}
function up(e: any) {
if (getButton(e) === button) {
// touchend event does not have x, y coordinates, use last touchmove event instead
if (e.type !== 'touchend' && e.type !== 'touchcancel') {
lastEvent = e;
}
end();
}
}
function up(e: any) {
if (getButton(e) === button) {
// touchend event does not have x, y coordinates, use last touchmove event instead
if (e.type !== 'touchend' && e.type !== 'touchcancel') {
lastEvent = e;
}
end();
}
}
function end() {
send(lastEvent, 'end');
window.removeEventListener(events.move, move);
window.removeEventListener(events.up, up);
events.up2 && window.removeEventListener(events.up2, up);
window.removeEventListener('blur', end);
dragging = false;
}
function end() {
send(lastEvent, 'end');
window.removeEventListener(events.move, move);
window.removeEventListener(events.up, up);
events.up2 && window.removeEventListener(events.up2, up);
window.removeEventListener('blur', end);
dragging = false;
}
function handler(e: any) {
if (!dragging) {
setupScrollAndRect();
dragging = true;
button = getButton(e);
startX = getX(e);
startY = getY(e);
send(e, 'start');
lastEvent = e;
function handler(e: any) {
if (!dragging) {
setupScrollAndRect();
dragging = true;
button = getButton(e);
startX = getX(e);
startY = getY(e);
send(e, 'start');
lastEvent = e;
window.addEventListener(events.move, move);
window.addEventListener(events.up, up);
events.up2 && window.addEventListener(events.up2, up);
window.addEventListener('blur', end);
e.stopPropagation();
window.addEventListener(events.move, move);
window.addEventListener(events.up, up);
events.up2 && window.addEventListener(events.up2, up);
window.addEventListener('blur', end);
e.stopPropagation();
if (options.prevent) {
e.preventDefault();
}
}
}
if (options.prevent) {
e.preventDefault();
}
}
}
element.addEventListener(events.down, handler);
return () => element.removeEventListener(events.down, handler);
});
element.addEventListener(events.down, handler);
return () => element.removeEventListener(events.down, handler);
});
return () => handlers.forEach(f => f());
return () => handlers.forEach(f => f());
}
@Directive({ selector: '[agDrag]' })
export class AgDrag implements OnInit, OnDestroy {
@Input('agDragRelative') relative: 'self' | 'parent' | undefined = undefined;
@Input('agDragPrevent') prevent = false;
@Output('agDrag') drag = new EventEmitter<AgDragEvent>();
private unsubscribe = noop;
constructor(private element: ElementRef) {
}
ngOnInit() {
this.unsubscribe = handleDrag(this.element.nativeElement, e => this.drag.emit(e), this);
}
ngOnDestroy() {
this.unsubscribe();
}
@Input('agDragRelative') relative: 'self' | 'parent' | undefined = undefined;
@Input('agDragPrevent') prevent = false;
@Output('agDrag') drag = new EventEmitter<AgDragEvent>();
private unsubscribe = noop;
constructor(private element: ElementRef) {
}
ngOnInit() {
this.unsubscribe = handleDrag(this.element.nativeElement, e => this.drag.emit(e), this);
}
ngOnDestroy() {
this.unsubscribe();
}
}
+10 -10
View File
@@ -1,17 +1,17 @@
import { Directive, OnInit, ElementRef } from '@angular/core';
@Directive({
selector: 'a[href]'
selector: 'a[href]'
})
export class Anchor implements OnInit {
constructor(private element: ElementRef) {
}
ngOnInit() {
const a = this.element.nativeElement as HTMLAnchorElement;
constructor(private element: ElementRef) {
}
ngOnInit() {
const a = this.element.nativeElement as HTMLAnchorElement;
if (/^(https?|mailto):/.test(a.href) && !a.target) {
a.setAttribute('target', '_blank');
a.setAttribute('rel', 'noopener noreferrer');
}
}
if (/^(https?|mailto):/.test(a.href) && !a.target) {
a.setAttribute('target', '_blank');
a.setAttribute('rel', 'noopener noreferrer');
}
}
}
@@ -2,29 +2,29 @@ import { Directive, Input, Optional } from '@angular/core';
import { NgModel } from '@angular/forms';
@Directive({
selector: '[btnHighlight]',
host: {
'[class.btn-default]': '!on',
'[class.btn-primary]': 'on',
},
selector: '[btnHighlight]',
host: {
'[class.btn-default]': '!on',
'[class.btn-primary]': 'on',
},
})
export class BtnHighlight {
@Input() btnHighlight?: boolean = undefined;
constructor(@Optional() private model?: NgModel) {
}
get on() {
const value = this.btnHighlight;
return (value === true || value === false || !this.model) ? value : !!this.model.value;
}
@Input() btnHighlight?: boolean = undefined;
constructor(@Optional() private model?: NgModel) {
}
get on() {
const value = this.btnHighlight;
return (value === true || value === false || !this.model) ? value : !!this.model.value;
}
}
@Directive({
selector: '[btnHighlightDanger]',
host: {
'[class.btn-default]': '!btnHighlightDanger',
'[class.btn-danger]': 'btnHighlightDanger',
},
selector: '[btnHighlightDanger]',
host: {
'[class.btn-default]': '!btnHighlightDanger',
'[class.btn-danger]': 'btnHighlightDanger',
},
})
export class BtnHighlightDanger {
@Input() btnHighlightDanger = false;
@Input() btnHighlightDanger = false;
}
+169 -169
View File
@@ -6,202 +6,202 @@ import { rect } from '../../../common/rect';
@Injectable({ providedIn: 'root' })
export class DraggableService {
root?: ElementRef;
draggedItem?: any;
activeDropZone?: DraggableDrop<any>;
dropZones: DraggableDrop<any>[] = [];
get rootElement(): HTMLElement {
return this.root ? this.root.nativeElement : document.body;
}
setActiveDropZone(dropZone: DraggableDrop<any> | undefined) {
if (this.activeDropZone !== dropZone) {
if (this.activeDropZone) {
this.activeDropZone.setActive(false);
}
root?: ElementRef;
draggedItem?: any;
activeDropZone?: DraggableDrop<any>;
dropZones: DraggableDrop<any>[] = [];
get rootElement(): HTMLElement {
return this.root ? this.root.nativeElement : document.body;
}
setActiveDropZone(dropZone: DraggableDrop<any> | undefined) {
if (this.activeDropZone !== dropZone) {
if (this.activeDropZone) {
this.activeDropZone.setActive(false);
}
this.activeDropZone = dropZone;
this.activeDropZone = dropZone;
if (this.activeDropZone) {
this.activeDropZone.setActive(true);
}
}
}
startMove(element: HTMLElement, item: any) {
this.setActiveDropZone(undefined);
this.rootElement.appendChild(element);
this.draggedItem = item;
this.initRects();
}
endMove() {
if (this.activeDropZone) {
this.activeDropZone.drop.emit(this.draggedItem);
this.setActiveDropZone(undefined);
}
if (this.activeDropZone) {
this.activeDropZone.setActive(true);
}
}
}
startMove(element: HTMLElement, item: any) {
this.setActiveDropZone(undefined);
this.rootElement.appendChild(element);
this.draggedItem = item;
this.initRects();
}
endMove() {
if (this.activeDropZone) {
this.activeDropZone.drop.emit(this.draggedItem);
this.setActiveDropZone(undefined);
}
this.draggedItem = undefined;
}
addDropZone(dropZone: DraggableDrop<any>) {
this.dropZones.push(dropZone);
this.draggedItem = undefined;
}
addDropZone(dropZone: DraggableDrop<any>) {
this.dropZones.push(dropZone);
if (this.draggedItem) {
this.initRects();
}
}
removeDropZone(dropZone: DraggableDrop<any>) {
removeItem(this.dropZones, dropZone);
if (this.draggedItem) {
this.initRects();
}
}
removeDropZone(dropZone: DraggableDrop<any>) {
removeItem(this.dropZones, dropZone);
if (this.draggedItem) {
this.initRects();
}
if (this.draggedItem) {
this.initRects();
}
if (this.activeDropZone === dropZone) {
this.setActiveDropZone(undefined);
}
}
updateHover(x: number, y: number) {
if (this.draggedItem) {
for (const zone of this.dropZones) {
if (pointInRect(x, y, zone.rect)) {
this.setActiveDropZone(zone);
return;
}
}
if (this.activeDropZone === dropZone) {
this.setActiveDropZone(undefined);
}
}
updateHover(x: number, y: number) {
if (this.draggedItem) {
for (const zone of this.dropZones) {
if (pointInRect(x, y, zone.rect)) {
this.setActiveDropZone(zone);
return;
}
}
this.setActiveDropZone(undefined);
}
}
private initRects() {
this.dropZones.forEach(i => i.initRect());
}
this.setActiveDropZone(undefined);
}
}
private initRects() {
this.dropZones.forEach(i => i.initRect());
}
}
@Component({
selector: 'draggable-outlet',
template: `<div></div>`,
styles: [`:host { position: fixed; top: 0; left: 0; z-index: 10000; }`],
selector: 'draggable-outlet',
template: `<div></div>`,
styles: [`:host { position: fixed; top: 0; left: 0; z-index: 10000; }`],
})
export class DraggableOutlet {
constructor(element: ElementRef, service: DraggableService) {
service.root = element;
}
constructor(element: ElementRef, service: DraggableService) {
service.root = element;
}
}
@Directive({ selector: '[draggableDrop]' })
export class DraggableDrop<T> implements OnInit, OnDestroy {
@Input('draggablePad') pad = 0;
@Output('draggableDrop') drop = new EventEmitter<T>();
rect = rect(0, 0, 0, 0);
constructor(private element: ElementRef, private service: DraggableService) {
}
ngOnInit() {
this.service.addDropZone(this);
}
ngOnDestroy() {
this.service.removeDropZone(this);
}
setActive(active: boolean) {
const element = this.element.nativeElement as HTMLElement;
@Input('draggablePad') pad = 0;
@Output('draggableDrop') drop = new EventEmitter<T>();
rect = rect(0, 0, 0, 0);
constructor(private element: ElementRef, private service: DraggableService) {
}
ngOnInit() {
this.service.addDropZone(this);
}
ngOnDestroy() {
this.service.removeDropZone(this);
}
setActive(active: boolean) {
const element = this.element.nativeElement as HTMLElement;
if (active) {
element.classList.add('draggable-hover');
} else {
element.classList.remove('draggable-hover');
}
}
initRect() {
const element = this.element.nativeElement as HTMLElement;
const clientBounds = element.getBoundingClientRect();
this.rect.x = clientBounds.left - this.pad;
this.rect.y = clientBounds.top - this.pad;
this.rect.w = clientBounds.width + 2 * this.pad;
this.rect.h = clientBounds.height + 2 * this.pad;
}
if (active) {
element.classList.add('draggable-hover');
} else {
element.classList.remove('draggable-hover');
}
}
initRect() {
const element = this.element.nativeElement as HTMLElement;
const clientBounds = element.getBoundingClientRect();
this.rect.x = clientBounds.left - this.pad;
this.rect.y = clientBounds.top - this.pad;
this.rect.w = clientBounds.width + 2 * this.pad;
this.rect.h = clientBounds.height + 2 * this.pad;
}
}
@Directive({
selector: '[draggableItem]',
host: {
'[style.touch-action]': `touchAction`,
}
selector: '[draggableItem]',
host: {
'[style.touch-action]': `touchAction`,
}
})
export class DraggableItem<T> implements OnInit, OnDestroy {
@Input('draggableItem') item: T | undefined;
@Output('draggableDrag') dragStarted = new EventEmitter<void>();
private startX = 0;
private startY = 0;
private draggable?: HTMLElement;
private width = 0;
private height = 0;
private unsubscribeDrag = noop;
private _disabled = false;
constructor(private element: ElementRef, private service: DraggableService) {
}
ngOnInit() {
this.setupDragEvents();
}
ngOnDestroy() {
this.unsubscribeDrag();
}
get touchAction() {
return this.disabled ? 'inherit' : 'none';
}
@Input('draggableDisabled') get disabled() {
return this._disabled;
}
set disabled(value) {
if (this._disabled !== value) {
this._disabled = value;
this.setupDragEvents();
}
}
private setupDragEvents() {
this.unsubscribeDrag();
this.unsubscribeDrag = noop;
@Input('draggableItem') item: T | undefined;
@Output('draggableDrag') dragStarted = new EventEmitter<void>();
private startX = 0;
private startY = 0;
private draggable?: HTMLElement;
private width = 0;
private height = 0;
private unsubscribeDrag = noop;
private _disabled = false;
constructor(private element: ElementRef, private service: DraggableService) {
}
ngOnInit() {
this.setupDragEvents();
}
ngOnDestroy() {
this.unsubscribeDrag();
}
get touchAction() {
return this.disabled ? 'inherit' : 'none';
}
@Input('draggableDisabled') get disabled() {
return this._disabled;
}
set disabled(value) {
if (this._disabled !== value) {
this._disabled = value;
this.setupDragEvents();
}
}
private setupDragEvents() {
this.unsubscribeDrag();
this.unsubscribeDrag = noop;
if (!this.disabled) {
this.unsubscribeDrag = handleDrag(this.element.nativeElement, e => this.drag(e), { prevent: true });
}
}
drag(e: AgDragEvent) {
if (this.item && !this.disabled && !this.draggable && (Math.abs(e.dx) > 5 || Math.abs(e.dy) > 5)) {
const element = this.element.nativeElement as HTMLElement;
const rect = element.getBoundingClientRect();
this.startX = rect.left;
this.startY = rect.top;
this.draggable = element.cloneNode(true) as HTMLElement;
this.draggable.style.position = 'absolute';
this.draggable.style.width = `${rect.width}px`;
this.draggable.style.height = `${rect.height}px`;
this.draggable.style.margin = '0';
this.draggable.classList.add('draggable-dragging');
this.width = rect.width;
this.height = rect.height;
if (!this.disabled) {
this.unsubscribeDrag = handleDrag(this.element.nativeElement, e => this.drag(e), { prevent: true });
}
}
drag(e: AgDragEvent) {
if (this.item && !this.disabled && !this.draggable && (Math.abs(e.dx) > 5 || Math.abs(e.dy) > 5)) {
const element = this.element.nativeElement as HTMLElement;
const rect = element.getBoundingClientRect();
this.startX = rect.left;
this.startY = rect.top;
this.draggable = element.cloneNode(true) as HTMLElement;
this.draggable.style.position = 'absolute';
this.draggable.style.width = `${rect.width}px`;
this.draggable.style.height = `${rect.height}px`;
this.draggable.style.margin = '0';
this.draggable.classList.add('draggable-dragging');
this.width = rect.width;
this.height = rect.height;
const src = element.querySelectorAll('canvas') as NodeListOf<HTMLCanvasElement>;
const dst = this.draggable.querySelectorAll('canvas') as NodeListOf<HTMLCanvasElement>;
const src = element.querySelectorAll('canvas') as NodeListOf<HTMLCanvasElement>;
const dst = this.draggable.querySelectorAll('canvas') as NodeListOf<HTMLCanvasElement>;
for (let i = 0; i < src.length; i++) {
const context = dst.item(i).getContext('2d');
context && context.drawImage(src.item(i), 0, 0);
}
for (let i = 0; i < src.length; i++) {
const context = dst.item(i).getContext('2d');
context && context.drawImage(src.item(i), 0, 0);
}
this.service.startMove(this.draggable, this.item!);
this.dragStarted.emit();
}
this.service.startMove(this.draggable, this.item!);
this.dragStarted.emit();
}
if (this.draggable) {
if (e.type === 'end') {
this.draggable!.parentNode!.removeChild(this.draggable!);
this.draggable = undefined;
this.service.endMove();
} else {
const x = clamp(this.startX + e.dx, 0, window.innerWidth - this.width);
const y = clamp(this.startY + e.dy, 0, window.innerHeight - this.height);
setTransform(this.draggable, `translate3d(${x}px, ${y}px, 0px)`);
this.service.updateHover(e.x, e.y);
}
}
}
if (this.draggable) {
if (e.type === 'end') {
this.draggable!.parentNode!.removeChild(this.draggable!);
this.draggable = undefined;
this.service.endMove();
} else {
const x = clamp(this.startX + e.dx, 0, window.innerWidth - this.width);
const y = clamp(this.startY + e.dy, 0, window.innerHeight - this.height);
setTransform(this.draggable, `translate3d(${x}px, ${y}px, 0px)`);
this.service.updateHover(e.x, e.y);
}
}
}
}
export const draggableComponents = [DraggableOutlet, DraggableItem, DraggableDrop];
+179 -179
View File
@@ -1,227 +1,227 @@
import {
Directive, HostListener, Input, Output, EventEmitter, TemplateRef, ViewContainerRef, ContentChild,
Renderer2, ElementRef, EmbeddedViewRef, Component, Injectable
Directive, HostListener, Input, Output, EventEmitter, TemplateRef, ViewContainerRef, ContentChild,
Renderer2, ElementRef, EmbeddedViewRef, Component, Injectable
} from '@angular/core';
import { uniqueId } from 'lodash';
import { focusFirstElement } from '../../../client/htmlUtils';
@Injectable({ providedIn: 'root' })
export class DropdownOutletService {
viewContainer?: ViewContainerRef;
rootElement?: HTMLElement;
viewContainer?: ViewContainerRef;
rootElement?: HTMLElement;
}
@Component({
selector: 'dropdown-outlet',
template: `<ng-template></ng-template>`,
selector: 'dropdown-outlet',
template: `<ng-template></ng-template>`,
})
export class DropdownOutlet {
constructor(service: DropdownOutletService, viewContainer: ViewContainerRef, element: ElementRef) {
service.viewContainer = viewContainer;
service.rootElement = element.nativeElement.parentElement;
}
constructor(service: DropdownOutletService, viewContainer: ViewContainerRef, element: ElementRef) {
service.viewContainer = viewContainer;
service.rootElement = element.nativeElement.parentElement;
}
}
@Directive({
selector: '[dropdownMenu]',
selector: '[dropdownMenu]',
})
export class DropdownMenu {
ref?: EmbeddedViewRef<any>;
id = uniqueId('dropdown-menu-');
private onClose?: () => void;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private renderer: Renderer2,
private service: DropdownOutletService,
) {
}
private get root(): HTMLElement {
return this.ref && this.ref.rootNodes[0];
}
open(useOutlet: boolean, rootElement: HTMLElement) {
if (!this.ref) {
if (useOutlet) {
this.ref = this.service.viewContainer!.createEmbeddedView(this.templateRef);
} else {
this.ref = this.viewContainer.createEmbeddedView(this.templateRef);
}
ref?: EmbeddedViewRef<any>;
id = uniqueId('dropdown-menu-');
private onClose?: () => void;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private renderer: Renderer2,
private service: DropdownOutletService,
) {
}
private get root(): HTMLElement {
return this.ref && this.ref.rootNodes[0];
}
open(useOutlet: boolean, rootElement: HTMLElement) {
if (!this.ref) {
if (useOutlet) {
this.ref = this.service.viewContainer!.createEmbeddedView(this.templateRef);
} else {
this.ref = this.viewContainer.createEmbeddedView(this.templateRef);
}
const { renderer, root } = this;
const { renderer, root } = this;
renderer.addClass(root, 'show');
renderer.setAttribute(root, 'id', this.id);
renderer.addClass(root, 'show');
renderer.setAttribute(root, 'id', this.id);
if (useOutlet) {
const positionMenu = () => {
const rect = rootElement.getBoundingClientRect();
const menuRect = root.getBoundingClientRect();
let transform: string;
if (useOutlet) {
const positionMenu = () => {
const rect = rootElement.getBoundingClientRect();
const menuRect = root.getBoundingClientRect();
let transform: string;
if ((rect.bottom + menuRect.height) > window.innerHeight) {
transform = `translate3d(${Math.round(rect.left)}px, ${Math.round(rect.top - menuRect.height)}px, 0)`;
renderer.addClass(root, 'dropdown-menu-up');
} else {
transform = `translate3d(${Math.round(rect.left)}px, ${Math.round(rect.bottom)}px, 0)`;
renderer.removeClass(root, 'dropdown-menu-up');
}
if ((rect.bottom + menuRect.height) > window.innerHeight) {
transform = `translate3d(${Math.round(rect.left)}px, ${Math.round(rect.top - menuRect.height)}px, 0)`;
renderer.addClass(root, 'dropdown-menu-up');
} else {
transform = `translate3d(${Math.round(rect.left)}px, ${Math.round(rect.bottom)}px, 0)`;
renderer.removeClass(root, 'dropdown-menu-up');
}
renderer.setStyle(root, 'transform', transform);
};
renderer.setStyle(root, 'transform', transform);
};
renderer.addClass(root, 'dropdown-in-outlet');
positionMenu();
renderer.addClass(root, 'dropdown-in-outlet');
positionMenu();
const closeDropdown = () => {
this.close();
};
const closeDropdown = () => {
this.close();
};
document.addEventListener('scroll', closeDropdown, true);
window.addEventListener('resize', closeDropdown, true);
document.addEventListener('scroll', closeDropdown, true);
window.addEventListener('resize', closeDropdown, true);
this.onClose = () => {
document.removeEventListener('scroll', closeDropdown, true);
window.removeEventListener('resize', closeDropdown, true);
};
}
}
}
close() {
if (this.ref) {
this.ref.destroy();
this.ref = undefined;
}
this.onClose = () => {
document.removeEventListener('scroll', closeDropdown, true);
window.removeEventListener('resize', closeDropdown, true);
};
}
}
}
close() {
if (this.ref) {
this.ref.destroy();
this.ref = undefined;
}
if (this.onClose) {
this.onClose();
this.onClose = undefined;
}
}
checkTarget(e: Event) {
return this.root && this.root.contains(e.target as any);
}
focusFirstElement() {
if (this.root) {
focusFirstElement(this.root);
}
}
if (this.onClose) {
this.onClose();
this.onClose = undefined;
}
}
checkTarget(e: Event) {
return this.root && this.root.contains(e.target as any);
}
focusFirstElement() {
if (this.root) {
focusFirstElement(this.root);
}
}
}
@Directive({
selector: '[dropdown]',
exportAs: 'ag-dropdown',
host: {
'[class.show]': 'isOpen',
},
selector: '[dropdown]',
exportAs: 'ag-dropdown',
host: {
'[class.show]': 'isOpen',
},
})
export class Dropdown {
dropdownToggle?: DropdownToggle;
@ContentChild(DropdownMenu, { static: false }) menu!: DropdownMenu;
@Input() autoClose: boolean | 'outsideClick' = true;
@Input() preventAutoCloseOnOutlet = false;
@Input() hookToCanvas = false;
@Input() focusOnOpen = true;
@Input() focusOnClose = true;
@Input() useOutlet = false;
@Input() isOpen = false;
@Output() isOpenChange = new EventEmitter<boolean>();
get menuId() {
return this.isOpen ? this.menu.id : '';
}
constructor(private element: ElementRef, private service: DropdownOutletService) {
}
open() {
if (!this.isOpen) {
this.isOpen = true;
this.isOpenChange.emit(true);
this.menu.open(this.useOutlet, this.element.nativeElement);
dropdownToggle?: DropdownToggle;
@ContentChild(DropdownMenu, { static: false }) menu!: DropdownMenu;
@Input() autoClose: boolean | 'outsideClick' = true;
@Input() preventAutoCloseOnOutlet = false;
@Input() hookToCanvas = false;
@Input() focusOnOpen = true;
@Input() focusOnClose = true;
@Input() useOutlet = false;
@Input() isOpen = false;
@Output() isOpenChange = new EventEmitter<boolean>();
get menuId() {
return this.isOpen ? this.menu.id : '';
}
constructor(private element: ElementRef, private service: DropdownOutletService) {
}
open() {
if (!this.isOpen) {
this.isOpen = true;
this.isOpenChange.emit(true);
this.menu.open(this.useOutlet, this.element.nativeElement);
setTimeout(() => {
document.addEventListener('click', this.closeHandler);
document.addEventListener('keydown', this.closeHandler);
setTimeout(() => {
document.addEventListener('click', this.closeHandler);
document.addEventListener('keydown', this.closeHandler);
if (this.focusOnOpen) {
this.menu.focusFirstElement();
}
if (this.focusOnOpen) {
this.menu.focusFirstElement();
}
if (this.hookToCanvas) {
const canvas = document.getElementById('canvas');
if (this.hookToCanvas) {
const canvas = document.getElementById('canvas');
if (canvas) {
canvas.addEventListener('touchstart', this.canvasCloseHandler);
canvas.addEventListener('mousedown', this.canvasCloseHandler);
}
}
});
}
}
close() {
if (this.isOpen) {
this.isOpen = false;
this.isOpenChange.emit(false);
this.menu.close();
if (canvas) {
canvas.addEventListener('touchstart', this.canvasCloseHandler);
canvas.addEventListener('mousedown', this.canvasCloseHandler);
}
}
});
}
}
close() {
if (this.isOpen) {
this.isOpen = false;
this.isOpenChange.emit(false);
this.menu.close();
if (this.focusOnClose && this.dropdownToggle) {
this.dropdownToggle.focus();
}
if (this.focusOnClose && this.dropdownToggle) {
this.dropdownToggle.focus();
}
document.removeEventListener('click', this.closeHandler);
document.removeEventListener('keydown', this.closeHandler);
document.removeEventListener('click', this.closeHandler);
document.removeEventListener('keydown', this.closeHandler);
if (this.hookToCanvas) {
const canvas = document.getElementById('canvas');
if (this.hookToCanvas) {
const canvas = document.getElementById('canvas');
if (canvas) {
canvas.removeEventListener('touchstart', this.canvasCloseHandler);
canvas.removeEventListener('mousedown', this.canvasCloseHandler);
}
}
}
}
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
private closeHandler: any = (e: KeyboardEvent) => {
if (
!e.keyCode
&& (this.autoClose || (this.dropdownToggle && this.dropdownToggle.checkTarget(e)))
&& !(this.preventAutoCloseOnOutlet && this.service.rootElement && this.service.rootElement.contains(e.target as any))
&& !(this.autoClose === 'outsideClick' && this.menu.checkTarget(e))
) {
this.close();
} else if (this.autoClose && e.keyCode === 27) { // esc
this.close();
}
}
private canvasCloseHandler: any = () => this.close();
if (canvas) {
canvas.removeEventListener('touchstart', this.canvasCloseHandler);
canvas.removeEventListener('mousedown', this.canvasCloseHandler);
}
}
}
}
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
private closeHandler: any = (e: KeyboardEvent) => {
if (
!e.keyCode
&& (this.autoClose || (this.dropdownToggle && this.dropdownToggle.checkTarget(e)))
&& !(this.preventAutoCloseOnOutlet && this.service.rootElement && this.service.rootElement.contains(e.target as any))
&& !(this.autoClose === 'outsideClick' && this.menu.checkTarget(e))
) {
this.close();
} else if (this.autoClose && e.keyCode === 27) { // esc
this.close();
}
}
private canvasCloseHandler: any = () => this.close();
}
@Directive({
selector: '[dropdownToggle]',
host: {
'aria-haspopup': 'true',
'[attr.aria-expanded]': 'dropdown.isOpen',
'[attr.aria-controls]': 'dropdown.isOpen ? dropdown.menuId : undefined',
},
selector: '[dropdownToggle]',
host: {
'aria-haspopup': 'true',
'[attr.aria-expanded]': 'dropdown.isOpen',
'[attr.aria-controls]': 'dropdown.isOpen ? dropdown.menuId : undefined',
},
})
export class DropdownToggle {
constructor(private element: ElementRef, public dropdown: Dropdown) {
dropdown.dropdownToggle = this;
}
@HostListener('click')
click() {
this.dropdown.toggle();
}
checkTarget(e: Event) {
return this.element.nativeElement.contains(e.target);
}
focus() {
this.element.nativeElement.focus();
}
constructor(private element: ElementRef, public dropdown: Dropdown) {
dropdown.dropdownToggle = this;
}
@HostListener('click')
click() {
this.dropdown.toggle();
}
checkTarget(e: Event) {
return this.element.nativeElement.contains(e.target);
}
focus() {
this.element.nativeElement.focus();
}
}
export const dropdownDirectives = [Dropdown, DropdownToggle, DropdownMenu, DropdownOutlet];
+15 -15
View File
@@ -1,22 +1,22 @@
import { Directive, ElementRef, Input, HostListener, HostBinding, Output, EventEmitter } from '@angular/core';
@Directive({
selector: '[fixToTop]',
selector: '[fixToTop]',
})
export class FixToTop {
@Input() fixToTopOffset = 0;
@Output() fixToTop = new EventEmitter<boolean>();
@HostBinding('class.fixed-to-top') fixed = false;
constructor(private element: ElementRef) {
}
@HostListener('window:scroll')
scroll() {
const element = this.element.nativeElement as HTMLElement;
const { top } = element.getBoundingClientRect();
@Input() fixToTopOffset = 0;
@Output() fixToTop = new EventEmitter<boolean>();
@HostBinding('class.fixed-to-top') fixed = false;
constructor(private element: ElementRef) {
}
@HostListener('window:scroll')
scroll() {
const element = this.element.nativeElement as HTMLElement;
const { top } = element.getBoundingClientRect();
if (this.fixed !== top < this.fixToTopOffset) {
this.fixed = top < this.fixToTopOffset;
this.fixToTop.emit(this.fixed);
}
}
if (this.fixed !== top < this.fixToTopOffset) {
this.fixed = top < this.fixToTopOffset;
this.fixToTop.emit(this.fixed);
}
}
}
@@ -1,15 +1,15 @@
import { Directive, AfterViewInit, ElementRef } from '@angular/core';
@Directive({
selector: '[focusTitle]',
host: {
'tabindex': '-1',
},
selector: '[focusTitle]',
host: {
'tabindex': '-1',
},
})
export class FocusTitle implements AfterViewInit {
constructor(private element: ElementRef) {
}
ngAfterViewInit() {
setTimeout(() => this.element.nativeElement.focus());
}
constructor(private element: ElementRef) {
}
ngAfterViewInit() {
setTimeout(() => this.element.nativeElement.focus());
}
}
@@ -3,55 +3,55 @@ import { isParentOf, focusFirstElement, findFocusableElements } from '../../../c
import { isMobile } from '../../../client/data';
@Directive({
selector: '[focusTrap]',
selector: '[focusTrap]',
})
export class FocusTrap implements OnInit, OnDestroy {
private on = true;
private lastActiveElement?: HTMLElement;
@Input() set focusTrap(value: boolean) {
if (this.on !== value) {
this.on = value;
this.update();
}
}
constructor(private element: ElementRef) {
}
ngOnInit() {
this.update();
}
ngOnDestroy() {
this.focusTrap = false;
}
private update() {
if (!isMobile) {
if (this.on) {
this.lastActiveElement = document.activeElement as HTMLElement;
document.addEventListener('focusin', this.focus);
private on = true;
private lastActiveElement?: HTMLElement;
@Input() set focusTrap(value: boolean) {
if (this.on !== value) {
this.on = value;
this.update();
}
}
constructor(private element: ElementRef) {
}
ngOnInit() {
this.update();
}
ngOnDestroy() {
this.focusTrap = false;
}
private update() {
if (!isMobile) {
if (this.on) {
this.lastActiveElement = document.activeElement as HTMLElement;
document.addEventListener('focusin', this.focus);
if (!isParentOf(this.element.nativeElement, this.lastActiveElement)) {
setTimeout(() => this.lastActiveElement = focusFirstElement(this.element.nativeElement));
}
} else {
this.lastActiveElement = undefined;
document.removeEventListener('focusin', this.focus);
}
}
}
private focus = (e: Event) => {
if (isParentOf(this.element.nativeElement, e.target as any)) {
this.lastActiveElement = e.target as any;
} else {
const focusable = findFocusableElements(this.element.nativeElement);
if (!isParentOf(this.element.nativeElement, this.lastActiveElement)) {
setTimeout(() => this.lastActiveElement = focusFirstElement(this.element.nativeElement));
}
} else {
this.lastActiveElement = undefined;
document.removeEventListener('focusin', this.focus);
}
}
}
private focus = (e: Event) => {
if (isParentOf(this.element.nativeElement, e.target as any)) {
this.lastActiveElement = e.target as any;
} else {
const focusable = findFocusableElements(this.element.nativeElement);
if (focusable.length) {
if (this.lastActiveElement === focusable[0]) {
this.lastActiveElement = focusable[focusable.length - 1];
} else {
this.lastActiveElement = focusable[0];
}
if (focusable.length) {
if (this.lastActiveElement === focusable[0]) {
this.lastActiveElement = focusable[focusable.length - 1];
} else {
this.lastActiveElement = focusable[0];
}
this.lastActiveElement.focus();
}
}
}
this.lastActiveElement.focus();
}
}
}
}
@@ -4,57 +4,57 @@ import { hasFeatureFlag, featureFlagsChanged } from '../../../client/clientUtils
import { Model } from '../../services/model';
@Directive({
selector: '[hasFeature]',
selector: '[hasFeature]',
})
export class HasFeature implements AfterViewInit, OnDestroy {
private subscriptions: Subscription[] = [];
private showing = false;
private _flag: string | undefined = undefined;
private _orMod = false;
private _alsoIf = true;
private ref?: EmbeddedViewRef<any>;
constructor(private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef, private model: Model) {
}
ngAfterViewInit() {
this.subscriptions.push(featureFlagsChanged.subscribe(() => this.update()));
this.subscriptions.push(this.model.accountChanged.subscribe(() => this.update()));
}
ngOnDestroy() {
this.subscriptions.forEach(s => s.unsubscribe());
}
@Input()
set hasFeature(value: string | undefined) {
if (this._flag !== value) {
this._flag = value;
this.update();
}
}
@Input()
set hasFeatureOrMod(value: boolean) {
if (this._orMod !== value) {
this._orMod = value;
this.update();
}
}
@Input()
set hasFeatureAlso(value: boolean) {
if (this._alsoIf !== value) {
this._alsoIf = value;
this.update();
}
}
private update() {
const show = this._alsoIf && (hasFeatureFlag(this._flag as any) || (this._orMod && this.model.isMod));
private subscriptions: Subscription[] = [];
private showing = false;
private _flag: string | undefined = undefined;
private _orMod = false;
private _alsoIf = true;
private ref?: EmbeddedViewRef<any>;
constructor(private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef, private model: Model) {
}
ngAfterViewInit() {
this.subscriptions.push(featureFlagsChanged.subscribe(() => this.update()));
this.subscriptions.push(this.model.accountChanged.subscribe(() => this.update()));
}
ngOnDestroy() {
this.subscriptions.forEach(s => s.unsubscribe());
}
@Input()
set hasFeature(value: string | undefined) {
if (this._flag !== value) {
this._flag = value;
this.update();
}
}
@Input()
set hasFeatureOrMod(value: boolean) {
if (this._orMod !== value) {
this._orMod = value;
this.update();
}
}
@Input()
set hasFeatureAlso(value: boolean) {
if (this._alsoIf !== value) {
this._alsoIf = value;
this.update();
}
}
private update() {
const show = this._alsoIf && (hasFeatureFlag(this._flag as any) || (this._orMod && this.model.isMod));
if (this.showing !== show) {
this.showing = show;
if (this.showing !== show) {
this.showing = show;
if (show) {
this.ref = this.ref || this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
this.ref = undefined;
}
}
}
if (show) {
this.ref = this.ref || this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
this.ref = undefined;
}
}
}
}
@@ -3,19 +3,19 @@ import { uniqueId } from 'lodash';
import { findParentElement } from '../../../client/htmlUtils';
@Directive({
selector: '[labelledBy]',
selector: '[labelledBy]',
})
export class LabelledBy implements OnInit {
@Input('labelledBy') selector!: string;
constructor(private element: ElementRef) {
}
ngOnInit() {
const element = this.element.nativeElement as HTMLElement;
const target = findParentElement(element, this.selector);
const id = element.id = element.id || uniqueId('labelled-by-');
@Input('labelledBy') selector!: string;
constructor(private element: ElementRef) {
}
ngOnInit() {
const element = this.element.nativeElement as HTMLElement;
const target = findParentElement(element, this.selector);
const id = element.id = element.id || uniqueId('labelled-by-');
if (target) {
target.setAttribute('aria-labelledby', id);
}
}
if (target) {
target.setAttribute('aria-labelledby', id);
}
}
}
@@ -2,13 +2,13 @@ import { Directive, HostBinding } from '@angular/core';
import { RouterLinkActive } from '@angular/router';
@Directive({
selector: '[linkCurrent]',
selector: '[linkCurrent]',
})
export class LinkCurrent {
constructor(private routerLinkActive: RouterLinkActive) {
}
@HostBinding('attr.aria-current')
get current() {
return this.routerLinkActive.isActive ? 'true' : undefined;
}
constructor(private routerLinkActive: RouterLinkActive) {
}
@HostBinding('attr.aria-current')
get current() {
return this.routerLinkActive.isActive ? 'true' : undefined;
}
}
@@ -2,11 +2,11 @@ import { Directive, Input, HostBinding } from '@angular/core';
import { getUrl } from '../../../client/rev';
@Directive({
selector: '[revSrc]',
selector: '[revSrc]',
})
export class RevSrc {
@HostBinding() get src() {
return this.revSrc && getUrl(this.revSrc);
}
@Input() revSrc?: string;
@HostBinding() get src() {
return this.revSrc && getUrl(this.revSrc);
}
@Input() revSrc?: string;
}
@@ -4,23 +4,23 @@ import { Tabset } from '../tabset/tabset';
import { StorageService } from '../../services/storageService';
@Directive({
selector: '[saveActiveTab]',
selector: '[saveActiveTab]',
})
export class SaveActiveTab implements OnInit, OnDestroy {
@Input('saveActiveTab') key!: string;
private subscription?: Subscription;
constructor(@Host() private tabset: Tabset, private storage: StorageService) {
}
ngOnInit() {
// this.tabset.activeIndex = parseInt(this.storage.getItem(this.key) || '0', 10);
this.tabset.select(parseInt(this.storage.getItem(this.key) || '0', 10));
this.subscription = this.tabset.activeIndexChange.subscribe((i: number) => {
this.storage.setItem(this.key, i.toString());
});
}
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
@Input('saveActiveTab') key!: string;
private subscription?: Subscription;
constructor(@Host() private tabset: Tabset, private storage: StorageService) {
}
ngOnInit() {
// this.tabset.activeIndex = parseInt(this.storage.getItem(this.key) || '0', 10);
this.tabset.select(parseInt(this.storage.getItem(this.key) || '0', 10));
this.subscription = this.tabset.activeIndexChange.subscribe((i: number) => {
this.storage.setItem(this.key, i.toString());
});
}
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
}
+66 -66
View File
@@ -5,76 +5,76 @@ import { font } from '../../../client/fonts';
import { getCharacterSprite } from '../../../graphics/spriteFont';
@Component({
selector: 'emote-box',
template: '<img #image class="emote-box pixelart" />',
styles: ['.emote-box { pointer-events: none; }'],
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'emote-box',
template: '<img #image class="emote-box pixelart" />',
styles: ['.emote-box { pointer-events: none; }'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class EmoteBox implements AfterViewInit {
@ViewChild('image', { static: true }) image!: ElementRef;
private emoteValue = '';
private scaleValue = 2;
private initialized = false;
constructor(private zone: NgZone) {
}
ngAfterViewInit() {
loadAndInitSpriteSheets()
.then(() => {
this.initialized = true;
this.zone.runOutsideAngular(() => this.redraw());
});
}
get emote() {
return this.emoteValue;
}
@Input()
set emote(value: string) {
if (this.emoteValue !== value) {
this.emoteValue = value;
this.zone.runOutsideAngular(() => this.redraw());
}
}
get scale() {
return this.scaleValue;
}
@Input()
set scale(value: number) {
if (this.scaleValue !== value) {
this.scaleValue = value;
this.zone.runOutsideAngular(() => this.redraw());
}
}
redraw() {
if (this.initialized) {
const emote = findEmoji(this.emote);
const sprite = font && emote && getCharacterSprite(emote.symbol, font);
const image = this.image.nativeElement as HTMLImageElement;
@ViewChild('image', { static: true }) image!: ElementRef;
private emoteValue = '';
private scaleValue = 2;
private initialized = false;
constructor(private zone: NgZone) {
}
ngAfterViewInit() {
loadAndInitSpriteSheets()
.then(() => {
this.initialized = true;
this.zone.runOutsideAngular(() => this.redraw());
});
}
get emote() {
return this.emoteValue;
}
@Input()
set emote(value: string) {
if (this.emoteValue !== value) {
this.emoteValue = value;
this.zone.runOutsideAngular(() => this.redraw());
}
}
get scale() {
return this.scaleValue;
}
@Input()
set scale(value: number) {
if (this.scaleValue !== value) {
this.scaleValue = value;
this.zone.runOutsideAngular(() => this.redraw());
}
}
redraw() {
if (this.initialized) {
const emote = findEmoji(this.emote);
const sprite = font && emote && getCharacterSprite(emote.symbol, font);
const image = this.image.nativeElement as HTMLImageElement;
if (sprite) {
const width = sprite.w + sprite.ox;
const height = 10; // sprite.h + sprite.oy;
if (sprite) {
const width = sprite.w + sprite.ox;
const height = 10; // sprite.h + sprite.oy;
image.style.width = `${width * this.scale}px`;
image.style.height = `${height * this.scale}px`;
image.style.marginTop = `${-this.scale}px`;
image.style.display = 'inline-block';
image.style.visibility = 'hidden';
image.style.width = `${width * this.scale}px`;
image.style.height = `${height * this.scale}px`;
image.style.marginTop = `${-this.scale}px`;
image.style.display = 'inline-block';
image.style.visibility = 'hidden';
if (emote) {
image.setAttribute('aria-label', emote.names[0]);
}
if (emote) {
image.setAttribute('aria-label', emote.names[0]);
}
getEmojiImageAsync(sprite, src => {
image.src = src;
image.alt = emote ? emote.symbol : '';
image.style.visibility = 'visible';
});
} else {
image.style.width = `0px`;
image.style.height = `0px`;
image.src = '';
image.alt = '';
}
}
}
getEmojiImageAsync(sprite, src => {
image.src = src;
image.alt = emote ? emote.symbol : '';
image.style.visibility = 'visible';
});
} else {
image.style.width = `0px`;
image.style.height = `0px`;
image.src = '';
image.alt = '';
}
}
}
}
@@ -2,46 +2,46 @@ import { Component, Input, Output, EventEmitter } from '@angular/core';
import { faLock } from '../../../client/icons';
@Component({
selector: 'fill-outline',
templateUrl: 'fill-outline.pug',
styleUrls: ['fill-outline.scss'],
selector: 'fill-outline',
templateUrl: 'fill-outline.pug',
styleUrls: ['fill-outline.scss'],
})
export class FillOutline {
readonly lockIcon = faLock;
@Input() label = 'Color';
@Input() indicatorColor = '';
@Input() base?: string;
@Input() fill?: string;
@Output() fillChange = new EventEmitter<string>();
@Input() outline?: string;
@Output() outlineChange = new EventEmitter<string>();
@Input() locked?: boolean;
@Output() lockedChange = new EventEmitter<boolean>();
@Input() nonLockable = false;
@Input() outlineLocked = false;
@Output() outlineLockedChange = new EventEmitter<boolean>();
@Input() outlineHidden = false;
@Output() change = new EventEmitter<void>();
get hasLock() {
return this.locked !== undefined;
}
onChange() {
this.change.emit();
}
onFillChange(value: string) {
this.fillChange.emit(value);
this.onChange();
}
onOutlineChange(value: string) {
this.outlineChange.emit(value);
this.onChange();
}
onLockedChange(value: boolean) {
this.lockedChange.emit(value);
this.onChange();
}
onOutlineLockedChange(value: boolean) {
this.outlineLockedChange.emit(value);
this.onChange();
}
readonly lockIcon = faLock;
@Input() label = 'Color';
@Input() indicatorColor = '';
@Input() base?: string;
@Input() fill?: string;
@Output() fillChange = new EventEmitter<string>();
@Input() outline?: string;
@Output() outlineChange = new EventEmitter<string>();
@Input() locked?: boolean;
@Output() lockedChange = new EventEmitter<boolean>();
@Input() nonLockable = false;
@Input() outlineLocked = false;
@Output() outlineLockedChange = new EventEmitter<boolean>();
@Input() outlineHidden = false;
@Output() change = new EventEmitter<void>();
get hasLock() {
return this.locked !== undefined;
}
onChange() {
this.change.emit();
}
onFillChange(value: string) {
this.fillChange.emit(value);
this.onChange();
}
onOutlineChange(value: string) {
this.outlineChange.emit(value);
this.onChange();
}
onLockedChange(value: boolean) {
this.lockedChange.emit(value);
this.onChange();
}
onOutlineLockedChange(value: boolean) {
this.outlineLockedChange.emit(value);
this.onChange();
}
}
@@ -7,55 +7,55 @@ import { removeItem } from '../../../common/utils';
import { SettingsService } from '../../services/settingsService';
@Component({
selector: 'friends-box',
templateUrl: 'friends-box.pug',
styleUrls: ['friends-box.scss'],
selector: 'friends-box',
templateUrl: 'friends-box.pug',
styleUrls: ['friends-box.scss'],
})
export class FriendsBox {
readonly friendsIcon = faUserFriends;
readonly cogIcon = faCog;
readonly addToPartyIcon = faUserPlus;
readonly userOptionsIcon = faUserCog;
readonly statusIcon = faCircle;
@Output() sendMessage = new EventEmitter<Friend>();
removing?: Friend;
constructor(private settings: SettingsService, private model: Model, private game: PonyTownGame) {
}
get friends() {
return this.model.friends;
}
get hidden() {
return !!this.settings.account.hidden;
}
toggleHidden() {
this.settings.account.hidden = !this.settings.account.hidden;
this.settings.saveAccountSettings(this.settings.account);
}
toggle() {
this.removing = undefined;
}
sendMessageTo(friend: Friend) {
this.sendMessage.emit(friend);
}
inviteToParty(friend: Friend) {
this.game.send(server => server.playerAction(friend.entityId, PlayerAction.InviteToParty, undefined));
}
remove(friend: Friend) {
this.removing = friend;
}
cancelRemove() {
this.removing = undefined;
}
confirmRemove() {
if (this.removing && this.model.friends) {
const { accountId } = this.removing;
this.game.send(server => server.actionParam(Action.RemoveFriend, accountId));
removeItem(this.model.friends, this.removing);
this.removing = undefined;
}
}
setStatus(status: string) {
this.settings.account.hidden = status === 'invisible';
this.settings.saveAccountSettings(this.settings.account);
}
readonly friendsIcon = faUserFriends;
readonly cogIcon = faCog;
readonly addToPartyIcon = faUserPlus;
readonly userOptionsIcon = faUserCog;
readonly statusIcon = faCircle;
@Output() sendMessage = new EventEmitter<Friend>();
removing?: Friend;
constructor(private settings: SettingsService, private model: Model, private game: PonyTownGame) {
}
get friends() {
return this.model.friends;
}
get hidden() {
return !!this.settings.account.hidden;
}
toggleHidden() {
this.settings.account.hidden = !this.settings.account.hidden;
this.settings.saveAccountSettings(this.settings.account);
}
toggle() {
this.removing = undefined;
}
sendMessageTo(friend: Friend) {
this.sendMessage.emit(friend);
}
inviteToParty(friend: Friend) {
this.game.send(server => server.playerAction(friend.entityId, PlayerAction.InviteToParty, undefined));
}
remove(friend: Friend) {
this.removing = friend;
}
cancelRemove() {
this.removing = undefined;
}
confirmRemove() {
if (this.removing && this.model.friends) {
const { accountId } = this.removing;
this.game.send(server => server.actionParam(Action.RemoveFriend, accountId));
removeItem(this.model.friends, this.removing);
this.removing = undefined;
}
}
setStatus(status: string) {
this.settings.account.hidden = status === 'invisible';
this.settings.saveAccountSettings(this.settings.account);
}
}
@@ -4,24 +4,24 @@ import { InstallService } from '../../services/installService';
import { isMobile } from '../../../client/data';
@Component({
selector: 'install-button',
templateUrl: 'install-button.pug',
styleUrls: ['install-button.scss'],
selector: 'install-button',
templateUrl: 'install-button.pug',
styleUrls: ['install-button.scss'],
})
export class InstallButton {
readonly closeIcon = faTimes;
constructor(private installService: InstallService) {
}
get canInstall() {
return this.installService.canInstall;
}
get isMobile() {
return isMobile;
}
install() {
this.installService.install();
}
dismiss() {
this.installService.dismiss();
}
readonly closeIcon = faTimes;
constructor(private installService: InstallService) {
}
get canInstall() {
return this.installService.canInstall;
}
get isMobile() {
return isMobile;
}
install() {
this.installService.install();
}
dismiss() {
this.installService.dismiss();
}
}
@@ -7,26 +7,26 @@ import { removeItem } from '../../../common/utils';
import { Model } from '../../services/model';
@Component({
selector: 'invites-modal',
templateUrl: 'invites-modal.pug',
selector: 'invites-modal',
templateUrl: 'invites-modal.pug',
})
export class InvitesModal implements OnInit {
@Output() close = new EventEmitter();
invites: (SupporterInvite & { pony: PalettePonyInfo; })[] = [];
error?: string;
constructor(private model: Model, private game: PonyTownGame) {
}
get inviteLimit() {
return this.model.supporterInviteLimit;
}
ngOnInit() {
this.game.send(server => server.getInvites())!
.then(invites => invites.map(i => ({ ...i, pony: toPalette(decompressPonyString(i.info)) })))
.then(invites => this.invites = invites);
}
remove(invite: SupporterInvite) {
this.error = undefined;
this.game.send(server => server.actionParam(Action.CancelSupporterInvite, invite.id));
removeItem(this.invites, invite);
}
@Output() close = new EventEmitter();
invites: (SupporterInvite & { pony: PalettePonyInfo; })[] = [];
error?: string;
constructor(private model: Model, private game: PonyTownGame) {
}
get inviteLimit() {
return this.model.supporterInviteLimit;
}
ngOnInit() {
this.game.send(server => server.getInvites())!
.then(invites => invites.map(i => ({ ...i, pony: toPalette(decompressPonyString(i.info)) })))
.then(invites => this.invites = invites);
}
remove(invite: SupporterInvite) {
this.error = undefined;
this.game.send(server => server.actionParam(Action.CancelSupporterInvite, invite.id));
removeItem(this.invites, invite);
}
}
+3 -3
View File
@@ -1,9 +1,9 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'kbd-key',
templateUrl: 'kbd-key.pug',
selector: 'kbd-key',
templateUrl: 'kbd-key.pug',
})
export class KbdKey {
@Input() title?: string;
@Input() title?: string;
}
+47 -47
View File
@@ -9,53 +9,53 @@ import { SettingsService } from '../../services/settingsService';
import { REQUEST_DATE_OF_BIRTH } from '../../../common/constants';
@Component({
selector: 'menu-bar',
templateUrl: 'menu-bar.pug',
styleUrls: ['menu-bar.scss'],
selector: 'menu-bar',
templateUrl: 'menu-bar.pug',
styleUrls: ['menu-bar.scss'],
})
export class MenuBar {
readonly signUpProviders = signUpProviders;
readonly signInProviders = signInProviders;
readonly starIcon = faStar;
readonly spinnerIcon = faSpinner;
readonly userIcon = faUser;
readonly alertIcon = faExclamationCircle;
readonly cogIcon = faCog;
readonly statusIcon = faCircle;
@Input() logo = false;
@Input() loading = false;
@Input() loadingError = false;
@Input() account?: AccountData;
@Output() signOut = new EventEmitter();
@Output() signIn = new EventEmitter<OAuthProvider>();
constructor(private model: Model, private settings: SettingsService) {
}
get hasSupporterIcon() {
return isSupporterOrPastSupporter(this.account);
}
get supporterTitle() {
return supporterTitle(this.account);
}
get supporterClass() {
return supporterClass(this.account);
}
get showAccountAlert() {
return this.model.missingBirthdate && REQUEST_DATE_OF_BIRTH;
}
get hidden() {
return !!this.settings.account.hidden;
}
icon(id: string) {
return getProviderIcon(id);
}
signInTo(provider: OAuthProvider) {
this.signIn.emit(provider);
}
@HostListener('window:resize')
resize() {
}
setStatus(status: string) {
this.settings.account.hidden = status === 'invisible';
this.settings.saveAccountSettings(this.settings.account);
}
readonly signUpProviders = signUpProviders;
readonly signInProviders = signInProviders;
readonly starIcon = faStar;
readonly spinnerIcon = faSpinner;
readonly userIcon = faUser;
readonly alertIcon = faExclamationCircle;
readonly cogIcon = faCog;
readonly statusIcon = faCircle;
@Input() logo = false;
@Input() loading = false;
@Input() loadingError = false;
@Input() account?: AccountData;
@Output() signOut = new EventEmitter();
@Output() signIn = new EventEmitter<OAuthProvider>();
constructor(private model: Model, private settings: SettingsService) {
}
get hasSupporterIcon() {
return isSupporterOrPastSupporter(this.account);
}
get supporterTitle() {
return supporterTitle(this.account);
}
get supporterClass() {
return supporterClass(this.account);
}
get showAccountAlert() {
return this.model.missingBirthdate && REQUEST_DATE_OF_BIRTH;
}
get hidden() {
return !!this.settings.account.hidden;
}
icon(id: string) {
return getProviderIcon(id);
}
signInTo(provider: OAuthProvider) {
this.signIn.emit(provider);
}
@HostListener('window:resize')
resize() {
}
setStatus(status: string) {
this.settings.account.hidden = status === 'invisible';
this.settings.saveAccountSettings(this.settings.account);
}
}
@@ -2,12 +2,12 @@ import { Component, Input } from '@angular/core';
import { emptyIcon } from '../../../client/icons';
@Component({
selector: 'menu-item',
templateUrl: 'menu-item.pug',
styleUrls: ['menu-item.scss'],
selector: 'menu-item',
templateUrl: 'menu-item.pug',
styleUrls: ['menu-item.scss'],
})
export class MenuItem {
@Input() route: any;
@Input() name?: string;
@Input() icon = emptyIcon;
@Input() route: any;
@Input() name?: string;
@Input() icon = emptyIcon;
}
+83 -83
View File
@@ -9,89 +9,89 @@ const ageLabels = ['', 'M', 'A', '', '', '[M]', '[A]'];
const ageTitles = ['Not set', 'Minor', 'Adult', '', '', 'Minor (locked)', 'Adult (locked)'];
@Component({
selector: 'mod-box',
templateUrl: 'mod-box.pug',
styleUrls: ['mod-box.scss'],
selector: 'mod-box',
templateUrl: 'mod-box.pug',
styleUrls: ['mod-box.scss'],
})
export class ModBox implements OnDestroy {
readonly flagIcon = faFlag;
readonly noteIcon = faStickyNote;
readonly muteIcon = faMicrophoneSlash;
readonly hideIcon = faEyeSlash;
readonly moreIcon = faUserCog;
readonly dangerIcon = faExclamationCircle;
readonly timeouts = TIMEOUTS;
@Input() pony!: Pony;
isNoteOpen = false;
constructor(private model: Model, private game: PonyTownGame) {
}
get ageLabel() {
return ageLabels[this.modInfo && this.modInfo.age || 0];
}
get ageTitle() {
return ageTitles[this.modInfo && this.modInfo.age || 0];
}
get modInfo() {
return this.pony.modInfo;
}
get account() {
return this.modInfo && this.modInfo.account;
}
get country() {
return this.modInfo && this.modInfo.country;
}
get mute() {
return this.modInfo && this.modInfo.mute;
}
get muteTooltip() {
return this.mute ? (this.mute === 'perma' ? 'Permanently Muted' : `Muted for ${this.mute}`) : 'Mute';
}
get shadow() {
return this.modInfo && this.modInfo.shadow;
}
get shadowTooltip() {
return this.shadow ? (this.shadow === 'perma' ? 'Permanently Shadowed' : `Shadowed for ${this.shadow}`) : 'Shadow';
}
get counters() {
return this.modInfo && this.modInfo.counters;
}
get hasCounters() {
const counters = this.counters;
return counters && (counters.spam || counters.swears || counters.timeouts);
}
get check() {
return this.model.modCheck;
}
get note() {
return this.modInfo && this.modInfo.note;
}
set note(value) {
if (this.modInfo) {
this.modInfo.note = value;
}
}
ngOnDestroy() {
if (this.isNoteOpen) {
this.blur();
}
}
className(value: string) {
return value ? (value === 'perma' ? 'btn-danger' : 'btn-warning') : 'btn-default';
}
report() {
this.modAction(ModAction.Report);
}
setMute(value: number) {
this.modAction(ModAction.Mute, value);
}
setShadow(value: number) {
this.modAction(ModAction.Shadow, value);
}
blur() {
this.game.send(server => server.setNote(this.pony.id, this.modInfo && this.modInfo.note || ''));
this.isNoteOpen = false;
}
modAction(type: ModAction, param = 0) {
return this.game.send(server => server.otherAction(this.pony.id, type, param));
}
readonly flagIcon = faFlag;
readonly noteIcon = faStickyNote;
readonly muteIcon = faMicrophoneSlash;
readonly hideIcon = faEyeSlash;
readonly moreIcon = faUserCog;
readonly dangerIcon = faExclamationCircle;
readonly timeouts = TIMEOUTS;
@Input() pony!: Pony;
isNoteOpen = false;
constructor(private model: Model, private game: PonyTownGame) {
}
get ageLabel() {
return ageLabels[this.modInfo && this.modInfo.age || 0];
}
get ageTitle() {
return ageTitles[this.modInfo && this.modInfo.age || 0];
}
get modInfo() {
return this.pony.modInfo;
}
get account() {
return this.modInfo && this.modInfo.account;
}
get country() {
return this.modInfo && this.modInfo.country;
}
get mute() {
return this.modInfo && this.modInfo.mute;
}
get muteTooltip() {
return this.mute ? (this.mute === 'perma' ? 'Permanently Muted' : `Muted for ${this.mute}`) : 'Mute';
}
get shadow() {
return this.modInfo && this.modInfo.shadow;
}
get shadowTooltip() {
return this.shadow ? (this.shadow === 'perma' ? 'Permanently Shadowed' : `Shadowed for ${this.shadow}`) : 'Shadow';
}
get counters() {
return this.modInfo && this.modInfo.counters;
}
get hasCounters() {
const counters = this.counters;
return counters && (counters.spam || counters.swears || counters.timeouts);
}
get check() {
return this.model.modCheck;
}
get note() {
return this.modInfo && this.modInfo.note;
}
set note(value) {
if (this.modInfo) {
this.modInfo.note = value;
}
}
ngOnDestroy() {
if (this.isNoteOpen) {
this.blur();
}
}
className(value: string) {
return value ? (value === 'perma' ? 'btn-danger' : 'btn-warning') : 'btn-default';
}
report() {
this.modAction(ModAction.Report);
}
setMute(value: number) {
this.modAction(ModAction.Mute, value);
}
setShadow(value: number) {
this.modAction(ModAction.Shadow, value);
}
blur() {
this.game.send(server => server.setNote(this.pony.id, this.modInfo && this.modInfo.note || ''));
this.isNoteOpen = false;
}
modAction(type: ModAction, param = 0) {
return this.game.send(server => server.otherAction(this.pony.id, type, param));
}
}
@@ -6,63 +6,63 @@ import { faBan } from '../../../client/icons';
import { getPaletteInfo } from '../../../common/pony';
@Component({
selector: 'notification-item',
templateUrl: 'notification-item.pug',
styleUrls: ['notification-item.scss'],
selector: 'notification-item',
templateUrl: 'notification-item.pug',
styleUrls: ['notification-item.scss'],
})
export class NotificationItem implements OnDestroy {
readonly banIcon = faBan;
@Input() notification!: Notification;
constructor(private game: PonyTownGame) {
}
get isOpen() {
return this.notification.open;
}
set isOpen(value: boolean) {
if (value) {
this.game.notifications.forEach(n => n.open = false);
}
readonly banIcon = faBan;
@Input() notification!: Notification;
constructor(private game: PonyTownGame) {
}
get isOpen() {
return this.notification.open;
}
set isOpen(value: boolean) {
if (value) {
this.game.notifications.forEach(n => n.open = false);
}
this.notification.open = value;
}
get okButton() {
return hasFlag(this.notification.flags, NotificationFlags.Ok);
}
get yesButton() {
return hasFlag(this.notification.flags, NotificationFlags.Yes);
}
get acceptButton() {
return hasFlag(this.notification.flags, NotificationFlags.Accept);
}
get noButton() {
return hasFlag(this.notification.flags, NotificationFlags.No);
}
get rejectButton() {
return hasFlag(this.notification.flags, NotificationFlags.Reject);
}
get ignoreButton() {
return hasFlag(this.notification.flags, NotificationFlags.Ignore);
}
get paletteInfo() {
return getPaletteInfo(this.notification.pony);
}
ngOnDestroy() {
this.isOpen = false;
}
accept() {
this.game.send(server => server.acceptNotification(this.notification.id));
}
reject() {
this.game.send(server => server.rejectNotification(this.notification.id));
}
ignore() {
this.reject();
this.notification.open = value;
}
get okButton() {
return hasFlag(this.notification.flags, NotificationFlags.Ok);
}
get yesButton() {
return hasFlag(this.notification.flags, NotificationFlags.Yes);
}
get acceptButton() {
return hasFlag(this.notification.flags, NotificationFlags.Accept);
}
get noButton() {
return hasFlag(this.notification.flags, NotificationFlags.No);
}
get rejectButton() {
return hasFlag(this.notification.flags, NotificationFlags.Reject);
}
get ignoreButton() {
return hasFlag(this.notification.flags, NotificationFlags.Ignore);
}
get paletteInfo() {
return getPaletteInfo(this.notification.pony);
}
ngOnDestroy() {
this.isOpen = false;
}
accept() {
this.game.send(server => server.acceptNotification(this.notification.id));
}
reject() {
this.game.send(server => server.rejectNotification(this.notification.id));
}
ignore() {
this.reject();
const pony = this.notification.pony;
const pony = this.notification.pony;
if (pony !== this.game.player) {
this.game.send(server => server.playerAction(pony.id, PlayerAction.Ignore, undefined));
pony.playerState = setFlag(pony.playerState, EntityPlayerState.Ignored, true);
}
}
if (pony !== this.game.player) {
this.game.send(server => server.playerAction(pony.id, PlayerAction.Ignore, undefined));
pony.playerState = setFlag(pony.playerState, EntityPlayerState.Ignored, true);
}
}
}
@@ -5,29 +5,29 @@ import { faEllipsisV } from '../../../client/icons';
const LIMIT = 8;
@Component({
selector: 'notification-list',
templateUrl: 'notification-list.pug',
styleUrls: ['notification-list.scss'],
selector: 'notification-list',
templateUrl: 'notification-list.pug',
styleUrls: ['notification-list.scss'],
})
export class NotificationList {
readonly ellipsisIcon = faEllipsisV;
@Input() notifications!: Notification[];
@Input() set notificationsLength(value: number) {
while (this.start > value) {
this.prev();
}
}
start = 0;
get limit() {
return this.start + LIMIT;
}
get hasMore() {
return this.notifications.length > (this.start + this.limit);
}
next() {
this.start += this.limit;
}
prev() {
this.start -= this.start <= LIMIT ? LIMIT : LIMIT - 1;
}
readonly ellipsisIcon = faEllipsisV;
@Input() notifications!: Notification[];
@Input() set notificationsLength(value: number) {
while (this.start > value) {
this.prev();
}
}
start = 0;
get limit() {
return this.start + LIMIT;
}
get hasMore() {
return this.notifications.length > (this.start + this.limit);
}
next() {
this.start += this.limit;
}
prev() {
this.start -= this.start <= LIMIT ? LIMIT : LIMIT - 1;
}
}
@@ -4,27 +4,27 @@ import { Model } from '../../services/model';
import { hardReload } from '../../../client/clientUtils';
@Component({
selector: 'page-loader',
templateUrl: 'page-loader.pug',
styleUrls: ['page-loader.scss'],
selector: 'page-loader',
templateUrl: 'page-loader.pug',
styleUrls: ['page-loader.scss'],
})
export class PageLoader {
readonly spinnerIcon = faSpinner;
constructor(private model: Model) {
}
get loading() {
return this.model.loading;
}
get updating() {
return this.model.updating;
}
get updatingTakesLongTime() {
return this.model.updatingTakesLongTime;
}
get loadingError() {
return this.model.loadingError;
}
reload() {
hardReload();
}
readonly spinnerIcon = faSpinner;
constructor(private model: Model) {
}
get loading() {
return this.model.loading;
}
get updating() {
return this.model.updating;
}
get updatingTakesLongTime() {
return this.model.updatingTakesLongTime;
}
get loadingError() {
return this.model.loadingError;
}
reload() {
hardReload();
}
}
+14 -14
View File
@@ -5,20 +5,20 @@ import { partyLeaderIcon, offlineIcon } from '../../../client/icons';
import { getPaletteInfo } from '../../../common/pony';
@Component({
selector: 'party-box',
templateUrl: 'party-box.pug',
styleUrls: ['party-box.scss'],
selector: 'party-box',
templateUrl: 'party-box.pug',
styleUrls: ['party-box.scss'],
})
export class PartyBox {
readonly leaderIcon = partyLeaderIcon;
readonly offlineIcon = offlineIcon;
@Input() member!: PartyMember;
constructor(private game: PonyTownGame) {
}
get paletteInfo() {
return this.member.pony && getPaletteInfo(this.member.pony);
}
click() {
this.game.select(this.member.pony);
}
readonly leaderIcon = partyLeaderIcon;
readonly offlineIcon = offlineIcon;
@Input() member!: PartyMember;
constructor(private game: PonyTownGame) {
}
get paletteInfo() {
return this.member.pony && getPaletteInfo(this.member.pony);
}
click() {
this.game.select(this.member.pony);
}
}
@@ -8,86 +8,86 @@ import { clamp } from '../../../common/utils';
import { isPartyLeader } from '../../../client/partyUtils';
function visibleMembers(members: PartyMember[], max: number, start: number) {
return members.length > max ? Math.max(max - (start > 0 ? 2 : 1), 1) : max;
return members.length > max ? Math.max(max - (start > 0 ? 2 : 1), 1) : max;
}
@Component({
selector: 'party-list',
templateUrl: 'party-list.pug',
styleUrls: ['party-list.scss'],
selector: 'party-list',
templateUrl: 'party-list.pug',
styleUrls: ['party-list.scss'],
})
export class PartyList implements OnInit, OnDestroy {
readonly ellipsisIcon = faEllipsisV;
readonly leaderIcon = partyLeaderIcon;
readonly cogIcon = faCog;
hidden = false;
start = 0;
maxMembers = PARTY_LIMIT - 1;
members: PartyMember[] = [];
private subscription?: Subscription;
constructor(private game: PonyTownGame) {
}
get hasParty() {
return this.game.party !== undefined;
}
get isLeader() {
return isPartyLeader(this.game);
}
get hasMore() {
return this.members.length > (this.start + this.visible);
}
get visible() {
return visibleMembers(this.members, this.maxMembers, this.start);
}
get limit() {
return this.start + this.visible;
}
ngOnInit() {
this.subscription = this.game.onPartyUpdate.subscribe(() => this.update());
this.resized();
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
isMe(member: PartyMember) {
return this.game.player && this.game.player.id === member.id;
}
leave() {
this.game.send(server => server.leaveParty());
}
update() {
if (this.hasParty) {
this.members = this.game.party ? this.game.party.members.filter(m => !m.self) : [];
readonly ellipsisIcon = faEllipsisV;
readonly leaderIcon = partyLeaderIcon;
readonly cogIcon = faCog;
hidden = false;
start = 0;
maxMembers = PARTY_LIMIT - 1;
members: PartyMember[] = [];
private subscription?: Subscription;
constructor(private game: PonyTownGame) {
}
get hasParty() {
return this.game.party !== undefined;
}
get isLeader() {
return isPartyLeader(this.game);
}
get hasMore() {
return this.members.length > (this.start + this.visible);
}
get visible() {
return visibleMembers(this.members, this.maxMembers, this.start);
}
get limit() {
return this.start + this.visible;
}
ngOnInit() {
this.subscription = this.game.onPartyUpdate.subscribe(() => this.update());
this.resized();
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
isMe(member: PartyMember) {
return this.game.player && this.game.player.id === member.id;
}
leave() {
this.game.send(server => server.leaveParty());
}
update() {
if (this.hasParty) {
this.members = this.game.party ? this.game.party.members.filter(m => !m.self) : [];
while (this.start > 0 && this.members.length <= this.start) {
this.start = 0;
}
} else {
this.members = [];
this.start = 0;
}
}
@HostListener('window:resize')
resized() {
const padding = 140 + 110;
const max = clamp(Math.floor((window.innerHeight - padding) / 43), 0, PARTY_LIMIT - 1);
while (this.start > 0 && this.members.length <= this.start) {
this.start = 0;
}
} else {
this.members = [];
this.start = 0;
}
}
@HostListener('window:resize')
resized() {
const padding = 140 + 110;
const max = clamp(Math.floor((window.innerHeight - padding) / 43), 0, PARTY_LIMIT - 1);
if (this.maxMembers !== max) {
this.start = 0;
this.maxMembers = max;
}
}
next() {
this.start += this.visible;
}
prev() {
const max = this.members.length - 1;
let start = 0;
if (this.maxMembers !== max) {
this.start = 0;
this.maxMembers = max;
}
}
next() {
this.start += this.visible;
}
prev() {
const max = this.members.length - 1;
let start = 0;
while (start < max && (start + visibleMembers(this.members, this.maxMembers, start)) !== this.start) {
start++;
}
while (start < max && (start + visibleMembers(this.members, this.maxMembers, start)) !== this.start) {
start++;
}
this.start = start;
}
this.start = start;
}
}
+5 -5
View File
@@ -1,11 +1,11 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'siteName',
name: 'siteName',
})
export class SiteNamePipe implements PipeTransform {
transform(value: string | undefined) {
const match = String(value || '').match(/(\w+)\.com/);
return match && match[1];
}
transform(value: string | undefined) {
const match = String(value || '').match(/(\w+)\.com/);
return match && match[1];
}
}
+157 -157
View File
@@ -2,8 +2,8 @@ import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { ServerInfo, AccountDataFlags } from '../../../common/interfaces';
import { RequestError, delay, includes, hasFlag } from '../../../common/utils';
import {
WEBGL_CREATION_ERROR, ACCESS_ERROR, ACCOUNT_ERROR, BROWSER_NOT_SUPPORTED_ERROR, NAME_ERROR, VERSION_ERROR,
OFFLINE_ERROR, PROTECTION_ERROR, NOT_AUTHENTICATED_ERROR, CHARACTER_LIMIT_ERROR
WEBGL_CREATION_ERROR, ACCESS_ERROR, ACCOUNT_ERROR, BROWSER_NOT_SUPPORTED_ERROR, NAME_ERROR, VERSION_ERROR,
OFFLINE_ERROR, PROTECTION_ERROR, NOT_AUTHENTICATED_ERROR, CHARACTER_LIMIT_ERROR
} from '../../../common/errors';
import { version } from '../../../client/data';
import { GameService } from '../../services/gameService';
@@ -16,168 +16,168 @@ import { ErrorReporter } from '../../services/errorReporter';
import { REQUEST_DATE_OF_BIRTH } from '../../../common/constants';
const ignoredErrors = [
WEBGL_CREATION_ERROR,
BROWSER_NOT_SUPPORTED_ERROR,
NAME_ERROR,
OFFLINE_ERROR,
VERSION_ERROR,
ACCESS_ERROR,
PROTECTION_ERROR,
NOT_AUTHENTICATED_ERROR,
CHARACTER_LIMIT_ERROR,
'Saving in progress',
WEBGL_CREATION_ERROR,
BROWSER_NOT_SUPPORTED_ERROR,
NAME_ERROR,
OFFLINE_ERROR,
VERSION_ERROR,
ACCESS_ERROR,
PROTECTION_ERROR,
NOT_AUTHENTICATED_ERROR,
CHARACTER_LIMIT_ERROR,
'Saving in progress',
];
@Component({
selector: 'play-box',
templateUrl: 'play-box.pug',
styleUrls: ['play-box.scss'],
selector: 'play-box',
templateUrl: 'play-box.pug',
styleUrls: ['play-box.scss'],
})
export class PlayBox implements OnInit {
readonly spinnerIcon = faSpinner;
readonly warningIcon = faExclamationCircle;
readonly infoIcon = faInfoCircle;
readonly requestBirthdate = REQUEST_DATE_OF_BIRTH;
@Output() errorChange = new EventEmitter<string | undefined>();
@Input() label?: string;
joining = false;
failedToLoadImages = false;
birthdate = '';
birthdateSet = false;
private locked = false;
constructor(
public gameService: GameService,
public model: Model,
private storage: StorageService,
private errorReporter: ErrorReporter,
) {
}
@Input()
get error() {
return this.gameService.error;
}
set error(value: string | undefined) {
if (this.gameService) {
this.gameService.error = value;
this.errorChange.emit(value);
}
}
get server() {
return this.gameService.server;
}
set server(value: ServerInfo | undefined) {
this.gameService.server = value;
}
get servers() {
return this.gameService.servers;
}
get offline(): boolean {
return this.gameService.offline;
}
get updateWarning() {
return this.gameService.updateWarning;
}
get invalidVersion(): boolean {
return !!(this.gameService.versionError || this.error === VERSION_ERROR
|| (this.gameService.version && this.gameService.version !== version));
}
get protectionError(): boolean {
return this.gameService.protectionError || this.error === PROTECTION_ERROR;
}
get canPlay(): boolean {
return !!this.server && this.gameService.canPlay && !this.locked && !this.invalidVersion && !this.failedToLoadImages;
}
get isAccessError(): boolean {
return this.error === ACCESS_ERROR || this.error === ACCOUNT_ERROR;
}
get isWebGLError(): boolean {
return this.error === WEBGL_CREATION_ERROR;
}
get isBrowserError(): boolean {
return this.error === BROWSER_NOT_SUPPORTED_ERROR;
}
get isOtherError(): boolean {
return !!this.error && !this.invalidVersion && !this.isAccessError && !this.isWebGLError && !this.isBrowserError;
}
get ponyLimit() {
return this.model.characterLimit;
}
get hasTooManyPonies(): boolean {
return this.model.ponies.length > this.ponyLimit;
}
get isMarkedForMultiples(): boolean {
const account = this.model.account;
return !!account && hasFlag(account.flags, AccountDataFlags.Duplicates);
}
get isAndroidBrowser() {
return isAndroidBrowser;
}
get isBrowserOutdated() {
return !isAndroidBrowser && isBrowserOutdated && !this.storage.getBoolean('dismiss-outdated-browser');
}
get leftMessage() {
return this.gameService.leftMessage;
}
get accountAlert() {
return this.model.accountAlert;
}
ngOnInit() {
loadAndInitSpriteSheets()
.then(loaded => this.failedToLoadImages = !loaded);
}
play() {
if (this.canPlay) {
this.joining = true;
this.locked = true;
this.error = undefined;
readonly spinnerIcon = faSpinner;
readonly warningIcon = faExclamationCircle;
readonly infoIcon = faInfoCircle;
readonly requestBirthdate = REQUEST_DATE_OF_BIRTH;
@Output() errorChange = new EventEmitter<string | undefined>();
@Input() label?: string;
joining = false;
failedToLoadImages = false;
birthdate = '';
birthdateSet = false;
private locked = false;
constructor(
public gameService: GameService,
public model: Model,
private storage: StorageService,
private errorReporter: ErrorReporter,
) {
}
@Input()
get error() {
return this.gameService.error;
}
set error(value: string | undefined) {
if (this.gameService) {
this.gameService.error = value;
this.errorChange.emit(value);
}
}
get server() {
return this.gameService.server;
}
set server(value: ServerInfo | undefined) {
this.gameService.server = value;
}
get servers() {
return this.gameService.servers;
}
get offline(): boolean {
return this.gameService.offline;
}
get updateWarning() {
return this.gameService.updateWarning;
}
get invalidVersion(): boolean {
return !!(this.gameService.versionError || this.error === VERSION_ERROR
|| (this.gameService.version && this.gameService.version !== version));
}
get protectionError(): boolean {
return this.gameService.protectionError || this.error === PROTECTION_ERROR;
}
get canPlay(): boolean {
return !!this.server && this.gameService.canPlay && !this.locked && !this.invalidVersion && !this.failedToLoadImages;
}
get isAccessError(): boolean {
return this.error === ACCESS_ERROR || this.error === ACCOUNT_ERROR;
}
get isWebGLError(): boolean {
return this.error === WEBGL_CREATION_ERROR;
}
get isBrowserError(): boolean {
return this.error === BROWSER_NOT_SUPPORTED_ERROR;
}
get isOtherError(): boolean {
return !!this.error && !this.invalidVersion && !this.isAccessError && !this.isWebGLError && !this.isBrowserError;
}
get ponyLimit() {
return this.model.characterLimit;
}
get hasTooManyPonies(): boolean {
return this.model.ponies.length > this.ponyLimit;
}
get isMarkedForMultiples(): boolean {
const account = this.model.account;
return !!account && hasFlag(account.flags, AccountDataFlags.Duplicates);
}
get isAndroidBrowser() {
return isAndroidBrowser;
}
get isBrowserOutdated() {
return !isAndroidBrowser && isBrowserOutdated && !this.storage.getBoolean('dismiss-outdated-browser');
}
get leftMessage() {
return this.gameService.leftMessage;
}
get accountAlert() {
return this.model.accountAlert;
}
ngOnInit() {
loadAndInitSpriteSheets()
.then(loaded => this.failedToLoadImages = !loaded);
}
play() {
if (this.canPlay) {
this.joining = true;
this.locked = true;
this.error = undefined;
const delayTime = (!DEVELOPMENT && this.gameService.wasPlaying) ? 1500 : 10;
const delayTime = (!DEVELOPMENT && this.gameService.wasPlaying) ? 1500 : 10;
delay(delayTime) // delay joing if user reloaded the game instead of leaving cleanly
.then(() => this.model.savePony(this.model.pony))
.then(pony => this.joining ? this.gameService.join(pony.id) : Promise.resolve())
.catch((e: RequestError) => {
if (!/^Cancelled/.test(e.message)) {
this.error = e.message;
delay(delayTime) // delay joing if user reloaded the game instead of leaving cleanly
.then(() => this.model.savePony(this.model.pony))
.then(pony => this.joining ? this.gameService.join(pony.id) : Promise.resolve())
.catch((e: RequestError) => {
if (!/^Cancelled/.test(e.message)) {
this.error = e.message;
if (!includes(ignoredErrors, e.message) && !/shader/.test(e.message)) {
this.errorReporter.reportError(e, { status: e.status, text: e.text });
}
if (!includes(ignoredErrors, e.message) && !/shader/.test(e.message)) {
this.errorReporter.reportError(e, { status: e.status, text: e.text });
}
DEVELOPMENT && console.error(e);
}
})
.finally(() => this.joining = false)
.then(() => delay(1500))
.finally(() => this.locked = false);
}
}
cancel() {
this.gameService.leave('Cancelled joining');
}
reload() {
location.reload(true);
}
hardReload() {
hardReload();
}
hasFlag(server: ServerInfo) {
return server.countryFlags && server.countryFlags.length;
}
getIcon(server: ServerInfo) {
switch (server.flag) {
case 'star': return faStar;
case 'test': return faWrench;
default: return faGlobe;
}
}
dismissOutdatedBrowser() {
this.storage.setBoolean('dismiss-outdated-browser', true);
}
saveBirthdate() {
if (this.birthdate) {
this.model.updateAccount({ birthdate: this.birthdate });
this.birthdateSet = true;
}
}
DEVELOPMENT && console.error(e);
}
})
.finally(() => this.joining = false)
.then(() => delay(1500))
.finally(() => this.locked = false);
}
}
cancel() {
this.gameService.leave('Cancelled joining');
}
reload() {
location.reload(true);
}
hardReload() {
hardReload();
}
hasFlag(server: ServerInfo) {
return server.countryFlags && server.countryFlags.length;
}
getIcon(server: ServerInfo) {
switch (server.flag) {
case 'star': return faStar;
case 'test': return faWrench;
default: return faGlobe;
}
}
dismissOutdatedBrowser() {
this.storage.setBoolean('dismiss-outdated-browser', true);
}
saveBirthdate() {
if (this.birthdate) {
this.model.updateAccount({ birthdate: this.birthdate });
this.birthdateSet = true;
}
}
}
@@ -3,10 +3,10 @@ import { supporterLink } from '../../../client/data';
import { GENERAL_RULES } from '../../../common/constants';
@Component({
selector: 'play-notice',
templateUrl: 'play-notice.pug',
selector: 'play-notice',
templateUrl: 'play-notice.pug',
})
export class PlayNotice {
readonly patreonLink = supporterLink;
readonly rules = GENERAL_RULES;
readonly patreonLink = supporterLink;
readonly rules = GENERAL_RULES;
}
+95 -95
View File
@@ -4,8 +4,8 @@ import { getPaletteInfo } from '../../../common/pony';
import { Model } from '../../services/model';
import { PonyTownGame } from '../../../client/game';
import {
partyLeaderIcon, faUserPlus, faUserTimes, faCheck, faMicrophoneSlash, faEyeSlash, faStar, faUserMinus,
faUserCog, faComment
partyLeaderIcon, faUserPlus, faUserTimes, faCheck, faMicrophoneSlash, faEyeSlash, faStar, faUserMinus,
faUserCog, faComment
} from '../../../client/icons';
import { DAY } from '../../../common/constants';
import { isPonyInParty, isPartyLeader } from '../../../client/partyUtils';
@@ -14,100 +14,100 @@ import { isIgnored, isHidden, isFriend } from '../../../common/entityUtils';
import { setFlag } from '../../../common/utils';
@Component({
selector: 'pony-box',
templateUrl: 'pony-box.pug',
styleUrls: ['pony-box.scss'],
selector: 'pony-box',
templateUrl: 'pony-box.pug',
styleUrls: ['pony-box.scss'],
})
export class PonyBox {
readonly leaderIcon = partyLeaderIcon;
readonly inviteIcon = faUserPlus;
readonly removeIcon = faUserTimes;
readonly cogIcon = faUserCog;
readonly checkIcon = faCheck;
readonly ignoreIcon = faMicrophoneSlash;
readonly hideIcon = faEyeSlash;
readonly starIcon = faStar;
readonly addFriendIcon = faUserPlus;
readonly removeFriendIcon = faUserMinus;
readonly messageIcon = faComment;
isIgnored = isIgnored;
isFriend = isFriend;
removingFriend = false;
@Input() pony?: Pony;
@Output() sendMessage = new EventEmitter<Entity>();
constructor(private model: Model, private game: PonyTownGame) {
}
get ignoredOrHidden() {
return this.pony && (isIgnored(this.pony) || isHidden(this.pony));
}
get isMod() {
return this.model.isMod;
}
get canInviteToParty() {
return this.pony && (!this.game.party || (isPartyLeader(this.game) && !isPonyInParty(this.game.party, this.pony, true)));
}
get canRemoveFromParty() {
return this.pony && isPartyLeader(this.game) && isPonyInParty(this.game.party, this.pony, true);
}
get canPromoteToLeader() {
return this.pony && isPartyLeader(this.game) && isPonyInParty(this.game.party, this.pony, false);
}
get special() {
const tag = getTag(this.pony && this.pony.tag);
return tag && tag.name;
}
get specialClass() {
const tag = getTag(this.pony && this.pony.tag);
return tag && tag.tagClass;
}
get paletteInfo() {
return this.pony && getPaletteInfo(this.pony);
}
inviteToParty() {
this.playerAction(PlayerAction.InviteToParty);
}
removeFromParty() {
this.playerAction(PlayerAction.RemoveFromParty);
}
promoteToLeader() {
this.playerAction(PlayerAction.PromotePartyLeader);
}
toggleIgnore() {
if (this.pony) {
const ignored = isIgnored(this.pony);
this.playerAction(ignored ? PlayerAction.Unignore : PlayerAction.Ignore);
this.pony.playerState = setFlag(this.pony.playerState, EntityPlayerState.Ignored, !ignored);
}
}
hidePlayer(days: number) {
this.playerAction(PlayerAction.HidePlayer, days * DAY);
}
addFriend() {
this.playerAction(PlayerAction.AddFriend);
}
removeFriend() {
this.playerAction(PlayerAction.RemoveFriend);
}
private playerAction(type: PlayerAction, param: any = undefined) {
const ponyId = this.pony && this.pony.id;
readonly leaderIcon = partyLeaderIcon;
readonly inviteIcon = faUserPlus;
readonly removeIcon = faUserTimes;
readonly cogIcon = faUserCog;
readonly checkIcon = faCheck;
readonly ignoreIcon = faMicrophoneSlash;
readonly hideIcon = faEyeSlash;
readonly starIcon = faStar;
readonly addFriendIcon = faUserPlus;
readonly removeFriendIcon = faUserMinus;
readonly messageIcon = faComment;
isIgnored = isIgnored;
isFriend = isFriend;
removingFriend = false;
@Input() pony?: Pony;
@Output() sendMessage = new EventEmitter<Entity>();
constructor(private model: Model, private game: PonyTownGame) {
}
get ignoredOrHidden() {
return this.pony && (isIgnored(this.pony) || isHidden(this.pony));
}
get isMod() {
return this.model.isMod;
}
get canInviteToParty() {
return this.pony && (!this.game.party || (isPartyLeader(this.game) && !isPonyInParty(this.game.party, this.pony, true)));
}
get canRemoveFromParty() {
return this.pony && isPartyLeader(this.game) && isPonyInParty(this.game.party, this.pony, true);
}
get canPromoteToLeader() {
return this.pony && isPartyLeader(this.game) && isPonyInParty(this.game.party, this.pony, false);
}
get special() {
const tag = getTag(this.pony && this.pony.tag);
return tag && tag.name;
}
get specialClass() {
const tag = getTag(this.pony && this.pony.tag);
return tag && tag.tagClass;
}
get paletteInfo() {
return this.pony && getPaletteInfo(this.pony);
}
inviteToParty() {
this.playerAction(PlayerAction.InviteToParty);
}
removeFromParty() {
this.playerAction(PlayerAction.RemoveFromParty);
}
promoteToLeader() {
this.playerAction(PlayerAction.PromotePartyLeader);
}
toggleIgnore() {
if (this.pony) {
const ignored = isIgnored(this.pony);
this.playerAction(ignored ? PlayerAction.Unignore : PlayerAction.Ignore);
this.pony.playerState = setFlag(this.pony.playerState, EntityPlayerState.Ignored, !ignored);
}
}
hidePlayer(days: number) {
this.playerAction(PlayerAction.HidePlayer, days * DAY);
}
addFriend() {
this.playerAction(PlayerAction.AddFriend);
}
removeFriend() {
this.playerAction(PlayerAction.RemoveFriend);
}
private playerAction(type: PlayerAction, param: any = undefined) {
const ponyId = this.pony && this.pony.id;
if (ponyId) {
this.game.send(server => server.playerAction(ponyId, type, param));
}
}
sendMessageTo() {
if (this.pony) {
this.sendMessage.emit(this.pony);
}
}
// supporter servers
get canInviteToSupporterServers() {
return false; // DEVELOPMENT; // TODO: check if ignored or hidden
}
get isInvitedToSupporterServers() {
return false;
}
inviteToSupporterServers() {
this.playerAction(PlayerAction.InviteToSupporterServers);
}
if (ponyId) {
this.game.send(server => server.playerAction(ponyId, type, param));
}
}
sendMessageTo() {
if (this.pony) {
this.sendMessage.emit(this.pony);
}
}
// supporter servers
get canInviteToSupporterServers() {
return false; // DEVELOPMENT; // TODO: check if ignored or hidden
}
get isInvitedToSupporterServers() {
return false;
}
inviteToSupporterServers() {
this.playerAction(PlayerAction.InviteToSupporterServers);
}
}
@@ -1,5 +1,5 @@
import {
Component, Input, AfterViewInit, OnChanges, ElementRef, ChangeDetectionStrategy, ViewChild, NgZone
Component, Input, AfterViewInit, OnChanges, ElementRef, ChangeDetectionStrategy, ViewChild, NgZone
} from '@angular/core';
import { PalettePonyInfo } from '../../../common/interfaces';
import { ContextSpriteBatch } from '../../../graphics/contextSpriteBatch';
@@ -10,15 +10,15 @@ import { drawPony } from '../../../client/ponyDraw';
import { paletteSpriteSheet } from '../../../generated/sprites';
const scales: { [key: string]: number } = {
large: 3,
medium: 2,
small: 1,
large: 3,
medium: 2,
small: 1,
};
const sizes: { [key: string]: number } = {
large: 100,
medium: 66,
small: 33,
large: 100,
medium: 66,
small: 33,
};
const BUFFER_SIZE = 34;
@@ -26,61 +26,61 @@ const options = defaultDrawPonyOptions();
const state = defaultPonyState();
@Component({
selector: 'portrait-box',
templateUrl: 'portrait-box.pug',
styleUrls: ['portrait-box.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'portrait-box',
templateUrl: 'portrait-box.pug',
styleUrls: ['portrait-box.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PortraitBox implements AfterViewInit, OnChanges {
@Input() noBorder = false;
@Input() flip = false;
@Input() size = 'large';
@Input() pony: PalettePonyInfo | undefined = undefined;
@ViewChild('canvas', { static: true }) canvas!: ElementRef;
private frame = 0;
private batch?: ContextSpriteBatch;
constructor(private zone: NgZone) {
}
ngAfterViewInit() {
loadAndInitSpriteSheets()
.then(() => this.redraw());
}
ngOnChanges() {
this.redraw();
}
private redraw() {
this.frame = this.frame || this.zone.runOutsideAngular(() => requestAnimationFrame(() => {
this.frame = 0;
this.draw();
}));
}
private draw() {
const canvas = this.canvas.nativeElement as HTMLCanvasElement;
const size = sizes[this.size];
resizeCanvasWithRatio(canvas, size, size);
@Input() noBorder = false;
@Input() flip = false;
@Input() size = 'large';
@Input() pony: PalettePonyInfo | undefined = undefined;
@ViewChild('canvas', { static: true }) canvas!: ElementRef;
private frame = 0;
private batch?: ContextSpriteBatch;
constructor(private zone: NgZone) {
}
ngAfterViewInit() {
loadAndInitSpriteSheets()
.then(() => this.redraw());
}
ngOnChanges() {
this.redraw();
}
private redraw() {
this.frame = this.frame || this.zone.runOutsideAngular(() => requestAnimationFrame(() => {
this.frame = 0;
this.draw();
}));
}
private draw() {
const canvas = this.canvas.nativeElement as HTMLCanvasElement;
const size = sizes[this.size];
resizeCanvasWithRatio(canvas, size, size);
const context = canvas.getContext('2d');
const context = canvas.getContext('2d');
if (context) {
context.save();
context.fillStyle = '#444';
context.fillRect(0, 0, canvas.width, canvas.height);
if (context) {
context.save();
context.fillStyle = '#444';
context.fillRect(0, 0, canvas.width, canvas.height);
if (this.pony) {
const scale = scales[this.size] * getPixelRatio();
this.batch = this.batch || new ContextSpriteBatch(createCanvas(BUFFER_SIZE, BUFFER_SIZE));
options.flipped = !this.flip;
if (this.pony) {
const scale = scales[this.size] * getPixelRatio();
this.batch = this.batch || new ContextSpriteBatch(createCanvas(BUFFER_SIZE, BUFFER_SIZE));
options.flipped = !this.flip;
this.batch.start(paletteSpriteSheet, 0);
drawPony(this.batch, this.pony, state, 25, 54, options);
this.batch.end();
this.batch.start(paletteSpriteSheet, 0);
drawPony(this.batch, this.pony, state, 25, 54, options);
this.batch.end();
disableImageSmoothing(context);
context.scale(this.flip ? scale : -scale, scale);
context.drawImage(this.batch.canvas, this.flip ? 0 : -BUFFER_SIZE, 0);
}
disableImageSmoothing(context);
context.scale(this.flip ? scale : -scale, scale);
context.drawImage(this.batch.canvas, this.flip ? 0 : -BUFFER_SIZE, 0);
}
context.restore();
}
}
context.restore();
}
}
}
@@ -2,20 +2,20 @@ import { Component, Input, Output, EventEmitter } from '@angular/core';
import { times } from 'lodash';
@Component({
selector: 'scale-picker',
templateUrl: 'scale-picker.pug',
selector: 'scale-picker',
templateUrl: 'scale-picker.pug',
})
export class ScalePicker {
@Input() scale = 1;
@Output() scaleChange = new EventEmitter<number>();
scales = [1, 2, 3, 4];
@Input() set maxScale(value: number) {
this.scales = times(value, i => i + 1);
}
setScale(value: number) {
if (value !== this.scale) {
this.scale = value;
this.scaleChange.emit(value);
}
}
@Input() scale = 1;
@Output() scaleChange = new EventEmitter<number>();
scales = [1, 2, 3, 4];
@Input() set maxScale(value: number) {
this.scales = times(value, i => i + 1);
}
setScale(value: number) {
if (value !== this.scale) {
this.scale = value;
this.scaleChange.emit(value);
}
}
}
@@ -7,67 +7,67 @@ const FILLS = ['Orange', 'DodgerBlue', 'LimeGreen', 'Orchid', 'crimson', 'Aquama
const OUTLINES = ['Chocolate', 'SteelBlue', 'ForestGreen', 'DarkOrchid', 'darkred', 'DarkTurquoise'];
@Directive({
selector: '[setOutlineHidden]',
selector: '[setOutlineHidden]',
})
export class SetOutlineHidden {
@Input() setOutlineHidden = false;
@Input() setOutlineHidden = false;
}
@Component({
selector: 'set-selection',
templateUrl: 'set-selection.pug',
styleUrls: ['set-selection.scss'],
selector: 'set-selection',
templateUrl: 'set-selection.pug',
styleUrls: ['set-selection.scss'],
})
export class SetSelection implements OnChanges {
readonly exampleFills = FILLS;
readonly exampleOutlines = OUTLINES;
@Input() label?: string;
@Input() base?: string;
@Input() set?: SpriteSet<string>;
@Input() sets?: ColorExtraSets;
@Input() sprites?: ColorExtraSet;
@Input() circle?: string;
@Input() outlineHidden = false;
@Input() nonLockable = false;
@Input() compact = false;
@Input() onlyPatterns = false;
@Input() darken = true;
@Output() change = new EventEmitter<void>();
constructor(@Optional() private hidden: SetOutlineHidden) {
}
get isOutlineHidden() {
return this.hidden ? this.hidden.setOutlineHidden : this.outlineHidden;
}
get patternColors() {
const set = this.getSet();
const pat = this.set && set && set[this.set.pattern || 0];
readonly exampleFills = FILLS;
readonly exampleOutlines = OUTLINES;
@Input() label?: string;
@Input() base?: string;
@Input() set?: SpriteSet<string>;
@Input() sets?: ColorExtraSets;
@Input() sprites?: ColorExtraSet;
@Input() circle?: string;
@Input() outlineHidden = false;
@Input() nonLockable = false;
@Input() compact = false;
@Input() onlyPatterns = false;
@Input() darken = true;
@Output() change = new EventEmitter<void>();
constructor(@Optional() private hidden: SetOutlineHidden) {
}
get isOutlineHidden() {
return this.hidden ? this.hidden.setOutlineHidden : this.outlineHidden;
}
get patternColors() {
const set = this.getSet();
const pat = this.set && set && set[this.set.pattern || 0];
if (pat && !pat.colors) {
return 0;
} else if (pat) {
return getColorCount(pat);
} else {
return this.nonLockable ? 1 : 0;
}
}
get showColorPatterns(): boolean {
const type = this.set && this.set.type || 0;
const set = this.sets && this.sets[type];
return !!set && set.length > 1;
}
ngOnChanges() {
this.sprites = this.sets ? this.sets.map(s => s ? s[0] : undefined) : undefined;
}
onChange() {
const set = this.getSet();
if (pat && !pat.colors) {
return 0;
} else if (pat) {
return getColorCount(pat);
} else {
return this.nonLockable ? 1 : 0;
}
}
get showColorPatterns(): boolean {
const type = this.set && this.set.type || 0;
const set = this.sets && this.sets[type];
return !!set && set.length > 1;
}
ngOnChanges() {
this.sprites = this.sets ? this.sets.map(s => s ? s[0] : undefined) : undefined;
}
onChange() {
const set = this.getSet();
if (this.set && set) {
this.set.pattern = clamp(this.set.pattern || 0, 0, set.length - 1);
}
if (this.set && set) {
this.set.pattern = clamp(this.set.pattern || 0, 0, set.length - 1);
}
this.change.emit();
}
private getSet(): ColorExtraSet | undefined {
return this.set && this.sets && this.sets[this.set.type || 0];
}
this.change.emit();
}
private getSet(): ColorExtraSet | undefined {
return this.set && this.sets && this.sets[this.set.type || 0];
}
}
@@ -8,124 +8,124 @@ import { Model } from '../../services/model';
import { PonyTownGame } from '../../../client/game';
import { Dropdown } from '../directives/dropdown';
import {
emptyIcon, faCog, faSearch, faSignOutAlt, faStepForward, faVolumeOff, faVolumeUp, faVolumeDown, faPlus, faMinus
emptyIcon, faCog, faSearch, faSignOutAlt, faStepForward, faVolumeOff, faVolumeUp, faVolumeDown, faPlus, faMinus
} from '../../../client/icons';
import { SettingsService } from '../../services/settingsService';
import { Audio } from '../../services/audio';
@Component({
selector: 'settings-box',
templateUrl: 'settings-box.pug',
styleUrls: ['settings-box.scss'],
selector: 'settings-box',
templateUrl: 'settings-box.pug',
styleUrls: ['settings-box.scss'],
})
export class SettingsBox implements OnInit, OnDestroy {
readonly cogIcon = faCog;
readonly searchIcon = faSearch;
readonly signOutIcon = faSignOutAlt;
readonly forwardIcon = faStepForward;
readonly emptyIcon = emptyIcon;
readonly plusIcon = faPlus;
readonly minusIcon = faMinus;
modalRef?: BsModalRef;
time?: string;
@ViewChild('dropdown', { static: true }) dropdown!: Dropdown;
@ViewChild('actionsModal', { static: true }) actionsModal!: TemplateRef<any>;
@ViewChild('settingsModal', { static: true }) settingsModal!: TemplateRef<any>;
@ViewChild('invitesModal', { static: true }) invitesModal!: TemplateRef<any>;
private subscription?: Subscription;
constructor(
private model: Model,
private modalService: BsModalService,
private settingsService: SettingsService,
private gameService: GameService,
private game: PonyTownGame,
private audio: Audio,
private zone: NgZone,
) {
}
get scale() {
return this.game.scale;
}
get volume() {
return this.game.volume;
}
set volume(value: number) {
this.settingsService.browser.volume = value;
this.settingsService.saveBrowserSettings();
this.audio.setVolume(value);
}
get server() {
return this.gameService.server && this.gameService.server.name || '';
}
get settings() {
return this.model.account && this.model.account.settings || {};
}
get track() {
return this.game.audio.trackName;
}
get volumeIcon() {
return this.volume === 0 ? faVolumeOff : (this.volume < 50 ? faVolumeDown : faVolumeUp);
}
get isMod() {
return this.model.isMod;
}
get hasInvites() {
return this.isMod; // TEMP
}
ngOnInit() {
this.game.onClock
.pipe(
distinctUntilChanged(),
)
.subscribe(text => {
if (this.dropdown.isOpen) {
this.zone.run(() => this.time = text);
} else {
this.time = text;
}
});
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
toggleVolume() {
this.volume = this.volume === 0 ? 50 : 0;
}
volumeStarted() {
this.game.audio.forcePlay();
}
nextTrack() {
this.game.audio.playRandomTrack();
}
leave() {
this.gameService.leave('From settings dropdown');
this.dropdown.close();
}
zoomOut() {
this.game.zoomOut();
}
zoomIn() {
this.game.zoomIn();
}
unhideAllHiddenPlayers() {
this.game.send(server => server.action(Action.UnhideAllHiddenPlayers));
this.dropdown.close();
}
openModal(template: TemplateRef<any>) {
this.modalRef = this.modalService.show(template, { ignoreBackdropClick: true });
}
openSettings() {
this.openModal(this.settingsModal);
this.dropdown.close();
}
openActions() {
this.openModal(this.actionsModal);
this.dropdown.close();
}
openInvites() {
if (BETA) {
this.openModal(this.invitesModal);
this.dropdown.close();
}
}
readonly cogIcon = faCog;
readonly searchIcon = faSearch;
readonly signOutIcon = faSignOutAlt;
readonly forwardIcon = faStepForward;
readonly emptyIcon = emptyIcon;
readonly plusIcon = faPlus;
readonly minusIcon = faMinus;
modalRef?: BsModalRef;
time?: string;
@ViewChild('dropdown', { static: true }) dropdown!: Dropdown;
@ViewChild('actionsModal', { static: true }) actionsModal!: TemplateRef<any>;
@ViewChild('settingsModal', { static: true }) settingsModal!: TemplateRef<any>;
@ViewChild('invitesModal', { static: true }) invitesModal!: TemplateRef<any>;
private subscription?: Subscription;
constructor(
private model: Model,
private modalService: BsModalService,
private settingsService: SettingsService,
private gameService: GameService,
private game: PonyTownGame,
private audio: Audio,
private zone: NgZone,
) {
}
get scale() {
return this.game.scale;
}
get volume() {
return this.game.volume;
}
set volume(value: number) {
this.settingsService.browser.volume = value;
this.settingsService.saveBrowserSettings();
this.audio.setVolume(value);
}
get server() {
return this.gameService.server && this.gameService.server.name || '';
}
get settings() {
return this.model.account && this.model.account.settings || {};
}
get track() {
return this.game.audio.trackName;
}
get volumeIcon() {
return this.volume === 0 ? faVolumeOff : (this.volume < 50 ? faVolumeDown : faVolumeUp);
}
get isMod() {
return this.model.isMod;
}
get hasInvites() {
return this.isMod; // TEMP
}
ngOnInit() {
this.game.onClock
.pipe(
distinctUntilChanged(),
)
.subscribe(text => {
if (this.dropdown.isOpen) {
this.zone.run(() => this.time = text);
} else {
this.time = text;
}
});
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
toggleVolume() {
this.volume = this.volume === 0 ? 50 : 0;
}
volumeStarted() {
this.game.audio.forcePlay();
}
nextTrack() {
this.game.audio.playRandomTrack();
}
leave() {
this.gameService.leave('From settings dropdown');
this.dropdown.close();
}
zoomOut() {
this.game.zoomOut();
}
zoomIn() {
this.game.zoomIn();
}
unhideAllHiddenPlayers() {
this.game.send(server => server.action(Action.UnhideAllHiddenPlayers));
this.dropdown.close();
}
openModal(template: TemplateRef<any>) {
this.modalRef = this.modalService.show(template, { ignoreBackdropClick: true });
}
openSettings() {
this.openModal(this.settingsModal);
this.dropdown.close();
}
openActions() {
this.openModal(this.actionsModal);
this.dropdown.close();
}
openInvites() {
if (BETA) {
this.openModal(this.invitesModal);
this.dropdown.close();
}
}
}
@@ -3,7 +3,7 @@ import { Subscription } from 'rxjs';
import { AccountSettings, BrowserSettings } from '../../../common/interfaces';
import { SettingsService } from '../../services/settingsService';
import {
DEFAULT_CHATLOG_OPACITY, MAX_CHATLOG_RANGE, MIN_CHATLOG_RANGE, isChatlogRangeUnlimited, MAX_FILTER_WORDS_LENGTH
DEFAULT_CHATLOG_OPACITY, MAX_CHATLOG_RANGE, MIN_CHATLOG_RANGE, isChatlogRangeUnlimited, MAX_FILTER_WORDS_LENGTH
} from '../../../common/constants';
import { StorageService } from '../../services/storageService';
import { cloneDeep } from '../../../common/utils';
@@ -12,111 +12,111 @@ import { updateRangeIndicator } from '../../../client/clientUtils';
import { faSlidersH, faCommentSlash, faGamepad, faImage } from '../../../client/icons';
@Component({
selector: 'settings-modal',
templateUrl: 'settings-modal.pug',
styleUrls: ['settings-modal.scss'],
selector: 'settings-modal',
templateUrl: 'settings-modal.pug',
styleUrls: ['settings-modal.scss'],
})
export class SettingsModal implements OnInit, OnDestroy {
readonly maxChatlogRange = MAX_CHATLOG_RANGE;
readonly minChatlogRange = MIN_CHATLOG_RANGE;
readonly gameIcon = faSlidersH;
readonly chatIcon = faCommentSlash;
readonly filtersIcon = faCommentSlash;
readonly controlsIcon = faGamepad;
readonly graphicsIcon = faImage;
@Output() close = new EventEmitter();
account: AccountSettings = {};
browser: BrowserSettings = {};
accountBackup: AccountSettings = {};
browserBackup: BrowserSettings = {};
private done = false;
private subscription?: Subscription;
constructor(
private settingsService: SettingsService,
private storage: StorageService,
private game: PonyTownGame,
) {
}
get pane() {
return this.storage.getItem('settings-modal-pane') || 'game';
}
set pane(value: string) {
this.storage.setItem('settings-modal-pane', value);
}
get lockLowGraphicsMode() {
return this.game.failedFBO;
}
get chatlogRangeText() {
const range = this.account.chatlogRange;
return isChatlogRangeUnlimited(range) ? 'entire screen' : `${range} tiles`;
}
ngOnInit() {
this.accountBackup = cloneDeep(this.settingsService.account);
this.browserBackup = cloneDeep(this.settingsService.browser);
this.account = this.settingsService.account;
this.browser = this.settingsService.browser;
this.setupDefaults();
this.subscription = this.game.onLeft.subscribe(() => this.cancel());
}
ngOnDestroy() {
this.finishChatlogRange();
readonly maxChatlogRange = MAX_CHATLOG_RANGE;
readonly minChatlogRange = MIN_CHATLOG_RANGE;
readonly gameIcon = faSlidersH;
readonly chatIcon = faCommentSlash;
readonly filtersIcon = faCommentSlash;
readonly controlsIcon = faGamepad;
readonly graphicsIcon = faImage;
@Output() close = new EventEmitter();
account: AccountSettings = {};
browser: BrowserSettings = {};
accountBackup: AccountSettings = {};
browserBackup: BrowserSettings = {};
private done = false;
private subscription?: Subscription;
constructor(
private settingsService: SettingsService,
private storage: StorageService,
private game: PonyTownGame,
) {
}
get pane() {
return this.storage.getItem('settings-modal-pane') || 'game';
}
set pane(value: string) {
this.storage.setItem('settings-modal-pane', value);
}
get lockLowGraphicsMode() {
return this.game.failedFBO;
}
get chatlogRangeText() {
const range = this.account.chatlogRange;
return isChatlogRangeUnlimited(range) ? 'entire screen' : `${range} tiles`;
}
ngOnInit() {
this.accountBackup = cloneDeep(this.settingsService.account);
this.browserBackup = cloneDeep(this.settingsService.browser);
this.account = this.settingsService.account;
this.browser = this.settingsService.browser;
this.setupDefaults();
this.subscription = this.game.onLeft.subscribe(() => this.cancel());
}
ngOnDestroy() {
this.finishChatlogRange();
if (!this.done) {
this.cancel();
}
if (!this.done) {
this.cancel();
}
this.subscription && this.subscription.unsubscribe();
}
reset() {
this.account = this.settingsService.account = {};
this.browser = this.settingsService.browser = {};
this.setupDefaults();
}
cancel() {
this.done = true;
this.settingsService.account = this.accountBackup;
this.settingsService.browser = this.browserBackup;
this.close.emit();
}
ok() {
if (this.account.filterWords) {
let filter = this.account.filterWords;
this.subscription && this.subscription.unsubscribe();
}
reset() {
this.account = this.settingsService.account = {};
this.browser = this.settingsService.browser = {};
this.setupDefaults();
}
cancel() {
this.done = true;
this.settingsService.account = this.accountBackup;
this.settingsService.browser = this.browserBackup;
this.close.emit();
}
ok() {
if (this.account.filterWords) {
let filter = this.account.filterWords;
while (filter.length > MAX_FILTER_WORDS_LENGTH && /\s/.test(filter)) {
filter = filter.trim().replace(/\s+\S+$/, '');
}
while (filter.length > MAX_FILTER_WORDS_LENGTH && /\s/.test(filter)) {
filter = filter.trim().replace(/\s+\S+$/, '');
}
if (filter.length > MAX_FILTER_WORDS_LENGTH) {
this.account.filterWords = '';
} else {
this.account.filterWords = filter;
}
}
if (filter.length > MAX_FILTER_WORDS_LENGTH) {
this.account.filterWords = '';
} else {
this.account.filterWords = filter;
}
}
this.done = true;
this.settingsService.saveAccountSettings(this.account);
this.settingsService.saveBrowserSettings(this.browser);
this.close.emit();
}
updateChatlogRange(range: number | undefined) {
document.body.classList.add('translucent-modals');
updateRangeIndicator(range, this.game);
}
finishChatlogRange() {
document.body.classList.remove('translucent-modals');
updateRangeIndicator(undefined, this.game);
}
private setupDefaults() {
if (this.account.chatlogOpacity === undefined) {
this.account.chatlogOpacity = DEFAULT_CHATLOG_OPACITY;
}
this.done = true;
this.settingsService.saveAccountSettings(this.account);
this.settingsService.saveBrowserSettings(this.browser);
this.close.emit();
}
updateChatlogRange(range: number | undefined) {
document.body.classList.add('translucent-modals');
updateRangeIndicator(range, this.game);
}
finishChatlogRange() {
document.body.classList.remove('translucent-modals');
updateRangeIndicator(undefined, this.game);
}
private setupDefaults() {
if (this.account.chatlogOpacity === undefined) {
this.account.chatlogOpacity = DEFAULT_CHATLOG_OPACITY;
}
if (this.account.chatlogRange === undefined) {
this.account.chatlogRange = MAX_CHATLOG_RANGE;
}
if (this.account.chatlogRange === undefined) {
this.account.chatlogRange = MAX_CHATLOG_RANGE;
}
if (this.account.filterWords === undefined) {
this.account.filterWords = '';
}
}
if (this.account.filterWords === undefined) {
this.account.filterWords = '';
}
}
}
+78 -78
View File
@@ -75,87 +75,87 @@ import { SaveActiveTab } from './directives/saveActiveTab';
import { SiteNamePipe } from './pipes/siteName';
const declarations = [
ActionBar,
ActionButton,
ActionsModal,
BitmapBox,
ButtMarkEditor,
MenuBar,
MenuItem,
CharacterList,
CharacterPreview,
CharacterSelect,
EmoteBox,
SliderBar,
SpriteBox,
SpriteSelection,
SupportButton,
SupporterPony,
SetSelection,
SetOutlineHidden,
CheckBox,
PortraitBox,
ScalePicker,
SwapBox,
ColorPicker,
CustomCheckbox,
DatePicker,
SignInBox,
PonyBox,
ModBox,
PartyBox,
PartyList,
SiteInfo,
SettingsBox,
SettingsModal,
FillOutline,
FriendsBox,
InstallButton,
InvitesModal,
KbdKey,
PlayBox,
PlayNotice,
PageLoader,
ChatBox,
ChatLog,
SiteLinks,
NotificationItem,
NotificationList,
...dropdownDirectives,
...tabsetComponents,
...draggableComponents,
...virtualListDirectives,
VirtualList,
Anchor,
BtnHighlight,
BtnHighlightDanger,
AgDrag,
AgAutoFocus,
LinkCurrent,
LabelledBy,
RevSrc,
FixToTop,
FocusTitle,
FocusTrap,
HasFeature,
SaveActiveTab,
SiteNamePipe,
ActionBar,
ActionButton,
ActionsModal,
BitmapBox,
ButtMarkEditor,
MenuBar,
MenuItem,
CharacterList,
CharacterPreview,
CharacterSelect,
EmoteBox,
SliderBar,
SpriteBox,
SpriteSelection,
SupportButton,
SupporterPony,
SetSelection,
SetOutlineHidden,
CheckBox,
PortraitBox,
ScalePicker,
SwapBox,
ColorPicker,
CustomCheckbox,
DatePicker,
SignInBox,
PonyBox,
ModBox,
PartyBox,
PartyList,
SiteInfo,
SettingsBox,
SettingsModal,
FillOutline,
FriendsBox,
InstallButton,
InvitesModal,
KbdKey,
PlayBox,
PlayNotice,
PageLoader,
ChatBox,
ChatLog,
SiteLinks,
NotificationItem,
NotificationList,
...dropdownDirectives,
...tabsetComponents,
...draggableComponents,
...virtualListDirectives,
VirtualList,
Anchor,
BtnHighlight,
BtnHighlightDanger,
AgDrag,
AgAutoFocus,
LinkCurrent,
LabelledBy,
RevSrc,
FixToTop,
FocusTitle,
FocusTrap,
HasFeature,
SaveActiveTab,
SiteNamePipe,
];
@NgModule({
imports: [
BrowserModule,
RouterModule,
FormsModule,
TooltipModule.forRoot(),
PopoverModule,
ButtonsModule,
ModalModule.forRoot(),
FontAwesomeModule,
// ScrollingModule,
],
declarations: declarations,
exports: declarations,
imports: [
BrowserModule,
RouterModule,
FormsModule,
TooltipModule.forRoot(),
PopoverModule,
ButtonsModule,
ModalModule.forRoot(),
FontAwesomeModule,
// ScrollingModule,
],
declarations: declarations,
exports: declarations,
})
export class SharedModule {
}
@@ -4,23 +4,23 @@ import { emptyIcon, oauthIcons } from '../../../client/icons';
import { OAuthProvider } from '../../../common/interfaces';
export function getProviderIcon(id: string) {
return oauthIcons[id] || emptyIcon;
return oauthIcons[id] || emptyIcon;
}
@Component({
selector: 'sign-in-box',
templateUrl: 'sign-in-box.pug',
styleUrls: ['sign-in-box.scss'],
selector: 'sign-in-box',
templateUrl: 'sign-in-box.pug',
styleUrls: ['sign-in-box.scss'],
})
export class SignInBox {
readonly signUpProviders = signUpProviders;
readonly signInProviders = signInProviders;
readonly local = local || DEVELOPMENT;
@Output() signIn = new EventEmitter<OAuthProvider>();
icon(id: string) {
return getProviderIcon(id);
}
signInTo(provider: OAuthProvider) {
this.signIn.emit(provider);
}
readonly signUpProviders = signUpProviders;
readonly signInProviders = signInProviders;
readonly local = local || DEVELOPMENT;
@Output() signIn = new EventEmitter<OAuthProvider>();
icon(id: string) {
return getProviderIcon(id);
}
signInTo(provider: OAuthProvider) {
this.signIn.emit(provider);
}
}
+10 -10
View File
@@ -4,16 +4,16 @@ import { toSocialSiteInfo } from '../../../client/clientUtils';
import { getProviderIcon } from '../sign-in-box/sign-in-box';
@Component({
selector: 'site-info',
templateUrl: 'site-info.pug',
styleUrls: ['site-info.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'site-info',
templateUrl: 'site-info.pug',
styleUrls: ['site-info.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SiteInfo {
info?: SocialSiteInfo;
icon: any;
@Input() set site(value: SocialSite | undefined) {
this.info = value && toSocialSiteInfo(value);
this.icon = getProviderIcon(this.info && this.info.icon || '');
}
info?: SocialSiteInfo;
icon: any;
@Input() set site(value: SocialSite | undefined) {
this.info = value && toSocialSiteInfo(value);
this.icon = getProviderIcon(this.info && this.info.icon || '');
}
}
@@ -1,10 +1,10 @@
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
@Component({
selector: 'site-links',
templateUrl: 'site-links.pug',
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'site-links',
templateUrl: 'site-links.pug',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SiteLinks {
@Input() links: string[] = [];
@Input() links: string[] = [];
}
@@ -1,89 +1,89 @@
import {
Component, Input, Output, EventEmitter, ElementRef, ChangeDetectionStrategy, HostListener, ViewChild
Component, Input, Output, EventEmitter, ElementRef, ChangeDetectionStrategy, HostListener, ViewChild
} from '@angular/core';
import { clamp } from 'lodash';
import { AgDragEvent } from '../directives/agDrag';
import { Key } from '../../../client/input/input';
@Component({
selector: 'slider-bar',
templateUrl: 'slider-bar.pug',
styleUrls: ['slider-bar.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'role': 'slider',
'[tabindex]': 'disabled ? -1 : 0',
'[attr.aria-valuemin]': 'min',
'[attr.aria-valuemax]': 'max',
'[attr.aria-valuenow]': 'value',
'[attr.aria-disabled]': 'disabled',
},
selector: 'slider-bar',
templateUrl: 'slider-bar.pug',
styleUrls: ['slider-bar.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'role': 'slider',
'[tabindex]': 'disabled ? -1 : 0',
'[attr.aria-valuemin]': 'min',
'[attr.aria-valuemax]': 'max',
'[attr.aria-valuenow]': 'value',
'[attr.aria-disabled]': 'disabled',
},
})
export class SliderBar {
@Input() min = 0;
@Input() max = 100;
@Input() step = 0;
@Input() largeStep = 10;
@Input() disabled = false;
@Input() value = 0;
@Output() valueChange = new EventEmitter<number>();
@Output() changed = new EventEmitter<number>();
@ViewChild('bar', { static: true }) bar!: ElementRef;
private currentWidth = 0;
get width() {
return clamp(((this.value - this.min) / (this.max - this.min)) * 100, 0, 100);
}
drag({ type, x, event }: AgDragEvent) {
if (this.disabled)
return;
@Input() min = 0;
@Input() max = 100;
@Input() step = 0;
@Input() largeStep = 10;
@Input() disabled = false;
@Input() value = 0;
@Output() valueChange = new EventEmitter<number>();
@Output() changed = new EventEmitter<number>();
@ViewChild('bar', { static: true }) bar!: ElementRef;
private currentWidth = 0;
get width() {
return clamp(((this.value - this.min) / (this.max - this.min)) * 100, 0, 100);
}
drag({ type, x, event }: AgDragEvent) {
if (this.disabled)
return;
event.preventDefault();
event.preventDefault();
if (type === 'start') {
this.currentWidth = this.bar.nativeElement.getBoundingClientRect().width;
}
if (type === 'start') {
this.currentWidth = this.bar.nativeElement.getBoundingClientRect().width;
}
let val = this.min + clamp(x / this.currentWidth, 0, 1) * (this.max - this.min);
let val = this.min + clamp(x / this.currentWidth, 0, 1) * (this.max - this.min);
if (this.step) {
val = Math.round(val / this.step) * this.step;
}
if (this.step) {
val = Math.round(val / this.step) * this.step;
}
this.setValue(val, false);
this.setValue(val, false);
if (type === 'end') {
this.changed.emit(val);
}
}
@HostListener('keydown', ['$event'])
keydown(e: KeyboardEvent) {
if (this.disabled)
return;
if (type === 'end') {
this.changed.emit(val);
}
}
@HostListener('keydown', ['$event'])
keydown(e: KeyboardEvent) {
if (this.disabled)
return;
const step = this.step || 1;
const step = this.step || 1;
if (e.keyCode === Key.LEFT || e.keyCode === Key.DOWN || e.keyCode === Key.PAGE_DOWN) {
e.preventDefault();
this.setValue(this.value - step * (e.keyCode === Key.PAGE_DOWN ? this.largeStep : 1), true);
} else if (e.keyCode === Key.RIGHT || e.keyCode === Key.UP || e.keyCode === Key.PAGE_UP) {
e.preventDefault();
this.setValue(this.value + step * (e.keyCode === Key.PAGE_UP ? this.largeStep : 1), true);
} else if (e.keyCode === Key.HOME) {
e.preventDefault();
this.setValue(this.min, true);
} else if (e.keyCode === Key.END) {
e.preventDefault();
this.setValue(this.max, true);
}
}
private setValue(value: number, emit: boolean) {
if (this.value !== value) {
this.value = clamp(value, this.min, this.max);
this.valueChange.emit(this.value);
if (e.keyCode === Key.LEFT || e.keyCode === Key.DOWN || e.keyCode === Key.PAGE_DOWN) {
e.preventDefault();
this.setValue(this.value - step * (e.keyCode === Key.PAGE_DOWN ? this.largeStep : 1), true);
} else if (e.keyCode === Key.RIGHT || e.keyCode === Key.UP || e.keyCode === Key.PAGE_UP) {
e.preventDefault();
this.setValue(this.value + step * (e.keyCode === Key.PAGE_UP ? this.largeStep : 1), true);
} else if (e.keyCode === Key.HOME) {
e.preventDefault();
this.setValue(this.min, true);
} else if (e.keyCode === Key.END) {
e.preventDefault();
this.setValue(this.max, true);
}
}
private setValue(value: number, emit: boolean) {
if (this.value !== value) {
this.value = clamp(value, this.min, this.max);
this.valueChange.emit(this.value);
if (emit) {
this.changed.emit(value);
}
}
}
if (emit) {
this.changed.emit(value);
}
}
}
}
+138 -138
View File
@@ -1,5 +1,5 @@
import {
Component, Input, AfterViewInit, ViewChild, ElementRef, NgZone, DoCheck, IterableDiffers, IterableDiffer, OnChanges
Component, Input, AfterViewInit, ViewChild, ElementRef, NgZone, DoCheck, IterableDiffers, IterableDiffer, OnChanges
} from '@angular/core';
import { Rect, Sprite, ColorExtra, Palette } from '../../../common/interfaces';
import { parseColor, colorToCSS } from '../../../common/color';
@@ -16,171 +16,171 @@ let redrawFrame = 0;
const forRedraw: SpriteBox[] = [];
function drawAll() {
redrawFrame = 0;
forRedraw.forEach(box => box.draw());
forRedraw.length = 0;
redrawFrame = 0;
forRedraw.forEach(box => box.draw());
forRedraw.length = 0;
}
@Component({
selector: 'sprite-box',
templateUrl: 'sprite-box.pug',
styleUrls: ['sprite-box.scss'],
selector: 'sprite-box',
templateUrl: 'sprite-box.pug',
styleUrls: ['sprite-box.scss'],
})
export class SpriteBox implements AfterViewInit, OnChanges, DoCheck {
readonly debug = DEVELOPMENT;
readonly noneIcon = faTimes;
@Input() size = 52;
@Input() scale = 2;
@Input() x = 0;
@Input() y = 0;
@Input() center = true;
@Input() index?: number;
@Input() sprite?: ColorExtra;
@Input() palette?: Palette;
@Input() fill?: string[] | string;
@Input() outline?: string[] | string;
@Input() reverseExtra?: boolean;
@Input() timestamp: any;
@Input() invisible = false;
@Input() darken = true;
@ViewChild('canvas', { static: true }) canvas!: ElementRef;
private _circle?: string;
private fillDiffer: IterableDiffer<string>;
private outlineDiffer: IterableDiffer<string>;
private batch?: ContextSpriteBatch;
constructor(private zone: NgZone, iterableDiffers: IterableDiffers) {
this.fillDiffer = iterableDiffers.find([]).create<string>();
this.outlineDiffer = iterableDiffers.find([]).create<string>();
}
@Input() get circle() {
return this._circle;
}
set circle(value) {
this._circle = colorToCSS(parseColor(value || ''));
}
ngAfterViewInit() {
loadAndInitSpriteSheets().then(() => this.redraw());
}
ngDoCheck() {
const fillChanges = this.fill && Array.isArray(this.fill) && this.fillDiffer.diff(this.fill);
const outlineChanges = this.outline && Array.isArray(this.outline) && this.outlineDiffer.diff(this.outline);
readonly debug = DEVELOPMENT;
readonly noneIcon = faTimes;
@Input() size = 52;
@Input() scale = 2;
@Input() x = 0;
@Input() y = 0;
@Input() center = true;
@Input() index?: number;
@Input() sprite?: ColorExtra;
@Input() palette?: Palette;
@Input() fill?: string[] | string;
@Input() outline?: string[] | string;
@Input() reverseExtra?: boolean;
@Input() timestamp: any;
@Input() invisible = false;
@Input() darken = true;
@ViewChild('canvas', { static: true }) canvas!: ElementRef;
private _circle?: string;
private fillDiffer: IterableDiffer<string>;
private outlineDiffer: IterableDiffer<string>;
private batch?: ContextSpriteBatch;
constructor(private zone: NgZone, iterableDiffers: IterableDiffers) {
this.fillDiffer = iterableDiffers.find([]).create<string>();
this.outlineDiffer = iterableDiffers.find([]).create<string>();
}
@Input() get circle() {
return this._circle;
}
set circle(value) {
this._circle = colorToCSS(parseColor(value || ''));
}
ngAfterViewInit() {
loadAndInitSpriteSheets().then(() => this.redraw());
}
ngDoCheck() {
const fillChanges = this.fill && Array.isArray(this.fill) && this.fillDiffer.diff(this.fill);
const outlineChanges = this.outline && Array.isArray(this.outline) && this.outlineDiffer.diff(this.outline);
if (fillChanges || outlineChanges) {
this.redraw();
}
}
ngOnChanges() {
this.redraw();
}
private redraw() {
if (!redrawFrame) {
this.zone.runOutsideAngular(() => redrawFrame = requestAnimationFrame(drawAll));
}
if (fillChanges || outlineChanges) {
this.redraw();
}
}
ngOnChanges() {
this.redraw();
}
private redraw() {
if (!redrawFrame) {
this.zone.runOutsideAngular(() => redrawFrame = requestAnimationFrame(drawAll));
}
if (forRedraw.indexOf(this) === -1) {
forRedraw.push(this);
}
}
draw() {
const size = this.size;
const scale = this.scale;
const canvas = this.canvas.nativeElement as HTMLCanvasElement;
if (forRedraw.indexOf(this) === -1) {
forRedraw.push(this);
}
}
draw() {
const size = this.size;
const scale = this.scale;
const canvas = this.canvas.nativeElement as HTMLCanvasElement;
if (!size || this.invisible)
return;
if (!size || this.invisible)
return;
if (canvas.width !== size || canvas.height !== size) {
canvas.width = size;
canvas.height = size;
}
if (canvas.width !== size || canvas.height !== size) {
canvas.width = size;
canvas.height = size;
}
const context = canvas.getContext('2d');
const context = canvas.getContext('2d');
if (!context)
return;
if (!context)
return;
context.save();
context.clearRect(0, 0, canvas.width, canvas.height);
context.save();
context.clearRect(0, 0, canvas.width, canvas.height);
const sprite = this.sprite;
const sprite = this.sprite;
if (sprite) {
if (this.circle) {
context.fillStyle = this.circle;
context.beginPath();
context.arc(canvas.width / 2, canvas.height / 2, canvas.width / 3, 0, Math.PI * 2);
context.fill();
}
if (sprite) {
if (this.circle) {
context.fillStyle = this.circle;
context.beginPath();
context.arc(canvas.width / 2, canvas.height / 2, canvas.width / 3, 0, Math.PI * 2);
context.fill();
}
const bufferSize = size / scale;
const batch = this.batch = this.batch || new ContextSpriteBatch(createCanvas(bufferSize, bufferSize));
resizeCanvas(batch.canvas, bufferSize, bufferSize);
const bufferSize = size / scale;
const batch = this.batch = this.batch || new ContextSpriteBatch(createCanvas(bufferSize, bufferSize));
resizeCanvas(batch.canvas, bufferSize, bufferSize);
const fills = Array.isArray(this.fill) ? this.fill : [this.fill];
const outlines = Array.isArray(this.outline) ? this.outline : [this.outline];
const paletteColors = toColorList(getColorsFromSet({ fills, outlines }, '000000', this.darken));
const palette = mockPaletteManager.addArray(paletteColors);
const extraPalette = sprite.palettes && mockPaletteManager.addArray(sprite.palettes[0]);
const fills = Array.isArray(this.fill) ? this.fill : [this.fill];
const outlines = Array.isArray(this.outline) ? this.outline : [this.outline];
const paletteColors = toColorList(getColorsFromSet({ fills, outlines }, '000000', this.darken));
const palette = mockPaletteManager.addArray(paletteColors);
const extraPalette = sprite.palettes && mockPaletteManager.addArray(sprite.palettes[0]);
let x = this.x;
let y = this.y;
let x = this.x;
let y = this.y;
if (this.center) {
const bounds = rect(0, 0, 0, 0);
addRect(bounds, sprite.color);
addRect(bounds, sprite.extra);
if (this.center) {
const bounds = rect(0, 0, 0, 0);
addRect(bounds, sprite.color);
addRect(bounds, sprite.extra);
if (sprite.colorMany) {
sprite.colorMany.forEach(c => addRect(bounds, c));
}
if (sprite.colorMany) {
sprite.colorMany.forEach(c => addRect(bounds, c));
}
x = Math.round((bufferSize - bounds.w) / 2 - bounds.x);
y = Math.round((bufferSize - bounds.h) / 2 - bounds.y);
}
x = Math.round((bufferSize - bounds.w) / 2 - bounds.x);
y = Math.round((bufferSize - bounds.h) / 2 - bounds.y);
}
batch.start(paletteSpriteSheet, 0);
batch.start(paletteSpriteSheet, 0);
if (this.reverseExtra) {
batch.drawSprite(sprite.extra, WHITE, extraPalette, x, y);
}
if (this.reverseExtra) {
batch.drawSprite(sprite.extra, WHITE, extraPalette, x, y);
}
if (sprite.colorMany) {
for (const color of sprite.colorMany) {
batch.drawSprite(color, WHITE, palette, x, y);
}
} else {
batch.drawSprite(sprite.color, WHITE, palette, x, y);
}
if (sprite.colorMany) {
for (const color of sprite.colorMany) {
batch.drawSprite(color, WHITE, palette, x, y);
}
} else {
batch.drawSprite(sprite.color, WHITE, palette, x, y);
}
if (!this.reverseExtra) {
batch.drawSprite(sprite.extra, WHITE, extraPalette, x, y);
}
if (!this.reverseExtra) {
batch.drawSprite(sprite.extra, WHITE, extraPalette, x, y);
}
batch.end();
batch.end();
disableImageSmoothing(context);
context.scale(scale, scale);
context.drawImage(batch.canvas, 0, 0);
}
disableImageSmoothing(context);
context.scale(scale, scale);
context.drawImage(batch.canvas, 0, 0);
}
context.restore();
}
context.restore();
}
}
function addRect(rect: Rect, sprite: Sprite | undefined) {
if (sprite && sprite.w && sprite.h) {
if (rect.w === 0 || rect.h === 0) {
rect.x = sprite.ox;
rect.y = sprite.oy;
rect.w = sprite.w;
rect.h = sprite.h;
} else {
const x = Math.min(rect.x, sprite.ox);
const y = Math.min(rect.y, sprite.oy);
rect.w = Math.max(rect.x + rect.w, sprite.ox + sprite.w) - x;
rect.h = Math.max(rect.y + rect.h, sprite.oy + sprite.h) - y;
rect.x = x;
rect.y = y;
}
}
if (sprite && sprite.w && sprite.h) {
if (rect.w === 0 || rect.h === 0) {
rect.x = sprite.ox;
rect.y = sprite.oy;
rect.w = sprite.w;
rect.h = sprite.h;
} else {
const x = Math.min(rect.x, sprite.ox);
const y = Math.min(rect.y, sprite.oy);
rect.w = Math.max(rect.x + rect.w, sprite.ox + sprite.w) - x;
rect.h = Math.max(rect.y + rect.h, sprite.oy + sprite.h) - y;
rect.x = x;
rect.y = y;
}
}
}
@@ -7,91 +7,91 @@ import { focusElementAfterTimeout } from '../../../client/htmlUtils';
const MAX = 999999;
@Component({
selector: 'sprite-selection',
templateUrl: 'sprite-selection.pug',
styleUrls: ['sprite-selection.scss'],
host: {
'role': 'radiogroup',
'tabindex': '0',
'(keydown)': 'keydown($event)',
'[attr.aria-activedescendant]': 'activeDescendant',
},
selector: 'sprite-selection',
templateUrl: 'sprite-selection.pug',
styleUrls: ['sprite-selection.scss'],
host: {
'role': 'radiogroup',
'tabindex': '0',
'(keydown)': 'keydown($event)',
'[attr.aria-activedescendant]': 'activeDescendant',
},
})
export class SpriteSelection {
@Input() selected = 0;
@Output() selectedChange = new EventEmitter<number>();
@Input() sprites?: ColorExtra[];
@Input() fill?: string | string[];
@Input() outline?: string | string[];
@Input() circle?: string;
@Input() reverseExtra = false;
@Input() limit = MAX;
@Input() skip = 0;
@Input() disabled = false;
@Input() emptyLabel?: string;
@Input() invisible = false;
@Input() darken = true;
id = uniqueId('sprite-selection-');
constructor(private element: ElementRef) {
}
get hasMore() {
return this.sprites && this.sprites.length > this.limit;
}
get end() {
return this.skip + this.limit;
}
get activeDescendant() {
return `${this.id}-${this.selected - this.skip}`;
}
isSelected(index: number) {
return this.selected === (index + this.skip);
}
select(index: number, focus = false) {
if (!this.disabled && this.selected !== index) {
this.selected = index;
this.selectedChange.emit(index);
@Input() selected = 0;
@Output() selectedChange = new EventEmitter<number>();
@Input() sprites?: ColorExtra[];
@Input() fill?: string | string[];
@Input() outline?: string | string[];
@Input() circle?: string;
@Input() reverseExtra = false;
@Input() limit = MAX;
@Input() skip = 0;
@Input() disabled = false;
@Input() emptyLabel?: string;
@Input() invisible = false;
@Input() darken = true;
id = uniqueId('sprite-selection-');
constructor(private element: ElementRef) {
}
get hasMore() {
return this.sprites && this.sprites.length > this.limit;
}
get end() {
return this.skip + this.limit;
}
get activeDescendant() {
return `${this.id}-${this.selected - this.skip}`;
}
isSelected(index: number) {
return this.selected === (index + this.skip);
}
select(index: number, focus = false) {
if (!this.disabled && this.selected !== index) {
this.selected = index;
this.selectedChange.emit(index);
if (this.hasMore && index >= this.end) {
this.showMore();
}
if (this.hasMore && index >= this.end) {
this.showMore();
}
if (focus) {
focusElementAfterTimeout(this.element.nativeElement, '.active');
}
}
}
showMore() {
this.limit = MAX;
}
keydown(e: KeyboardEvent) {
const select = this.handleKey(e.keyCode);
if (focus) {
focusElementAfterTimeout(this.element.nativeElement, '.active');
}
}
}
showMore() {
this.limit = MAX;
}
keydown(e: KeyboardEvent) {
const select = this.handleKey(e.keyCode);
if (select !== undefined) {
e.preventDefault();
this.select(select, true);
}
}
private handleKey(keyCode: number): number | undefined {
if (this.sprites) {
if (keyCode === Key.RIGHT || keyCode === Key.DOWN) {
if (this.selected >= (this.sprites.length - 1)) {
return this.skip;
} else {
return this.selected + 1;
}
} else if (keyCode === Key.LEFT || keyCode === Key.UP) {
if (this.selected <= this.skip) {
return this.sprites.length - 1;
} else {
return this.selected - 1;
}
} else if (keyCode === Key.HOME) {
return this.skip;
} else if (keyCode === Key.END) {
return this.sprites.length - 1;
}
}
if (select !== undefined) {
e.preventDefault();
this.select(select, true);
}
}
private handleKey(keyCode: number): number | undefined {
if (this.sprites) {
if (keyCode === Key.RIGHT || keyCode === Key.DOWN) {
if (this.selected >= (this.sprites.length - 1)) {
return this.skip;
} else {
return this.selected + 1;
}
} else if (keyCode === Key.LEFT || keyCode === Key.UP) {
if (this.selected <= this.skip) {
return this.sprites.length - 1;
} else {
return this.selected - 1;
}
} else if (keyCode === Key.HOME) {
return this.skip;
} else if (keyCode === Key.END) {
return this.sprites.length - 1;
}
}
return undefined;
}
return undefined;
}
}
@@ -3,15 +3,15 @@ import { Model } from '../../services/model';
import { supporterLink } from '../../../client/data';
@Component({
selector: 'support-button',
templateUrl: 'support-button.pug',
styleUrls: ['support-button.scss'],
selector: 'support-button',
templateUrl: 'support-button.pug',
styleUrls: ['support-button.scss'],
})
export class SupportButton {
readonly patreonLink = supporterLink;
constructor(private model: Model) {
}
get supporter() {
return this.model.supporter;
}
readonly patreonLink = supporterLink;
constructor(private model: Model) {
}
get supporter() {
return this.model.supporter;
}
}
@@ -9,80 +9,80 @@ import { CharacterPreview } from '../character-preview/character-preview';
import { decompressPonyString } from '../../../common/compressPony';
const BLEP: Expression = {
...defaultExpression,
muzzle: Muzzle.Blep,
...defaultExpression,
muzzle: Muzzle.Blep,
};
const EXCITED: Expression = {
...defaultExpression,
muzzle: Muzzle.SmileOpen,
...defaultExpression,
muzzle: Muzzle.SmileOpen,
};
const DERP: Expression = {
...defaultExpression,
muzzle: Muzzle.SmileOpen,
leftIris: Iris.Up,
...defaultExpression,
muzzle: Muzzle.SmileOpen,
leftIris: Iris.Up,
};
@Component({
selector: 'supporter-pony',
templateUrl: 'supporter-pony.pug',
selector: 'supporter-pony',
templateUrl: 'supporter-pony.pug',
})
export class SupporterPony implements OnInit, OnDestroy {
@ViewChild('characterPreview', { static: true }) characterPreview!: CharacterPreview;
@Input() scale = 3;
pony = decompressPonyString(SUPPORTER_PONY);
state = defaultPonyState();
private expression?: Expression;
private headAnimation?: HeadAnimation;
private headTime = 0;
private loop: FrameLoop;
constructor(frameService: FrameService) {
this.loop = frameService.create(delta => this.tick(delta));
}
ngOnInit() {
this.loop.init();
}
ngOnDestroy() {
this.loop.destroy();
}
excite() {
this.headTime = 0;
this.headAnimation = excite;
this.expression = Math.random() < 0.2 ? DERP : EXCITED;
}
reset() {
this.expression = undefined;
}
private tick(delta: number) {
this.headTime += delta;
@ViewChild('characterPreview', { static: true }) characterPreview!: CharacterPreview;
@Input() scale = 3;
pony = decompressPonyString(SUPPORTER_PONY);
state = defaultPonyState();
private expression?: Expression;
private headAnimation?: HeadAnimation;
private headTime = 0;
private loop: FrameLoop;
constructor(frameService: FrameService) {
this.loop = frameService.create(delta => this.tick(delta));
}
ngOnInit() {
this.loop.init();
}
ngOnDestroy() {
this.loop.destroy();
}
excite() {
this.headTime = 0;
this.headAnimation = excite;
this.expression = Math.random() < 0.2 ? DERP : EXCITED;
}
reset() {
this.expression = undefined;
}
private tick(delta: number) {
this.headTime += delta;
if (this.headAnimation) {
const frame = Math.floor(this.headTime * this.headAnimation.fps);
if (this.headAnimation) {
const frame = Math.floor(this.headTime * this.headAnimation.fps);
if (frame >= this.headAnimation.frames.length && !this.headAnimation.loop) {
this.headAnimation = undefined;
this.state.headAnimation = undefined;
this.state.headAnimationFrame = 0;
this.characterPreview.blink();
} else {
this.state.headAnimation = this.headAnimation;
this.state.headAnimationFrame = frame % this.headAnimation.frames.length;
}
} else {
this.state.headAnimation = undefined;
if (frame >= this.headAnimation.frames.length && !this.headAnimation.loop) {
this.headAnimation = undefined;
this.state.headAnimation = undefined;
this.state.headAnimationFrame = 0;
this.characterPreview.blink();
} else {
this.state.headAnimation = this.headAnimation;
this.state.headAnimationFrame = frame % this.headAnimation.frames.length;
}
} else {
this.state.headAnimation = undefined;
if (this.expression) {
if (Math.random() < 0.01) {
this.expression = undefined;
}
} else {
if (Math.random() < 0.005) {
this.expression = BLEP;
}
}
}
if (this.expression) {
if (Math.random() < 0.01) {
this.expression = undefined;
}
} else {
if (Math.random() < 0.005) {
this.expression = BLEP;
}
}
}
this.state.expression = this.expression;
}
this.state.expression = this.expression;
}
}
+29 -29
View File
@@ -8,36 +8,36 @@ import { Model } from '../../services/model';
// import { SWAP_TIMEOUT, SECOND } from '../../../common/constants';
@Component({
selector: 'swap-box',
templateUrl: 'swap-box.pug',
styleUrls: ['swap-box.scss'],
selector: 'swap-box',
templateUrl: 'swap-box.pug',
styleUrls: ['swap-box.scss'],
})
export class SwapBox {
readonly swapIcon = faExchangeAlt;
readonly timerIcon = faClock;
previewInfo: any;
@ViewChild('dropdown', { static: true }) dropdown!: Dropdown;
timeout = false;
constructor(private game: PonyTownGame, private zone: NgZone, private model: Model) {
}
toggleSwapDropdown() {
this.zone.run(() => setTimeout(() => { }, 10));
}
swapPony(pony: PonyObject) {
this.game.send(server => server.actionParam(Action.SwapCharacter, pony.id));
setTimeout(() => {
this.dropdown && this.dropdown.close();
pony.lastUsed = (new Date()).toISOString();
this.model.sortPonies();
});
readonly swapIcon = faExchangeAlt;
readonly timerIcon = faClock;
previewInfo: any;
@ViewChild('dropdown', { static: true }) dropdown!: Dropdown;
timeout = false;
constructor(private game: PonyTownGame, private zone: NgZone, private model: Model) {
}
toggleSwapDropdown() {
this.zone.run(() => setTimeout(() => { }, 10));
}
swapPony(pony: PonyObject) {
this.game.send(server => server.actionParam(Action.SwapCharacter, pony.id));
setTimeout(() => {
this.dropdown && this.dropdown.close();
pony.lastUsed = (new Date()).toISOString();
this.model.sortPonies();
});
// if (!this.timeout) {
// this.timeout = true;
// setTimeout(() => this.timeout = false, SWAP_TIMEOUT + SECOND);
// }
}
preview(pony: PonyObject | undefined) {
const info = pony && pony.ponyInfo;
this.previewInfo = info && toPalette(info, mockPaletteManager);
}
// if (!this.timeout) {
// this.timeout = true;
// setTimeout(() => this.timeout = false, SWAP_TIMEOUT + SECOND);
// }
}
preview(pony: PonyObject | undefined) {
const info = pony && pony.ponyInfo;
this.previewInfo = info && toPalette(info, mockPaletteManager);
}
}
+66 -66
View File
@@ -1,93 +1,93 @@
import {
Component, Directive, TemplateRef, Input, ContentChild, ContentChildren, QueryList, Output, EventEmitter
Component, Directive, TemplateRef, Input, ContentChild, ContentChildren, QueryList, Output, EventEmitter
} from '@angular/core';
import { uniqueId } from 'lodash';
import { Key } from '../../../client/input/input';
@Directive({
selector: '[tabTitle]'
selector: '[tabTitle]'
})
export class TabTitle {
constructor(public templateRef: TemplateRef<any>) {
}
constructor(public templateRef: TemplateRef<any>) {
}
}
@Directive({
selector: '[tabContent]',
selector: '[tabContent]',
})
export class TabContent {
constructor(public templateRef: TemplateRef<any>) {
}
constructor(public templateRef: TemplateRef<any>) {
}
}
@Directive({
selector: 'tab',
selector: 'tab',
})
export class Tab {
@Input() id = uniqueId(`tabset-tab`);
@Input() title?: string;
@Input() icon?: any;
@Input() disabled = false;
@ContentChild(TabContent, { static: false }) contentTpl?: TabContent;
@ContentChild(TabTitle, { static: false }) titleTpl?: TabTitle;
@Input() id = uniqueId(`tabset-tab`);
@Input() title?: string;
@Input() icon?: any;
@Input() disabled = false;
@ContentChild(TabContent, { static: false }) contentTpl?: TabContent;
@ContentChild(TabTitle, { static: false }) titleTpl?: TabTitle;
}
@Component({
selector: 'tabset',
templateUrl: 'tabset.pug',
selector: 'tabset',
templateUrl: 'tabset.pug',
})
export class Tabset {
justifyClass?: string;
@ContentChildren(Tab) tabs!: QueryList<Tab>;
@Input() label = '';
@Input() destroyOnHide = true;
@Input()
set justify(className: 'start' | 'center' | 'end' | 'fill' | 'justified') {
if (className === 'fill' || className === 'justified') {
this.justifyClass = `nav-${className}`;
} else {
this.justifyClass = `justify-content-${className}`;
}
}
@Input() orientation: 'horizontal' | 'vertical' = 'horizontal';
@Input() type: 'tabs' | 'pills' = 'tabs';
@Input() activeIndex = 0;
@Output() activeIndexChange = new EventEmitter<number>();
constructor() {
this.justify = 'start';
}
get navClass() {
return `nav-${this.type}${this.orientation === 'horizontal' ? ` ${this.justifyClass}` : ' flex-column'}`;
}
select(index: number) {
if (this.activeIndex !== index) {
this.activeIndex = index;
this.activeIndexChange.emit(index);
}
}
keydown(e: KeyboardEvent) {
const index = this.handleKey(e.keyCode);
justifyClass?: string;
@ContentChildren(Tab) tabs!: QueryList<Tab>;
@Input() label = '';
@Input() destroyOnHide = true;
@Input()
set justify(className: 'start' | 'center' | 'end' | 'fill' | 'justified') {
if (className === 'fill' || className === 'justified') {
this.justifyClass = `nav-${className}`;
} else {
this.justifyClass = `justify-content-${className}`;
}
}
@Input() orientation: 'horizontal' | 'vertical' = 'horizontal';
@Input() type: 'tabs' | 'pills' = 'tabs';
@Input() activeIndex = 0;
@Output() activeIndexChange = new EventEmitter<number>();
constructor() {
this.justify = 'start';
}
get navClass() {
return `nav-${this.type}${this.orientation === 'horizontal' ? ` ${this.justifyClass}` : ' flex-column'}`;
}
select(index: number) {
if (this.activeIndex !== index) {
this.activeIndex = index;
this.activeIndexChange.emit(index);
}
}
keydown(e: KeyboardEvent) {
const index = this.handleKey(e.keyCode);
if (index !== undefined) {
e.preventDefault();
const element = document.getElementById(this.tabs.toArray()[index].id);
element && element.focus();
this.select(index);
}
}
private handleKey(keyCode: number) {
if (keyCode === Key.LEFT) {
return this.activeIndex === 0 ? this.tabs.length - 1 : this.activeIndex - 1;
} else if (keyCode === Key.RIGHT) {
return this.activeIndex === this.tabs.length - 1 ? 0 : this.activeIndex + 1;
} else if (keyCode === Key.HOME) {
return 0;
} else if (keyCode === Key.END) {
return this.tabs.length - 1;
} else {
return undefined;
}
}
if (index !== undefined) {
e.preventDefault();
const element = document.getElementById(this.tabs.toArray()[index].id);
element && element.focus();
this.select(index);
}
}
private handleKey(keyCode: number) {
if (keyCode === Key.LEFT) {
return this.activeIndex === 0 ? this.tabs.length - 1 : this.activeIndex - 1;
} else if (keyCode === Key.RIGHT) {
return this.activeIndex === this.tabs.length - 1 ? 0 : this.activeIndex + 1;
} else if (keyCode === Key.HOME) {
return 0;
} else if (keyCode === Key.END) {
return this.tabs.length - 1;
} else {
return undefined;
}
}
}
export const tabsetComponents = [TabContent, TabTitle, Tabset, Tab];
@@ -1,167 +1,167 @@
import {
Directive, DoCheck, Input, ViewContainerRef, TemplateRef, IterableDiffers, IterableDiffer,
EmbeddedViewRef, Component, ElementRef, ViewChild, NgZone, OnDestroy, ChangeDetectorRef, AfterViewInit
Directive, DoCheck, Input, ViewContainerRef, TemplateRef, IterableDiffers, IterableDiffer,
EmbeddedViewRef, Component, ElementRef, ViewChild, NgZone, OnDestroy, ChangeDetectorRef, AfterViewInit
} from '@angular/core';
interface Context<T> {
$implicit: T;
index: number;
count: number;
_currentIndex: number;
$implicit: T;
index: number;
count: number;
_currentIndex: number;
}
@Component({
selector: 'virtual-list',
template: '<div #padStart></div><ng-content></ng-content><div #padEnd></div>',
styleUrls: ['virtual-list.scss'],
host: {
'tabindex': '0',
},
selector: 'virtual-list',
template: '<div #padStart></div><ng-content></ng-content><div #padEnd></div>',
styleUrls: ['virtual-list.scss'],
host: {
'tabindex': '0',
},
})
export class VirtualList {
@Input() itemSize = 50;
@ViewChild('padStart', { static: true }) padStart!: ElementRef;
@ViewChild('padEnd', { static: true }) padEnd!: ElementRef;
constructor(public element: ElementRef) {
}
@Input() itemSize = 50;
@ViewChild('padStart', { static: true }) padStart!: ElementRef;
@ViewChild('padEnd', { static: true }) padEnd!: ElementRef;
constructor(public element: ElementRef) {
}
}
@Directive({
selector: '[virtualFor][virtualForOf]',
selector: '[virtualFor][virtualForOf]',
})
export class VirtualFor<T> implements DoCheck, OnDestroy, AfterViewInit {
@Input()
set virtualForOf(forOf: T[]) {
this.forOf = forOf;
this.forOfDirty = true;
}
private forOf!: T[];
private forOfDirty: boolean = true;
private differ: IterableDiffer<T> | null = null;
private first = 0;
private last = 0;
constructor(
private viewContainer: ViewContainerRef,
private template: TemplateRef<Context<T>>,
private differs: IterableDiffers,
private list: VirtualList,
private changeDetector: ChangeDetectorRef,
zone: NgZone,
) {
zone.runOutsideAngular(() => {
list.element.nativeElement.addEventListener('scroll', this.detect);
window.addEventListener('resize', this.detect);
});
}
private detect = () => this.changeDetector.detectChanges();
@Input()
set virtualForTemplate(value: TemplateRef<Context<T>>) {
if (value) {
this.template = value;
}
}
ngOnDestroy() {
this.list.element.nativeElement.removeEventListener('scroll', this.detect);
window.removeEventListener('resize', this.detect);
}
ngAfterViewInit() {
setTimeout(this.detect, 0);
}
ngDoCheck(): void {
if (this.forOfDirty) {
this.forOfDirty = false;
const value = this.forOf;
@Input()
set virtualForOf(forOf: T[]) {
this.forOf = forOf;
this.forOfDirty = true;
}
private forOf!: T[];
private forOfDirty: boolean = true;
private differ: IterableDiffer<T> | null = null;
private first = 0;
private last = 0;
constructor(
private viewContainer: ViewContainerRef,
private template: TemplateRef<Context<T>>,
private differs: IterableDiffers,
private list: VirtualList,
private changeDetector: ChangeDetectorRef,
zone: NgZone,
) {
zone.runOutsideAngular(() => {
list.element.nativeElement.addEventListener('scroll', this.detect);
window.addEventListener('resize', this.detect);
});
}
private detect = () => this.changeDetector.detectChanges();
@Input()
set virtualForTemplate(value: TemplateRef<Context<T>>) {
if (value) {
this.template = value;
}
}
ngOnDestroy() {
this.list.element.nativeElement.removeEventListener('scroll', this.detect);
window.removeEventListener('resize', this.detect);
}
ngAfterViewInit() {
setTimeout(this.detect, 0);
}
ngDoCheck(): void {
if (this.forOfDirty) {
this.forOfDirty = false;
const value = this.forOf;
if (!this.differ && value) {
try {
this.differ = this.differs.find(value).create();
} catch {
throw new Error(`Cannot find a differ`);
}
}
}
if (!this.differ && value) {
try {
this.differ = this.differs.find(value).create();
} catch {
throw new Error(`Cannot find a differ`);
}
}
}
const changes = this.differ && this.differ.diff(this.forOf);
const element = this.list.element.nativeElement as HTMLElement;
const itemSize = this.list.itemSize;
const { height } = element.getBoundingClientRect();
const scroll = element.scrollTop;
const first = Math.floor(scroll / itemSize);
const last = first + Math.ceil(height / itemSize);
let scrollChanged = false;
const changes = this.differ && this.differ.diff(this.forOf);
const element = this.list.element.nativeElement as HTMLElement;
const itemSize = this.list.itemSize;
const { height } = element.getBoundingClientRect();
const scroll = element.scrollTop;
const first = Math.floor(scroll / itemSize);
const last = first + Math.ceil(height / itemSize);
let scrollChanged = false;
if (this.first !== first || this.last !== last) {
this.first = first;
this.last = last;
scrollChanged = true;
}
if (this.first !== first || this.last !== last) {
this.first = first;
this.last = last;
scrollChanged = true;
}
if (changes || scrollChanged) {
this.applyChanges();
}
}
private applyChanges() {
const viewContainer = this.viewContainer;
const first = this.first;
const last = this.last;
const forOf = this.forOf;
const actualLast = Math.min(last, forOf.length - 1);
if (changes || scrollChanged) {
this.applyChanges();
}
}
private applyChanges() {
const viewContainer = this.viewContainer;
const first = this.first;
const last = this.last;
const forOf = this.forOf;
const actualLast = Math.min(last, forOf.length - 1);
type Ref = EmbeddedViewRef<Context<T>>;
const insertTuples: { item: T; view: Ref; }[] = [];
const views: Ref[] = [];
type Ref = EmbeddedViewRef<Context<T>>;
const insertTuples: { item: T; view: Ref; }[] = [];
const views: Ref[] = [];
for (let i = viewContainer.length - 1; i >= 0; i--) {
const ref = viewContainer.get(i) as Ref;
for (let i = viewContainer.length - 1; i >= 0; i--) {
const ref = viewContainer.get(i) as Ref;
if (ref.context._currentIndex < first || ref.context._currentIndex > actualLast) {
viewContainer.detach(i);
views.push(ref);
}
}
if (ref.context._currentIndex < first || ref.context._currentIndex > actualLast) {
viewContainer.detach(i);
views.push(ref);
}
}
for (let index = first, i = 0; index <= actualLast; index++ , i++) {
if (viewContainer.length <= i || (viewContainer.get(i) as Ref).context._currentIndex !== index) {
let view = views.pop();
for (let index = first, i = 0; index <= actualLast; index++ , i++) {
if (viewContainer.length <= i || (viewContainer.get(i) as Ref).context._currentIndex !== index) {
let view = views.pop();
if (view) {
view.context.$implicit = null!;
view.context._currentIndex = index;
viewContainer.insert(view, i);
} else {
const context: Context<T> = { $implicit: null!, index: -1, count: -1, _currentIndex: index };
view = viewContainer.createEmbeddedView(this.template, context, i);
}
if (view) {
view.context.$implicit = null!;
view.context._currentIndex = index;
viewContainer.insert(view, i);
} else {
const context: Context<T> = { $implicit: null!, index: -1, count: -1, _currentIndex: index };
view = viewContainer.createEmbeddedView(this.template, context, i);
}
insertTuples.push({ item: forOf[index], view });
}
}
insertTuples.push({ item: forOf[index], view });
}
}
if (DEVELOPMENT && viewContainer.length !== (actualLast - first + 1)) {
console.error('virtual-list: Invalid length', viewContainer.length, first, actualLast);
}
if (DEVELOPMENT && viewContainer.length !== (actualLast - first + 1)) {
console.error('virtual-list: Invalid length', viewContainer.length, first, actualLast);
}
for (const view of views) {
view.destroy();
}
for (const view of views) {
view.destroy();
}
for (let i = 0; i < insertTuples.length; i++) {
insertTuples[i].view.context.$implicit = insertTuples[i].item;
}
for (let i = 0; i < insertTuples.length; i++) {
insertTuples[i].view.context.$implicit = insertTuples[i].item;
}
const count = forOf.length;
const count = forOf.length;
for (let i = 0, ilen = viewContainer.length; i < ilen; i++) {
const viewRef = viewContainer.get(i) as Ref;
viewRef.context.$implicit = forOf[first + i];
viewRef.context.index = first + i;
viewRef.context.count = count;
}
for (let i = 0, ilen = viewContainer.length; i < ilen; i++) {
const viewRef = viewContainer.get(i) as Ref;
viewRef.context.$implicit = forOf[first + i];
viewRef.context.index = first + i;
viewRef.context.count = count;
}
const itemSize = this.list.itemSize;
this.list.padStart.nativeElement.style.height = `${first * itemSize}px`;
this.list.padEnd.nativeElement.style.height = `${(forOf.length - actualLast - 1) * itemSize}px`;
}
const itemSize = this.list.itemSize;
this.list.padStart.nativeElement.style.height = `${first * itemSize}px`;
this.list.padEnd.nativeElement.style.height = `${(forOf.length - actualLast - 1) * itemSize}px`;
}
}
export const virtualListDirectives = [VirtualFor];