mirror of
https://github.com/Terncode/pixel.horse.git
synced 2026-09-24 21:55:52 +02:00
tslint changes
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -12,112 +12,112 @@ let currentPage = 0;
|
||||
let not = false;
|
||||
|
||||
@Component({
|
||||
selector: 'admin-accounts',
|
||||
templateUrl: 'admin-accounts.pug',
|
||||
selector: 'admin-accounts',
|
||||
templateUrl: 'admin-accounts.pug',
|
||||
})
|
||||
export class AdminAccounts implements OnInit {
|
||||
readonly syncIcon = faSync;
|
||||
readonly filterIcon = faFilter;
|
||||
readonly commentIcon = faComment;
|
||||
readonly eraserIcon = faEraser;
|
||||
readonly spinnerIcon = faSpinner;
|
||||
readonly filters = [
|
||||
'all',
|
||||
'banned',
|
||||
'timed out',
|
||||
'with flags',
|
||||
'notes',
|
||||
'supporters',
|
||||
];
|
||||
totalItems = 99999999;
|
||||
itemsOnPageIds: string[] = [];
|
||||
itemsPerPage = 20;
|
||||
loading = false;
|
||||
private expanded = new Set<string>();
|
||||
constructor(public model: AdminModel, private router: Router) {
|
||||
}
|
||||
get showOnly() {
|
||||
return showOnly;
|
||||
}
|
||||
set showOnly(value) {
|
||||
if (showOnly !== value) {
|
||||
showOnly = value;
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
get not() {
|
||||
return not;
|
||||
}
|
||||
set not(value) {
|
||||
if (not !== value) {
|
||||
not = value;
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
get autoRefresh() {
|
||||
return autoRefresh;
|
||||
}
|
||||
set autoRefresh(value) {
|
||||
autoRefresh = value;
|
||||
}
|
||||
get currentPage() {
|
||||
return currentPage;
|
||||
}
|
||||
set currentPage(value) {
|
||||
if (currentPage !== value) {
|
||||
currentPage = value;
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
get search() {
|
||||
return search;
|
||||
}
|
||||
set search(value) {
|
||||
if (search !== value) {
|
||||
search = value;
|
||||
this.execSearch();
|
||||
}
|
||||
}
|
||||
private execSearch = debounce(() => this.refresh(), 500);
|
||||
get duplicateEntries() {
|
||||
return this.model.duplicateEntries;
|
||||
}
|
||||
refreshDuplicates() {
|
||||
this.model.checkDuplicateEntries(true);
|
||||
}
|
||||
limit(account: Account) {
|
||||
return this.expanded.has(account._id) ? 99999 : 2;
|
||||
}
|
||||
expand(account: Account) {
|
||||
this.expanded.add(account._id);
|
||||
}
|
||||
ngOnInit() {
|
||||
this.model.accountPromise
|
||||
.then(() => this.refresh());
|
||||
}
|
||||
refresh(force = false) {
|
||||
this.loading = true;
|
||||
this.model.findAccounts({
|
||||
search: this.search.trim(),
|
||||
not: this.not,
|
||||
showOnly: this.showOnly,
|
||||
page: this.currentPage - 1,
|
||||
itemsPerPage: this.itemsPerPage,
|
||||
force,
|
||||
}).then(result => {
|
||||
if (result && result.page === (this.currentPage - 1)) {
|
||||
this.totalItems = result.totalItems;
|
||||
this.itemsOnPageIds = result.accounts;
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
createAccount() {
|
||||
const name = prompt('enter new account name');
|
||||
readonly syncIcon = faSync;
|
||||
readonly filterIcon = faFilter;
|
||||
readonly commentIcon = faComment;
|
||||
readonly eraserIcon = faEraser;
|
||||
readonly spinnerIcon = faSpinner;
|
||||
readonly filters = [
|
||||
'all',
|
||||
'banned',
|
||||
'timed out',
|
||||
'with flags',
|
||||
'notes',
|
||||
'supporters',
|
||||
];
|
||||
totalItems = 99999999;
|
||||
itemsOnPageIds: string[] = [];
|
||||
itemsPerPage = 20;
|
||||
loading = false;
|
||||
private expanded = new Set<string>();
|
||||
constructor(public model: AdminModel, private router: Router) {
|
||||
}
|
||||
get showOnly() {
|
||||
return showOnly;
|
||||
}
|
||||
set showOnly(value) {
|
||||
if (showOnly !== value) {
|
||||
showOnly = value;
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
get not() {
|
||||
return not;
|
||||
}
|
||||
set not(value) {
|
||||
if (not !== value) {
|
||||
not = value;
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
get autoRefresh() {
|
||||
return autoRefresh;
|
||||
}
|
||||
set autoRefresh(value) {
|
||||
autoRefresh = value;
|
||||
}
|
||||
get currentPage() {
|
||||
return currentPage;
|
||||
}
|
||||
set currentPage(value) {
|
||||
if (currentPage !== value) {
|
||||
currentPage = value;
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
get search() {
|
||||
return search;
|
||||
}
|
||||
set search(value) {
|
||||
if (search !== value) {
|
||||
search = value;
|
||||
this.execSearch();
|
||||
}
|
||||
}
|
||||
private execSearch = debounce(() => this.refresh(), 500);
|
||||
get duplicateEntries() {
|
||||
return this.model.duplicateEntries;
|
||||
}
|
||||
refreshDuplicates() {
|
||||
this.model.checkDuplicateEntries(true);
|
||||
}
|
||||
limit(account: Account) {
|
||||
return this.expanded.has(account._id) ? 99999 : 2;
|
||||
}
|
||||
expand(account: Account) {
|
||||
this.expanded.add(account._id);
|
||||
}
|
||||
ngOnInit() {
|
||||
this.model.accountPromise
|
||||
.then(() => this.refresh());
|
||||
}
|
||||
refresh(force = false) {
|
||||
this.loading = true;
|
||||
this.model.findAccounts({
|
||||
search: this.search.trim(),
|
||||
not: this.not,
|
||||
showOnly: this.showOnly,
|
||||
page: this.currentPage - 1,
|
||||
itemsPerPage: this.itemsPerPage,
|
||||
force,
|
||||
}).then(result => {
|
||||
if (result && result.page === (this.currentPage - 1)) {
|
||||
this.totalItems = result.totalItems;
|
||||
this.itemsOnPageIds = result.accounts;
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
createAccount() {
|
||||
const name = prompt('enter new account name');
|
||||
|
||||
if (name) {
|
||||
this.model.createAccount(name)
|
||||
.then(id => this.router.navigate(['accounts', id]));
|
||||
}
|
||||
}
|
||||
if (name) {
|
||||
this.model.createAccount(name)
|
||||
.then(id => this.router.navigate(['accounts', id]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,95 +5,95 @@ import { AdminModel } from '../../services/adminModel';
|
||||
import { BaseTable, BaseTableState } from '../base-table';
|
||||
import { AdminChatLog } from '../shared/admin-chat-log/admin-chat-log';
|
||||
import {
|
||||
faBell, faSync, faClock, faTrash, faComments, faHdd, faMicrochip, faCertificate, faClone, faPatreon
|
||||
faBell, faSync, faClock, faTrash, faComments, faHdd, faMicrochip, faCertificate, faClone, faPatreon
|
||||
} from '../../../client/icons';
|
||||
|
||||
let state: BaseTableState;
|
||||
|
||||
@Component({
|
||||
selector: 'admin-events',
|
||||
templateUrl: 'admin-events.pug',
|
||||
selector: 'admin-events',
|
||||
templateUrl: 'admin-events.pug',
|
||||
})
|
||||
export class AdminEvents extends BaseTable<Event> implements OnInit, OnDestroy {
|
||||
readonly bellIcon = faBell;
|
||||
readonly syncIcon = faSync;
|
||||
readonly clockIcon = faClock;
|
||||
readonly trashIcon = faTrash;
|
||||
readonly commentsIcon = faComments;
|
||||
readonly hddIcon = faHdd;
|
||||
readonly ramIcon = faMicrochip;
|
||||
readonly certificateIcon = faCertificate;
|
||||
readonly duplicateIcon = faClone;
|
||||
readonly patreonIcon = faPatreon;
|
||||
@ViewChild('chatLog', { static: true }) chatLog!: AdminChatLog;
|
||||
private chatEvent?: Event;
|
||||
constructor(private model: AdminModel) {
|
||||
super();
|
||||
}
|
||||
get status() {
|
||||
return this.model.state.status;
|
||||
}
|
||||
get isLowDiskSpace() {
|
||||
return this.model.isLowDiskSpace;
|
||||
}
|
||||
get isLowMemory() {
|
||||
return this.model.isLowMemory;
|
||||
}
|
||||
get isOldCertificate() {
|
||||
return this.model.isOldCertificate;
|
||||
}
|
||||
get isOldPatreon() {
|
||||
return this.model.isOldPatreon;
|
||||
}
|
||||
get items() {
|
||||
return this.model.events;
|
||||
}
|
||||
get duplicateEntries() {
|
||||
return this.model.duplicateEntries;
|
||||
}
|
||||
get notifications() {
|
||||
return this.model.notifications;
|
||||
}
|
||||
ngOnInit() {
|
||||
this.model.updated = () => this.updateItems();
|
||||
this.setState(state);
|
||||
this.updateItems();
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.model.updated = () => { };
|
||||
}
|
||||
cleanupDeleted() {
|
||||
this.model.cleanupDeletedEvents();
|
||||
}
|
||||
refreshDuplicates() {
|
||||
this.model.checkDuplicateEntries(true);
|
||||
}
|
||||
removeEvents(olderThan: number) {
|
||||
const date = fromNow(-olderThan);
|
||||
const oldEvents = this.items.filter(e => e.updatedAt.getTime() < date.getTime());
|
||||
return Promise.all(oldEvents.map(e => this.model.removeEvent(e._id).then(() => this.removedEvent(e))));
|
||||
}
|
||||
showChat(e?: ChatEvent) {
|
||||
this.chatEvent = e && e.event;
|
||||
this.chatLog.show(e && e.account);
|
||||
}
|
||||
addChat(e: ChatEvent) {
|
||||
if (e.account) {
|
||||
this.chatLog.add(e.account);
|
||||
}
|
||||
}
|
||||
removedEvent(e: Event) {
|
||||
if (this.chatEvent === e) {
|
||||
this.chatLog.close();
|
||||
}
|
||||
}
|
||||
toggleNotifications() {
|
||||
this.model.toggleNotifications();
|
||||
}
|
||||
protected onChange() {
|
||||
state = this.getState();
|
||||
}
|
||||
protected updatePage() {
|
||||
super.updatePage();
|
||||
}
|
||||
readonly bellIcon = faBell;
|
||||
readonly syncIcon = faSync;
|
||||
readonly clockIcon = faClock;
|
||||
readonly trashIcon = faTrash;
|
||||
readonly commentsIcon = faComments;
|
||||
readonly hddIcon = faHdd;
|
||||
readonly ramIcon = faMicrochip;
|
||||
readonly certificateIcon = faCertificate;
|
||||
readonly duplicateIcon = faClone;
|
||||
readonly patreonIcon = faPatreon;
|
||||
@ViewChild('chatLog', { static: true }) chatLog!: AdminChatLog;
|
||||
private chatEvent?: Event;
|
||||
constructor(private model: AdminModel) {
|
||||
super();
|
||||
}
|
||||
get status() {
|
||||
return this.model.state.status;
|
||||
}
|
||||
get isLowDiskSpace() {
|
||||
return this.model.isLowDiskSpace;
|
||||
}
|
||||
get isLowMemory() {
|
||||
return this.model.isLowMemory;
|
||||
}
|
||||
get isOldCertificate() {
|
||||
return this.model.isOldCertificate;
|
||||
}
|
||||
get isOldPatreon() {
|
||||
return this.model.isOldPatreon;
|
||||
}
|
||||
get items() {
|
||||
return this.model.events;
|
||||
}
|
||||
get duplicateEntries() {
|
||||
return this.model.duplicateEntries;
|
||||
}
|
||||
get notifications() {
|
||||
return this.model.notifications;
|
||||
}
|
||||
ngOnInit() {
|
||||
this.model.updated = () => this.updateItems();
|
||||
this.setState(state);
|
||||
this.updateItems();
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.model.updated = () => { };
|
||||
}
|
||||
cleanupDeleted() {
|
||||
this.model.cleanupDeletedEvents();
|
||||
}
|
||||
refreshDuplicates() {
|
||||
this.model.checkDuplicateEntries(true);
|
||||
}
|
||||
removeEvents(olderThan: number) {
|
||||
const date = fromNow(-olderThan);
|
||||
const oldEvents = this.items.filter(e => e.updatedAt.getTime() < date.getTime());
|
||||
return Promise.all(oldEvents.map(e => this.model.removeEvent(e._id).then(() => this.removedEvent(e))));
|
||||
}
|
||||
showChat(e?: ChatEvent) {
|
||||
this.chatEvent = e && e.event;
|
||||
this.chatLog.show(e && e.account);
|
||||
}
|
||||
addChat(e: ChatEvent) {
|
||||
if (e.account) {
|
||||
this.chatLog.add(e.account);
|
||||
}
|
||||
}
|
||||
removedEvent(e: Event) {
|
||||
if (this.chatEvent === e) {
|
||||
this.chatLog.close();
|
||||
}
|
||||
}
|
||||
toggleNotifications() {
|
||||
this.model.toggleNotifications();
|
||||
}
|
||||
protected onChange() {
|
||||
state = this.getState();
|
||||
}
|
||||
protected updatePage() {
|
||||
super.updatePage();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,33 +4,33 @@ import { Event } from '../../../common/adminInterfaces';
|
||||
import { AdminModel } from '../../services/adminModel';
|
||||
|
||||
@Component({
|
||||
selector: 'admin-origin-details',
|
||||
templateUrl: 'admin-origin-details.pug',
|
||||
selector: 'admin-origin-details',
|
||||
templateUrl: 'admin-origin-details.pug',
|
||||
})
|
||||
export class AdminOriginDetails implements OnInit {
|
||||
events?: Event[];
|
||||
accounts: string[] = [];
|
||||
ip?: string;
|
||||
constructor(private route: ActivatedRoute, private model: AdminModel) {
|
||||
}
|
||||
get whoisHref() {
|
||||
return `http://whois.urih.com/record/${this.ip}/`;
|
||||
}
|
||||
ngOnInit() {
|
||||
this.route.params.forEach(p => {
|
||||
this.ip = p['ip'];
|
||||
this.update();
|
||||
});
|
||||
}
|
||||
private update() {
|
||||
this.accounts = [];
|
||||
events?: Event[];
|
||||
accounts: string[] = [];
|
||||
ip?: string;
|
||||
constructor(private route: ActivatedRoute, private model: AdminModel) {
|
||||
}
|
||||
get whoisHref() {
|
||||
return `http://whois.urih.com/record/${this.ip}/`;
|
||||
}
|
||||
ngOnInit() {
|
||||
this.route.params.forEach(p => {
|
||||
this.ip = p['ip'];
|
||||
this.update();
|
||||
});
|
||||
}
|
||||
private update() {
|
||||
this.accounts = [];
|
||||
|
||||
if (this.model.connected && this.ip) {
|
||||
this.model.getAccountsByOrigin(this.ip)
|
||||
.then(accounts => this.accounts = accounts || []);
|
||||
this.events = this.model.events
|
||||
.filter(e => e.origin && e.origin.ip === this.ip)
|
||||
.slice(0, 20);
|
||||
}
|
||||
}
|
||||
if (this.model.connected && this.ip) {
|
||||
this.model.getAccountsByOrigin(this.ip)
|
||||
.then(accounts => this.accounts = accounts || []);
|
||||
this.events = this.model.events
|
||||
.filter(e => e.origin && e.origin.ip === this.ip)
|
||||
.slice(0, 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,50 +4,50 @@ import { faSync, faEraser, faClock, faUser, faChevronDown, faSpinner } from '../
|
||||
import { AdminModel } from '../../services/adminModel';
|
||||
|
||||
@Component({
|
||||
selector: 'admin-origins',
|
||||
templateUrl: 'admin-origins.pug',
|
||||
selector: 'admin-origins',
|
||||
templateUrl: 'admin-origins.pug',
|
||||
})
|
||||
export class AdminOrigins implements OnInit {
|
||||
readonly syncIcon = faSync;
|
||||
readonly eraserIcon = faEraser;
|
||||
readonly clockIcon = faClock;
|
||||
readonly userIcon = faUser;
|
||||
readonly chevronDownIcon = faChevronDown;
|
||||
readonly spinnerIcon = faSpinner;
|
||||
stats?: OriginStats;
|
||||
other?: OtherStats;
|
||||
requestStats: RequestStats[] = [];
|
||||
pending = false;
|
||||
constructor(public model: AdminModel) {
|
||||
}
|
||||
ngOnInit() {
|
||||
if (this.model.connected) {
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
update() {
|
||||
this.model.getOriginStats().then(stats => this.stats = stats);
|
||||
this.model.getOtherStats().then(stats => this.other = stats);
|
||||
}
|
||||
clear(count: number, andHigher = false) {
|
||||
this.clearAll(count, andHigher, true, true, false);
|
||||
}
|
||||
clearOld(count: number) {
|
||||
this.clearAll(count, true, true, false, false);
|
||||
}
|
||||
clearSingles(count: number) {
|
||||
this.clearAll(count, true, false, true, false);
|
||||
}
|
||||
clearTo10(count: number) {
|
||||
this.clearAll(count, true, false, true, true);
|
||||
}
|
||||
clearAll(count: number, andHigher: boolean, old: boolean, singles: boolean, trim: boolean) {
|
||||
this.pending = true;
|
||||
readonly syncIcon = faSync;
|
||||
readonly eraserIcon = faEraser;
|
||||
readonly clockIcon = faClock;
|
||||
readonly userIcon = faUser;
|
||||
readonly chevronDownIcon = faChevronDown;
|
||||
readonly spinnerIcon = faSpinner;
|
||||
stats?: OriginStats;
|
||||
other?: OtherStats;
|
||||
requestStats: RequestStats[] = [];
|
||||
pending = false;
|
||||
constructor(public model: AdminModel) {
|
||||
}
|
||||
ngOnInit() {
|
||||
if (this.model.connected) {
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
update() {
|
||||
this.model.getOriginStats().then(stats => this.stats = stats);
|
||||
this.model.getOtherStats().then(stats => this.other = stats);
|
||||
}
|
||||
clear(count: number, andHigher = false) {
|
||||
this.clearAll(count, andHigher, true, true, false);
|
||||
}
|
||||
clearOld(count: number) {
|
||||
this.clearAll(count, true, true, false, false);
|
||||
}
|
||||
clearSingles(count: number) {
|
||||
this.clearAll(count, true, false, true, false);
|
||||
}
|
||||
clearTo10(count: number) {
|
||||
this.clearAll(count, true, false, true, true);
|
||||
}
|
||||
clearAll(count: number, andHigher: boolean, old: boolean, singles: boolean, trim: boolean) {
|
||||
this.pending = true;
|
||||
|
||||
return this.model.clearOrigins(count, andHigher, { old, singles, trim })
|
||||
.finally(() => {
|
||||
this.pending = false;
|
||||
this.update();
|
||||
});
|
||||
}
|
||||
return this.model.clearOrigins(count, andHigher, { old, singles, trim })
|
||||
.finally(() => {
|
||||
this.pending = false;
|
||||
this.update();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,85 +6,85 @@ import { Subscription } from '../../../common/interfaces';
|
||||
import { showTextInNewTab } from '../../../client/htmlUtils';
|
||||
|
||||
interface Field {
|
||||
key: keyof GeneralSettings;
|
||||
title: string;
|
||||
value: string | undefined;
|
||||
key: keyof GeneralSettings;
|
||||
title: string;
|
||||
value: string | undefined;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'admin-other',
|
||||
templateUrl: 'admin-other.pug',
|
||||
selector: 'admin-other',
|
||||
templateUrl: 'admin-other.pug',
|
||||
})
|
||||
export class AdminOther implements OnInit, OnDestroy {
|
||||
fields: Field[] = [
|
||||
{ key: 'suspiciousNames', title: 'Suspicious pony names & emails to report', value: undefined },
|
||||
{ key: 'suspiciousAuths', title: 'Suspicious auths to report', value: undefined },
|
||||
{ key: 'suspiciousMessages', title: 'Suspicious messages to report (instant)', value: undefined },
|
||||
{ key: 'suspiciousSafeMessages', title: 'Suspicious messages to report (safe only) (5+)', value: undefined },
|
||||
{ key: 'suspiciousSafeWholeMessages', title: 'Suspicious messages to report (safe only) (5+) (whole words)', value: undefined },
|
||||
{ key: 'suspiciousSafeInstantMessages', title: 'Suspicious messages to report (safe only) (instant)', value: undefined },
|
||||
{
|
||||
key: 'suspiciousSafeInstantWholeMessages',
|
||||
title: 'Suspicious messages to report (safe only) (whole words) (instant)',
|
||||
value: undefined
|
||||
},
|
||||
];
|
||||
max = 100;
|
||||
value = 0;
|
||||
error?: string;
|
||||
succeeded = false;
|
||||
suspiciousPonies?: string;
|
||||
suspiciousPoniesError?: string;
|
||||
ignoreErrors?: string;
|
||||
account: Account | undefined;
|
||||
private subscription?: Subscription;
|
||||
constructor(public model: AdminModel) {
|
||||
}
|
||||
ngOnInit() {
|
||||
this.model.accountPromise.then(() => {
|
||||
this.resetFields();
|
||||
this.resetSuspiciousPonies();
|
||||
});
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
saveFields() {
|
||||
let settings: any = {};
|
||||
this.fields.forEach(field => settings[field.key] = field.value);
|
||||
this.model.updateSettings(settings);
|
||||
}
|
||||
resetFields() {
|
||||
this.fields.map(field => {
|
||||
field.value = this.model.state.loginServers[0][field.key] as string | undefined;
|
||||
});
|
||||
}
|
||||
saveSuspiciousPonies() {
|
||||
this.suspiciousPoniesError = undefined;
|
||||
fields: Field[] = [
|
||||
{ key: 'suspiciousNames', title: 'Suspicious pony names & emails to report', value: undefined },
|
||||
{ key: 'suspiciousAuths', title: 'Suspicious auths to report', value: undefined },
|
||||
{ key: 'suspiciousMessages', title: 'Suspicious messages to report (instant)', value: undefined },
|
||||
{ key: 'suspiciousSafeMessages', title: 'Suspicious messages to report (safe only) (5+)', value: undefined },
|
||||
{ key: 'suspiciousSafeWholeMessages', title: 'Suspicious messages to report (safe only) (5+) (whole words)', value: undefined },
|
||||
{ key: 'suspiciousSafeInstantMessages', title: 'Suspicious messages to report (safe only) (instant)', value: undefined },
|
||||
{
|
||||
key: 'suspiciousSafeInstantWholeMessages',
|
||||
title: 'Suspicious messages to report (safe only) (whole words) (instant)',
|
||||
value: undefined
|
||||
},
|
||||
];
|
||||
max = 100;
|
||||
value = 0;
|
||||
error?: string;
|
||||
succeeded = false;
|
||||
suspiciousPonies?: string;
|
||||
suspiciousPoniesError?: string;
|
||||
ignoreErrors?: string;
|
||||
account: Account | undefined;
|
||||
private subscription?: Subscription;
|
||||
constructor(public model: AdminModel) {
|
||||
}
|
||||
ngOnInit() {
|
||||
this.model.accountPromise.then(() => {
|
||||
this.resetFields();
|
||||
this.resetSuspiciousPonies();
|
||||
});
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
saveFields() {
|
||||
let settings: any = {};
|
||||
this.fields.forEach(field => settings[field.key] = field.value);
|
||||
this.model.updateSettings(settings);
|
||||
}
|
||||
resetFields() {
|
||||
this.fields.map(field => {
|
||||
field.value = this.model.state.loginServers[0][field.key] as string | undefined;
|
||||
});
|
||||
}
|
||||
saveSuspiciousPonies() {
|
||||
this.suspiciousPoniesError = undefined;
|
||||
|
||||
try {
|
||||
compact(this.suspiciousPonies!.split(/\n/g).map(x => x.trim())).map(x => JSON.parse(x));
|
||||
this.model.updateSettings({ suspiciousPonies: this.suspiciousPonies });
|
||||
} catch (e) {
|
||||
this.suspiciousPoniesError = e.message;
|
||||
}
|
||||
}
|
||||
resetSuspiciousPonies() {
|
||||
this.suspiciousPonies = this.model.state.loginServers[0].suspiciousPonies;
|
||||
}
|
||||
updatePatreon() {
|
||||
this.model.server.updatePatreon();
|
||||
}
|
||||
updatePatreonToken(patreonToken: string) {
|
||||
this.model.updateSettings({ patreonToken });
|
||||
}
|
||||
getLastPatreonData() {
|
||||
this.model.getLastPatreonData()
|
||||
.then(data => {
|
||||
showTextInNewTab(JSON.stringify(data, null, 2));
|
||||
});
|
||||
}
|
||||
updatePastSupporters() {
|
||||
this.model.updatePastSupporters();
|
||||
}
|
||||
try {
|
||||
compact(this.suspiciousPonies!.split(/\n/g).map(x => x.trim())).map(x => JSON.parse(x));
|
||||
this.model.updateSettings({ suspiciousPonies: this.suspiciousPonies });
|
||||
} catch (e) {
|
||||
this.suspiciousPoniesError = e.message;
|
||||
}
|
||||
}
|
||||
resetSuspiciousPonies() {
|
||||
this.suspiciousPonies = this.model.state.loginServers[0].suspiciousPonies;
|
||||
}
|
||||
updatePatreon() {
|
||||
this.model.server.updatePatreon();
|
||||
}
|
||||
updatePatreonToken(patreonToken: string) {
|
||||
this.model.updateSettings({ patreonToken });
|
||||
}
|
||||
getLastPatreonData() {
|
||||
this.model.getLastPatreonData()
|
||||
.then(data => {
|
||||
showTextInNewTab(JSON.stringify(data, null, 2));
|
||||
});
|
||||
}
|
||||
updatePastSupporters() {
|
||||
this.model.updatePastSupporters();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,89 +9,89 @@ let currentPage = 1;
|
||||
let query: FindPonyQuery = {};
|
||||
|
||||
@Component({
|
||||
selector: 'admin-ponies',
|
||||
templateUrl: 'admin-ponies.pug',
|
||||
styleUrls: ['admin-ponies.scss'],
|
||||
selector: 'admin-ponies',
|
||||
templateUrl: 'admin-ponies.pug',
|
||||
styleUrls: ['admin-ponies.scss'],
|
||||
})
|
||||
export class AdminPonies implements OnInit {
|
||||
readonly syncIcon = faSync;
|
||||
readonly filterIcon = faFilter;
|
||||
readonly spinnerIcon = faSpinner;
|
||||
readonly trashIcon = faTrash;
|
||||
readonly commentIcon = faComment;
|
||||
items?: string[];
|
||||
itemsPerPage = 20;
|
||||
query: FindPonyQuery = {};
|
||||
loading = false;
|
||||
private lastQuery: FindPonyQuery = {};
|
||||
private totalCount?: number;
|
||||
private execSearch = debounce(() => this.fetchPonies(false), 1000);
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
get totalItems() {
|
||||
return this.totalCount === undefined ? this.model.counts.characters : this.totalCount;
|
||||
}
|
||||
get search() {
|
||||
return query.search;
|
||||
}
|
||||
set search(value: string | undefined) {
|
||||
if (query.search !== value) {
|
||||
query.search = value;
|
||||
this.execSearch();
|
||||
}
|
||||
}
|
||||
get orderBy() {
|
||||
return query.orderBy;
|
||||
}
|
||||
set orderBy(value: string | undefined) {
|
||||
if (query.orderBy !== value) {
|
||||
query.orderBy = value;
|
||||
this.execSearch();
|
||||
}
|
||||
}
|
||||
get currentPage() {
|
||||
return currentPage;
|
||||
}
|
||||
set currentPage(value) {
|
||||
if (currentPage !== value) {
|
||||
currentPage = value;
|
||||
this.fetchPonies(isEqual(this.lastQuery, query));
|
||||
}
|
||||
}
|
||||
ngOnInit() {
|
||||
if (this.model.connected) {
|
||||
this.fetchPonies(false);
|
||||
}
|
||||
}
|
||||
refresh() {
|
||||
this.fetchPonies(false);
|
||||
}
|
||||
remove(pony: Character) {
|
||||
if (confirm('Are you sure?')) {
|
||||
this.model.removePony(pony._id)
|
||||
.then(() => delay(500))
|
||||
.then(() => this.refresh());
|
||||
}
|
||||
}
|
||||
private fetchPonies(skipTotalCount: boolean) {
|
||||
const thisQuery = cloneDeep(query);
|
||||
this.lastQuery = cloneDeep(query);
|
||||
this.loading = true;
|
||||
readonly syncIcon = faSync;
|
||||
readonly filterIcon = faFilter;
|
||||
readonly spinnerIcon = faSpinner;
|
||||
readonly trashIcon = faTrash;
|
||||
readonly commentIcon = faComment;
|
||||
items?: string[];
|
||||
itemsPerPage = 20;
|
||||
query: FindPonyQuery = {};
|
||||
loading = false;
|
||||
private lastQuery: FindPonyQuery = {};
|
||||
private totalCount?: number;
|
||||
private execSearch = debounce(() => this.fetchPonies(false), 1000);
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
get totalItems() {
|
||||
return this.totalCount === undefined ? this.model.counts.characters : this.totalCount;
|
||||
}
|
||||
get search() {
|
||||
return query.search;
|
||||
}
|
||||
set search(value: string | undefined) {
|
||||
if (query.search !== value) {
|
||||
query.search = value;
|
||||
this.execSearch();
|
||||
}
|
||||
}
|
||||
get orderBy() {
|
||||
return query.orderBy;
|
||||
}
|
||||
set orderBy(value: string | undefined) {
|
||||
if (query.orderBy !== value) {
|
||||
query.orderBy = value;
|
||||
this.execSearch();
|
||||
}
|
||||
}
|
||||
get currentPage() {
|
||||
return currentPage;
|
||||
}
|
||||
set currentPage(value) {
|
||||
if (currentPage !== value) {
|
||||
currentPage = value;
|
||||
this.fetchPonies(isEqual(this.lastQuery, query));
|
||||
}
|
||||
}
|
||||
ngOnInit() {
|
||||
if (this.model.connected) {
|
||||
this.fetchPonies(false);
|
||||
}
|
||||
}
|
||||
refresh() {
|
||||
this.fetchPonies(false);
|
||||
}
|
||||
remove(pony: Character) {
|
||||
if (confirm('Are you sure?')) {
|
||||
this.model.removePony(pony._id)
|
||||
.then(() => delay(500))
|
||||
.then(() => this.refresh());
|
||||
}
|
||||
}
|
||||
private fetchPonies(skipTotalCount: boolean) {
|
||||
const thisQuery = cloneDeep(query);
|
||||
this.lastQuery = cloneDeep(query);
|
||||
this.loading = true;
|
||||
|
||||
thisQuery.search = thisQuery.search && thisQuery.search.trim();
|
||||
thisQuery.search = thisQuery.search && thisQuery.search.trim();
|
||||
|
||||
this.model.findPonies(thisQuery, this.currentPage - 1, skipTotalCount)
|
||||
.then(result => {
|
||||
if (result) {
|
||||
if (!skipTotalCount) {
|
||||
this.totalCount = result.totalCount;
|
||||
}
|
||||
this.model.findPonies(thisQuery, this.currentPage - 1, skipTotalCount)
|
||||
.then(result => {
|
||||
if (result) {
|
||||
if (!skipTotalCount) {
|
||||
this.totalCount = result.totalCount;
|
||||
}
|
||||
|
||||
this.items = result.items;
|
||||
} else {
|
||||
this.items = [];
|
||||
}
|
||||
})
|
||||
.finally(() => this.loading = false);
|
||||
}
|
||||
this.items = result.items;
|
||||
} else {
|
||||
this.items = [];
|
||||
}
|
||||
})
|
||||
.finally(() => this.loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,286 +6,286 @@ import { SERVER_FPS } from '../../../../common/constants';
|
||||
import { AgDragEvent } from '../../../shared/directives/agDrag';
|
||||
|
||||
interface Tooltip {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
text: string;
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface ListingEntry {
|
||||
name: string;
|
||||
count: number;
|
||||
selfTime: number;
|
||||
totalTime: number;
|
||||
selfPercent: number;
|
||||
totalPercent: number;
|
||||
name: string;
|
||||
count: number;
|
||||
selfTime: number;
|
||||
totalTime: number;
|
||||
selfPercent: number;
|
||||
totalPercent: number;
|
||||
}
|
||||
|
||||
const frameTime = 1000 / SERVER_FPS;
|
||||
const timePadding = 10; // ms
|
||||
|
||||
@Component({
|
||||
selector: 'admin-reports-perf',
|
||||
templateUrl: 'admin-reports-perf.pug',
|
||||
selector: 'admin-reports-perf',
|
||||
templateUrl: 'admin-reports-perf.pug',
|
||||
})
|
||||
export class AdminReportsPerf {
|
||||
@ViewChild('container', { static: true }) container!: ElementRef;
|
||||
@ViewChild('canvas', { static: true }) canvas!: ElementRef;
|
||||
@ViewChild('tooltip', { static: true }) tooltip!: ElementRef;
|
||||
loaded = false;
|
||||
server = '';
|
||||
startTime = 0;
|
||||
endTime = 0;
|
||||
listing: ListingEntry[] = [];
|
||||
timings: TimingEntry[] = [];
|
||||
private tooltips: Tooltip[] = [];
|
||||
private startTimeFrom = 0;
|
||||
private endTimeFrom = 0;
|
||||
private frame: any = 0;
|
||||
private lastZoom = 0;
|
||||
constructor(private model: AdminModel) {
|
||||
if (DEVELOPMENT) {
|
||||
const interval = setInterval(() => {
|
||||
if (findById(this.model.state.gameServers, 'dev')) {
|
||||
clearInterval(interval);
|
||||
this.load('dev');
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
get servers() {
|
||||
return this.model.state.gameServers.map(s => s.id);
|
||||
}
|
||||
async load(server: string) {
|
||||
this.server = server;
|
||||
this.timings = [];
|
||||
this.loaded = false;
|
||||
const result = await this.model.getTimings(server);
|
||||
@ViewChild('container', { static: true }) container!: ElementRef;
|
||||
@ViewChild('canvas', { static: true }) canvas!: ElementRef;
|
||||
@ViewChild('tooltip', { static: true }) tooltip!: ElementRef;
|
||||
loaded = false;
|
||||
server = '';
|
||||
startTime = 0;
|
||||
endTime = 0;
|
||||
listing: ListingEntry[] = [];
|
||||
timings: TimingEntry[] = [];
|
||||
private tooltips: Tooltip[] = [];
|
||||
private startTimeFrom = 0;
|
||||
private endTimeFrom = 0;
|
||||
private frame: any = 0;
|
||||
private lastZoom = 0;
|
||||
constructor(private model: AdminModel) {
|
||||
if (DEVELOPMENT) {
|
||||
const interval = setInterval(() => {
|
||||
if (findById(this.model.state.gameServers, 'dev')) {
|
||||
clearInterval(interval);
|
||||
this.load('dev');
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
get servers() {
|
||||
return this.model.state.gameServers.map(s => s.id);
|
||||
}
|
||||
async load(server: string) {
|
||||
this.server = server;
|
||||
this.timings = [];
|
||||
this.loaded = false;
|
||||
const result = await this.model.getTimings(server);
|
||||
|
||||
if (result) {
|
||||
this.loaded = true;
|
||||
this.timings = result as TimingEntry[];
|
||||
this.setupZoom();
|
||||
this.recalcListing();
|
||||
}
|
||||
if (result) {
|
||||
this.loaded = true;
|
||||
this.timings = result as TimingEntry[];
|
||||
this.setupZoom();
|
||||
this.recalcListing();
|
||||
}
|
||||
|
||||
this.redraw();
|
||||
}
|
||||
mouseMove(e: MouseEvent) {
|
||||
const rect = this.container.nativeElement.getBoundingClientRect() as DOMRect;
|
||||
const x = e.pageX - rect.left;
|
||||
const y = e.pageY - rect.top;
|
||||
this.redraw();
|
||||
}
|
||||
mouseMove(e: MouseEvent) {
|
||||
const rect = this.container.nativeElement.getBoundingClientRect() as DOMRect;
|
||||
const x = e.pageX - rect.left;
|
||||
const y = e.pageY - rect.top;
|
||||
|
||||
const tooltip = this.tooltips.find(t => pointInRect(x, y, t));
|
||||
const element = this.tooltip.nativeElement as HTMLElement;
|
||||
const tooltip = this.tooltips.find(t => pointInRect(x, y, t));
|
||||
const element = this.tooltip.nativeElement as HTMLElement;
|
||||
|
||||
if (tooltip) {
|
||||
element.style.display = 'block';
|
||||
element.style.left = `${x + 10}px`;
|
||||
element.style.top = `${y + 10}px`;
|
||||
element.innerText = tooltip.text;
|
||||
} else {
|
||||
element.style.display = 'none';
|
||||
}
|
||||
}
|
||||
wheel(e: WheelEvent) {
|
||||
e.preventDefault();
|
||||
if (tooltip) {
|
||||
element.style.display = 'block';
|
||||
element.style.left = `${x + 10}px`;
|
||||
element.style.top = `${y + 10}px`;
|
||||
element.innerText = tooltip.text;
|
||||
} else {
|
||||
element.style.display = 'none';
|
||||
}
|
||||
}
|
||||
wheel(e: WheelEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
const deltaY = clamp(e.deltaY, -1, 1);
|
||||
const change = ((this.endTime - this.startTime) * 0.2 * deltaY);
|
||||
const rect = this.container.nativeElement.getBoundingClientRect() as DOMRect;
|
||||
const ratioFromLeft = (e.pageX - rect.left) / rect.width;
|
||||
const deltaY = clamp(e.deltaY, -1, 1);
|
||||
const change = ((this.endTime - this.startTime) * 0.2 * deltaY);
|
||||
const rect = this.container.nativeElement.getBoundingClientRect() as DOMRect;
|
||||
const ratioFromLeft = (e.pageX - rect.left) / rect.width;
|
||||
|
||||
this.startTime = this.startTime - change * ratioFromLeft;
|
||||
this.endTime = this.endTime + change * (1 - ratioFromLeft);
|
||||
this.startTime = this.startTime - change * ratioFromLeft;
|
||||
this.endTime = this.endTime + change * (1 - ratioFromLeft);
|
||||
|
||||
this.redraw();
|
||||
}
|
||||
setupZoom() {
|
||||
switch (this.lastZoom) {
|
||||
case 0:
|
||||
default:
|
||||
this.resetZoom();
|
||||
break;
|
||||
case 1:
|
||||
this.fitZoom();
|
||||
break;
|
||||
case 2:
|
||||
this.fullFrameZoom();
|
||||
break;
|
||||
}
|
||||
}
|
||||
resetZoom() {
|
||||
const firstTime = this.timings[0].time;
|
||||
const lastTime = this.timings[this.timings.length - 1].time;
|
||||
this.startTime = firstTime - timePadding;
|
||||
this.endTime = lastTime + timePadding;
|
||||
this.redraw();
|
||||
this.lastZoom = 0;
|
||||
}
|
||||
fitZoom() {
|
||||
const firstTime = this.timings[0].time;
|
||||
const lastTime = this.timings[this.timings.length - 1].time;
|
||||
const totalTime = lastTime - firstTime;
|
||||
this.startTime = firstTime - totalTime * 0.05;
|
||||
this.endTime = lastTime + totalTime * 0.05;
|
||||
this.redraw();
|
||||
this.lastZoom = 1;
|
||||
}
|
||||
fullFrameZoom() {
|
||||
const firstTime = this.timings[0].time;
|
||||
const lastTime = firstTime + frameTime;
|
||||
this.startTime = firstTime - 2;
|
||||
this.endTime = lastTime + 2;
|
||||
this.redraw();
|
||||
this.lastZoom = 2;
|
||||
}
|
||||
drag(e: AgDragEvent) {
|
||||
if (e.type === 'start') {
|
||||
this.startTimeFrom = this.startTime;
|
||||
this.endTimeFrom = this.endTime;
|
||||
}
|
||||
this.redraw();
|
||||
}
|
||||
setupZoom() {
|
||||
switch (this.lastZoom) {
|
||||
case 0:
|
||||
default:
|
||||
this.resetZoom();
|
||||
break;
|
||||
case 1:
|
||||
this.fitZoom();
|
||||
break;
|
||||
case 2:
|
||||
this.fullFrameZoom();
|
||||
break;
|
||||
}
|
||||
}
|
||||
resetZoom() {
|
||||
const firstTime = this.timings[0].time;
|
||||
const lastTime = this.timings[this.timings.length - 1].time;
|
||||
this.startTime = firstTime - timePadding;
|
||||
this.endTime = lastTime + timePadding;
|
||||
this.redraw();
|
||||
this.lastZoom = 0;
|
||||
}
|
||||
fitZoom() {
|
||||
const firstTime = this.timings[0].time;
|
||||
const lastTime = this.timings[this.timings.length - 1].time;
|
||||
const totalTime = lastTime - firstTime;
|
||||
this.startTime = firstTime - totalTime * 0.05;
|
||||
this.endTime = lastTime + totalTime * 0.05;
|
||||
this.redraw();
|
||||
this.lastZoom = 1;
|
||||
}
|
||||
fullFrameZoom() {
|
||||
const firstTime = this.timings[0].time;
|
||||
const lastTime = firstTime + frameTime;
|
||||
this.startTime = firstTime - 2;
|
||||
this.endTime = lastTime + 2;
|
||||
this.redraw();
|
||||
this.lastZoom = 2;
|
||||
}
|
||||
drag(e: AgDragEvent) {
|
||||
if (e.type === 'start') {
|
||||
this.startTimeFrom = this.startTime;
|
||||
this.endTimeFrom = this.endTime;
|
||||
}
|
||||
|
||||
const scale = (this.endTime - this.startTime) / this.canvas.nativeElement.width;
|
||||
this.startTime = this.startTimeFrom - e.dx * scale;
|
||||
this.endTime = this.endTimeFrom - e.dx * scale;
|
||||
this.redraw();
|
||||
}
|
||||
redraw() {
|
||||
this.frame = this.frame || requestAnimationFrame(() => this.draw());
|
||||
}
|
||||
draw() {
|
||||
this.frame = 0;
|
||||
const scale = (this.endTime - this.startTime) / this.canvas.nativeElement.width;
|
||||
this.startTime = this.startTimeFrom - e.dx * scale;
|
||||
this.endTime = this.endTimeFrom - e.dx * scale;
|
||||
this.redraw();
|
||||
}
|
||||
redraw() {
|
||||
this.frame = this.frame || requestAnimationFrame(() => this.draw());
|
||||
}
|
||||
draw() {
|
||||
this.frame = 0;
|
||||
|
||||
const rect = this.container.nativeElement.getBoundingClientRect() as DOMRect;
|
||||
const canvas = this.canvas.nativeElement as HTMLCanvasElement;
|
||||
canvas.width = rect.width;
|
||||
canvas.height = 400;
|
||||
const rect = this.container.nativeElement.getBoundingClientRect() as DOMRect;
|
||||
const canvas = this.canvas.nativeElement as HTMLCanvasElement;
|
||||
canvas.width = rect.width;
|
||||
canvas.height = 400;
|
||||
|
||||
const context = canvas.getContext('2d')!;
|
||||
context.fillStyle = '#222';
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
const context = canvas.getContext('2d')!;
|
||||
context.fillStyle = '#222';
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
this.tooltips.length = 0;
|
||||
this.tooltips.length = 0;
|
||||
|
||||
if (!this.timings.length)
|
||||
return;
|
||||
if (!this.timings.length)
|
||||
return;
|
||||
|
||||
const firstTime = this.timings[0].time;
|
||||
const startTime = this.startTime;
|
||||
const endTime = this.endTime;
|
||||
const totalTime = endTime - startTime;
|
||||
const timeScale = canvas.width / totalTime;
|
||||
const firstTime = this.timings[0].time;
|
||||
const startTime = this.startTime;
|
||||
const endTime = this.endTime;
|
||||
const totalTime = endTime - startTime;
|
||||
const timeScale = canvas.width / totalTime;
|
||||
|
||||
function timeToX(time: number) {
|
||||
return (time - startTime) * timeScale;
|
||||
}
|
||||
function timeToX(time: number) {
|
||||
return (time - startTime) * timeScale;
|
||||
}
|
||||
|
||||
function timeToXAligned(time: number) {
|
||||
return Math.floor(timeToX(time)) + 0.5;
|
||||
}
|
||||
function timeToXAligned(time: number) {
|
||||
return Math.floor(timeToX(time)) + 0.5;
|
||||
}
|
||||
|
||||
context.textBaseline = 'middle';
|
||||
context.font = 'Arial 14px normal';
|
||||
context.textBaseline = 'middle';
|
||||
context.font = 'Arial 14px normal';
|
||||
|
||||
// scale
|
||||
const scaleHeight = 20;
|
||||
// scale
|
||||
const scaleHeight = 20;
|
||||
|
||||
context.strokeStyle = '#666';
|
||||
context.beginPath();
|
||||
context.strokeStyle = '#666';
|
||||
context.beginPath();
|
||||
|
||||
for (let time = firstTime; time < endTime; time += frameTime) {
|
||||
context.moveTo(timeToXAligned(time), 0);
|
||||
context.lineTo(timeToXAligned(time), canvas.height);
|
||||
}
|
||||
for (let time = firstTime; time < endTime; time += frameTime) {
|
||||
context.moveTo(timeToXAligned(time), 0);
|
||||
context.lineTo(timeToXAligned(time), canvas.height);
|
||||
}
|
||||
|
||||
context.stroke();
|
||||
context.stroke();
|
||||
|
||||
context.strokeStyle = '#ddd';
|
||||
context.beginPath();
|
||||
context.moveTo(0, scaleHeight + 0.5);
|
||||
context.lineTo(canvas.width, scaleHeight + 0.5);
|
||||
context.stroke();
|
||||
context.strokeStyle = '#ddd';
|
||||
context.beginPath();
|
||||
context.moveTo(0, scaleHeight + 0.5);
|
||||
context.lineTo(canvas.width, scaleHeight + 0.5);
|
||||
context.stroke();
|
||||
|
||||
// entries
|
||||
const rowHeight = 20;
|
||||
const startStack: TimingEntry[] = [];
|
||||
// entries
|
||||
const rowHeight = 20;
|
||||
const startStack: TimingEntry[] = [];
|
||||
|
||||
for (const entry of this.timings) {
|
||||
if (entry.type === TimingEntryType.Start) {
|
||||
startStack.push(entry);
|
||||
} else {
|
||||
const start = startStack.pop()!;
|
||||
const name = start.name!;
|
||||
const startX = timeToX(start.time);
|
||||
const endX = timeToX(entry.time);
|
||||
const y = scaleHeight + 2 + startStack.length * rowHeight;
|
||||
const w = endX - startX;
|
||||
let text = name;
|
||||
const time = entry.time - start.time;
|
||||
for (const entry of this.timings) {
|
||||
if (entry.type === TimingEntryType.Start) {
|
||||
startStack.push(entry);
|
||||
} else {
|
||||
const start = startStack.pop()!;
|
||||
const name = start.name!;
|
||||
const startX = timeToX(start.time);
|
||||
const endX = timeToX(entry.time);
|
||||
const y = scaleHeight + 2 + startStack.length * rowHeight;
|
||||
const w = endX - startX;
|
||||
let text = name;
|
||||
const time = entry.time - start.time;
|
||||
|
||||
if (startStack.length === 0) {
|
||||
context.fillStyle = '#efc457';
|
||||
text = `${text} (${time.toFixed(2)} ms) ${(100 * time / frameTime).toFixed(0)}%`;
|
||||
} else if (/\(\)$/.test(name)) {
|
||||
context.fillStyle = '#d4ecc6';
|
||||
} else {
|
||||
context.fillStyle = '#c6dcec';
|
||||
}
|
||||
if (startStack.length === 0) {
|
||||
context.fillStyle = '#efc457';
|
||||
text = `${text} (${time.toFixed(2)} ms) ${(100 * time / frameTime).toFixed(0)}%`;
|
||||
} else if (/\(\)$/.test(name)) {
|
||||
context.fillStyle = '#d4ecc6';
|
||||
} else {
|
||||
context.fillStyle = '#c6dcec';
|
||||
}
|
||||
|
||||
context.fillRect(startX, y, w, rowHeight - 1);
|
||||
this.tooltips.push({ x: startX, y, w, h: rowHeight, text: `${name}\n${time.toFixed(2)} ms` });
|
||||
context.fillRect(startX, y, w, rowHeight - 1);
|
||||
this.tooltips.push({ x: startX, y, w, h: rowHeight, text: `${name}\n${time.toFixed(2)} ms` });
|
||||
|
||||
if (w > 4) {
|
||||
context.fillStyle = '#222';
|
||||
context.fillText(text, startX + 4, y + rowHeight / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
recalcListing() {
|
||||
interface Entry extends TimingEntry {
|
||||
excludedTime: number;
|
||||
}
|
||||
if (w > 4) {
|
||||
context.fillStyle = '#222';
|
||||
context.fillText(text, startX + 4, y + rowHeight / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
recalcListing() {
|
||||
interface Entry extends TimingEntry {
|
||||
excludedTime: number;
|
||||
}
|
||||
|
||||
this.listing = [];
|
||||
const startStack: Entry[] = [];
|
||||
this.listing = [];
|
||||
const startStack: Entry[] = [];
|
||||
|
||||
for (const entry of this.timings) {
|
||||
if (entry.type === TimingEntryType.Start) {
|
||||
startStack.push({ ...entry, excludedTime: 0 });
|
||||
} else {
|
||||
const start = startStack.pop()!;
|
||||
const name = start.name!;
|
||||
const time = entry.time - start.time;
|
||||
for (const entry of this.timings) {
|
||||
if (entry.type === TimingEntryType.Start) {
|
||||
startStack.push({ ...entry, excludedTime: 0 });
|
||||
} else {
|
||||
const start = startStack.pop()!;
|
||||
const name = start.name!;
|
||||
const time = entry.time - start.time;
|
||||
|
||||
let listing = this.listing.find(l => l.name === name);
|
||||
let listing = this.listing.find(l => l.name === name);
|
||||
|
||||
if (!listing) {
|
||||
listing = { name, selfTime: 0, totalTime: 0, selfPercent: 0, totalPercent: 0, count: 0 };
|
||||
this.listing.push(listing);
|
||||
}
|
||||
if (!listing) {
|
||||
listing = { name, selfTime: 0, totalTime: 0, selfPercent: 0, totalPercent: 0, count: 0 };
|
||||
this.listing.push(listing);
|
||||
}
|
||||
|
||||
listing.count++;
|
||||
listing.selfTime += (time - start.excludedTime);
|
||||
listing.totalTime += time;
|
||||
listing.count++;
|
||||
listing.selfTime += (time - start.excludedTime);
|
||||
listing.totalTime += time;
|
||||
|
||||
if (startStack.length) {
|
||||
startStack[startStack.length - 1].excludedTime += time;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (startStack.length) {
|
||||
startStack[startStack.length - 1].excludedTime += time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const firstTime = this.timings[0].time;
|
||||
const lastTime = this.timings[this.timings.length - 1].time;
|
||||
const totalTime = lastTime - firstTime;
|
||||
const firstTime = this.timings[0].time;
|
||||
const lastTime = this.timings[this.timings.length - 1].time;
|
||||
const totalTime = lastTime - firstTime;
|
||||
|
||||
for (const listing of this.listing) {
|
||||
listing.selfPercent = 100 * listing.selfTime / totalTime;
|
||||
listing.totalPercent = 100 * listing.totalTime / totalTime;
|
||||
}
|
||||
for (const listing of this.listing) {
|
||||
listing.selfPercent = 100 * listing.selfTime / totalTime;
|
||||
listing.totalPercent = 100 * listing.totalTime / totalTime;
|
||||
}
|
||||
|
||||
this.listing.sort((a, b) => b.selfTime - a.selfTime);
|
||||
}
|
||||
this.listing.sort((a, b) => b.selfTime - a.selfTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'admin-reports',
|
||||
templateUrl: 'admin-reports.pug',
|
||||
selector: 'admin-reports',
|
||||
templateUrl: 'admin-reports.pug',
|
||||
})
|
||||
export class AdminReports {
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'admin-sign-in',
|
||||
templateUrl: 'admin-sign-in.pug',
|
||||
selector: 'admin-sign-in',
|
||||
templateUrl: 'admin-sign-in.pug',
|
||||
})
|
||||
export class AdminSignIn {
|
||||
}
|
||||
|
||||
@@ -1,111 +1,111 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
GameServerState, SERVER_SETTINGS, LOGIN_SERVER_SETTINGS, ServerStats, RequestStats,
|
||||
UserCountStats, Stats, StatsTable
|
||||
GameServerState, SERVER_SETTINGS, LOGIN_SERVER_SETTINGS, ServerStats, RequestStats,
|
||||
UserCountStats, Stats, StatsTable
|
||||
} from '../../../common/adminInterfaces';
|
||||
import { hasRole } from '../../../common/accountUtils';
|
||||
import { AdminModel } from '../../services/adminModel';
|
||||
import { faCog, faSlidersH } from '../../../client/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'admin-state',
|
||||
templateUrl: 'admin-state.pug',
|
||||
styleUrls: ['admin-state.scss'],
|
||||
selector: 'admin-state',
|
||||
templateUrl: 'admin-state.pug',
|
||||
styleUrls: ['admin-state.scss'],
|
||||
})
|
||||
export class AdminState {
|
||||
readonly cogIcon = faCog;
|
||||
readonly optionsIcon = faSlidersH;
|
||||
readonly loginOptions = LOGIN_SERVER_SETTINGS;
|
||||
readonly options = SERVER_SETTINGS;
|
||||
private stats = new Map<string, ServerStats>();
|
||||
private statsTables = new Map<string, StatsTable>();
|
||||
requestStats?: RequestStats[];
|
||||
userCounts?: UserCountStats[];
|
||||
showSettings: any = {};
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
get state() {
|
||||
return this.model.state;
|
||||
}
|
||||
get loginServers() {
|
||||
return this.model.state.loginServers;
|
||||
}
|
||||
get servers() {
|
||||
return this.model.state.gameServers;
|
||||
}
|
||||
get isSuperadmin() {
|
||||
return hasRole(this.model.account, 'superadmin');
|
||||
}
|
||||
kickAll(server: GameServerState) {
|
||||
return this.model.kickAll(server.id);
|
||||
}
|
||||
updateLoginSetting(key: string, value: boolean) {
|
||||
return this.model.updateSettings({ [key]: value });
|
||||
}
|
||||
updateLoginSettings(state: any) {
|
||||
return this.model.updateSettings(state);
|
||||
}
|
||||
updateServerSetting(server: GameServerState, key: string, value: boolean) {
|
||||
return this.model.updateGameServerSettings(server.id, { [key]: value });
|
||||
}
|
||||
updateServerSettings(server: GameServerState, state: any) {
|
||||
return this.model.updateGameServerSettings(server.id, state);
|
||||
}
|
||||
// request stats
|
||||
fetchRequestStats() {
|
||||
this.model.getRequestStats()
|
||||
.then(stats => {
|
||||
this.requestStats = stats && stats.requests;
|
||||
this.userCounts = stats && stats.userCounts;
|
||||
});
|
||||
}
|
||||
resetRequestStats() {
|
||||
this.requestStats = undefined;
|
||||
this.userCounts = undefined;
|
||||
}
|
||||
// socket stats
|
||||
fetchStats(server: GameServerState) {
|
||||
return this.model.fetchServerStats(server.id)
|
||||
.then(stats => stats && this.stats.set(server.id, stats));
|
||||
}
|
||||
resetStats(server: GameServerState) {
|
||||
this.stats.delete(server.id);
|
||||
}
|
||||
getStats(server: GameServerState) {
|
||||
return this.stats.get(server.id);
|
||||
}
|
||||
// stats tables
|
||||
fetchCountryStats(server: GameServerState) {
|
||||
return this.fetchStatsTable(server, Stats.Country);
|
||||
}
|
||||
fetchSupportStats(server: GameServerState) {
|
||||
return this.fetchStatsTable(server, Stats.Support);
|
||||
}
|
||||
fetchMapStats(server: GameServerState) {
|
||||
return this.fetchStatsTable(server, Stats.Maps);
|
||||
}
|
||||
resetStatsTable(server: GameServerState) {
|
||||
this.statsTables.delete(server.id);
|
||||
}
|
||||
getStatsTable(server: GameServerState) {
|
||||
return this.statsTables.get(server.id);
|
||||
}
|
||||
private fetchStatsTable(server: GameServerState, stats: Stats) {
|
||||
return this.model.fetchServerStatsTable(server.id, stats)
|
||||
.then(stats => stats && this.statsTables.set(server.id, stats));
|
||||
}
|
||||
// updates
|
||||
notifyOfUpdate(server = '*') {
|
||||
if (confirm('Are you sure?')) {
|
||||
this.model.notifyUpdate(server);
|
||||
}
|
||||
}
|
||||
shutdownServers(server = '*') {
|
||||
if (confirm('Are you sure?')) {
|
||||
this.model.shutdownServers(server);
|
||||
}
|
||||
}
|
||||
resetUpdating(server = '*') {
|
||||
this.model.resetUpdating(server);
|
||||
}
|
||||
readonly cogIcon = faCog;
|
||||
readonly optionsIcon = faSlidersH;
|
||||
readonly loginOptions = LOGIN_SERVER_SETTINGS;
|
||||
readonly options = SERVER_SETTINGS;
|
||||
private stats = new Map<string, ServerStats>();
|
||||
private statsTables = new Map<string, StatsTable>();
|
||||
requestStats?: RequestStats[];
|
||||
userCounts?: UserCountStats[];
|
||||
showSettings: any = {};
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
get state() {
|
||||
return this.model.state;
|
||||
}
|
||||
get loginServers() {
|
||||
return this.model.state.loginServers;
|
||||
}
|
||||
get servers() {
|
||||
return this.model.state.gameServers;
|
||||
}
|
||||
get isSuperadmin() {
|
||||
return hasRole(this.model.account, 'superadmin');
|
||||
}
|
||||
kickAll(server: GameServerState) {
|
||||
return this.model.kickAll(server.id);
|
||||
}
|
||||
updateLoginSetting(key: string, value: boolean) {
|
||||
return this.model.updateSettings({ [key]: value });
|
||||
}
|
||||
updateLoginSettings(state: any) {
|
||||
return this.model.updateSettings(state);
|
||||
}
|
||||
updateServerSetting(server: GameServerState, key: string, value: boolean) {
|
||||
return this.model.updateGameServerSettings(server.id, { [key]: value });
|
||||
}
|
||||
updateServerSettings(server: GameServerState, state: any) {
|
||||
return this.model.updateGameServerSettings(server.id, state);
|
||||
}
|
||||
// request stats
|
||||
fetchRequestStats() {
|
||||
this.model.getRequestStats()
|
||||
.then(stats => {
|
||||
this.requestStats = stats && stats.requests;
|
||||
this.userCounts = stats && stats.userCounts;
|
||||
});
|
||||
}
|
||||
resetRequestStats() {
|
||||
this.requestStats = undefined;
|
||||
this.userCounts = undefined;
|
||||
}
|
||||
// socket stats
|
||||
fetchStats(server: GameServerState) {
|
||||
return this.model.fetchServerStats(server.id)
|
||||
.then(stats => stats && this.stats.set(server.id, stats));
|
||||
}
|
||||
resetStats(server: GameServerState) {
|
||||
this.stats.delete(server.id);
|
||||
}
|
||||
getStats(server: GameServerState) {
|
||||
return this.stats.get(server.id);
|
||||
}
|
||||
// stats tables
|
||||
fetchCountryStats(server: GameServerState) {
|
||||
return this.fetchStatsTable(server, Stats.Country);
|
||||
}
|
||||
fetchSupportStats(server: GameServerState) {
|
||||
return this.fetchStatsTable(server, Stats.Support);
|
||||
}
|
||||
fetchMapStats(server: GameServerState) {
|
||||
return this.fetchStatsTable(server, Stats.Maps);
|
||||
}
|
||||
resetStatsTable(server: GameServerState) {
|
||||
this.statsTables.delete(server.id);
|
||||
}
|
||||
getStatsTable(server: GameServerState) {
|
||||
return this.statsTables.get(server.id);
|
||||
}
|
||||
private fetchStatsTable(server: GameServerState, stats: Stats) {
|
||||
return this.model.fetchServerStatsTable(server.id, stats)
|
||||
.then(stats => stats && this.statsTables.set(server.id, stats));
|
||||
}
|
||||
// updates
|
||||
notifyOfUpdate(server = '*') {
|
||||
if (confirm('Are you sure?')) {
|
||||
this.model.notifyUpdate(server);
|
||||
}
|
||||
}
|
||||
shutdownServers(server = '*') {
|
||||
if (confirm('Are you sure?')) {
|
||||
this.model.shutdownServers(server);
|
||||
}
|
||||
}
|
||||
resetUpdating(server = '*') {
|
||||
this.model.resetUpdating(server);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,84 +56,84 @@ import { AdminApp } from './admin';
|
||||
import { ErrorReporter } from '../services/errorReporter';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: '', component: AdminState },
|
||||
{ path: 'sign-in', component: AdminSignIn },
|
||||
{ path: 'events', component: AdminEvents },
|
||||
{ path: 'accounts', component: AdminAccounts },
|
||||
{ path: 'accounts/:id', component: AdminAccountDetails },
|
||||
{ path: 'ponies', component: AdminPonies },
|
||||
{ path: 'origins', component: AdminOrigins },
|
||||
{ path: 'origins/:ip/:country', component: AdminOriginDetails },
|
||||
{
|
||||
path: 'reports',
|
||||
component: AdminReports,
|
||||
children: [
|
||||
{ path: '', redirectTo: 'perf', pathMatch: 'full' },
|
||||
{ path: 'perf', component: AdminReportsPerf },
|
||||
],
|
||||
},
|
||||
{ path: 'other', component: AdminOther },
|
||||
{ path: '', component: AdminState },
|
||||
{ path: 'sign-in', component: AdminSignIn },
|
||||
{ path: 'events', component: AdminEvents },
|
||||
{ path: 'accounts', component: AdminAccounts },
|
||||
{ path: 'accounts/:id', component: AdminAccountDetails },
|
||||
{ path: 'ponies', component: AdminPonies },
|
||||
{ path: 'origins', component: AdminOrigins },
|
||||
{ path: 'origins/:ip/:country', component: AdminOriginDetails },
|
||||
{
|
||||
path: 'reports',
|
||||
component: AdminReports,
|
||||
children: [
|
||||
{ path: '', redirectTo: 'perf', pathMatch: 'full' },
|
||||
{ path: 'perf', component: AdminReportsPerf },
|
||||
],
|
||||
},
|
||||
{ path: 'other', component: AdminOther },
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
RouterModule,
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
PopoverModule.forRoot(),
|
||||
PaginationModule.forRoot(),
|
||||
ButtonsModule.forRoot(),
|
||||
ModalModule,
|
||||
TooltipModule,
|
||||
SharedModule,
|
||||
RouterModule.forRoot(routes),
|
||||
FontAwesomeModule,
|
||||
],
|
||||
declarations: [
|
||||
KeysPipe,
|
||||
OrderByPipe,
|
||||
TranslitPipe,
|
||||
EventsTable,
|
||||
AccountInfo,
|
||||
AccountInfoRemote,
|
||||
AccountStatus,
|
||||
AccountTooltip,
|
||||
OriginInfo,
|
||||
OriginInfoRemote,
|
||||
OriginListRemote,
|
||||
PonyInfo,
|
||||
PonyInfoRemote,
|
||||
PonyListRemote,
|
||||
AuthInfo,
|
||||
AuthInfoRemote,
|
||||
AuthInfoEdit,
|
||||
AuthList,
|
||||
AuthListRemote,
|
||||
OnOffSwitch,
|
||||
AdminChatLog,
|
||||
BanIcon,
|
||||
EmailList,
|
||||
FromNow,
|
||||
TimeField,
|
||||
UAInfo,
|
||||
AdminState,
|
||||
AdminEvents,
|
||||
AdminOther,
|
||||
AdminAccounts,
|
||||
AdminAccountDetails,
|
||||
AdminPonies,
|
||||
AdminReports,
|
||||
AdminReportsPerf,
|
||||
AdminOrigins,
|
||||
AdminOriginDetails,
|
||||
AdminSignIn,
|
||||
AdminApp,
|
||||
],
|
||||
providers: [
|
||||
ErrorReporter,
|
||||
],
|
||||
bootstrap: [AdminApp],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
RouterModule,
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
PopoverModule.forRoot(),
|
||||
PaginationModule.forRoot(),
|
||||
ButtonsModule.forRoot(),
|
||||
ModalModule,
|
||||
TooltipModule,
|
||||
SharedModule,
|
||||
RouterModule.forRoot(routes),
|
||||
FontAwesomeModule,
|
||||
],
|
||||
declarations: [
|
||||
KeysPipe,
|
||||
OrderByPipe,
|
||||
TranslitPipe,
|
||||
EventsTable,
|
||||
AccountInfo,
|
||||
AccountInfoRemote,
|
||||
AccountStatus,
|
||||
AccountTooltip,
|
||||
OriginInfo,
|
||||
OriginInfoRemote,
|
||||
OriginListRemote,
|
||||
PonyInfo,
|
||||
PonyInfoRemote,
|
||||
PonyListRemote,
|
||||
AuthInfo,
|
||||
AuthInfoRemote,
|
||||
AuthInfoEdit,
|
||||
AuthList,
|
||||
AuthListRemote,
|
||||
OnOffSwitch,
|
||||
AdminChatLog,
|
||||
BanIcon,
|
||||
EmailList,
|
||||
FromNow,
|
||||
TimeField,
|
||||
UAInfo,
|
||||
AdminState,
|
||||
AdminEvents,
|
||||
AdminOther,
|
||||
AdminAccounts,
|
||||
AdminAccountDetails,
|
||||
AdminPonies,
|
||||
AdminReports,
|
||||
AdminReportsPerf,
|
||||
AdminOrigins,
|
||||
AdminOriginDetails,
|
||||
AdminSignIn,
|
||||
AdminApp,
|
||||
],
|
||||
providers: [
|
||||
ErrorReporter,
|
||||
],
|
||||
bootstrap: [AdminApp],
|
||||
})
|
||||
export class AdminAppModule {
|
||||
}
|
||||
|
||||
@@ -5,71 +5,71 @@ import { PopoverConfig } from 'ngx-bootstrap/popover';
|
||||
import { hasRole } from '../../common/accountUtils';
|
||||
import { AdminModel } from '../services/adminModel';
|
||||
import {
|
||||
faSpinner, faSlidersH, faExclamationCircle, faUsers, faHorseHead, faMapMarkerAlt, faChartPie, faCog
|
||||
faSpinner, faSlidersH, faExclamationCircle, faUsers, faHorseHead, faMapMarkerAlt, faChartPie, faCog
|
||||
} from '../../client/icons';
|
||||
|
||||
export function tooltipConfig() {
|
||||
return Object.assign(new TooltipConfig(), { container: 'body' });
|
||||
return Object.assign(new TooltipConfig(), { container: 'body' });
|
||||
}
|
||||
|
||||
export function popoverConfig() {
|
||||
return Object.assign(new PopoverConfig(), { container: 'body' });
|
||||
return Object.assign(new PopoverConfig(), { container: 'body' });
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'pony-town-app',
|
||||
templateUrl: 'admin.pug',
|
||||
styleUrls: ['admin.scss'],
|
||||
providers: [
|
||||
{ provide: TooltipConfig, useFactory: tooltipConfig },
|
||||
{ provide: PopoverConfig, useFactory: popoverConfig },
|
||||
]
|
||||
selector: 'pony-town-app',
|
||||
templateUrl: 'admin.pug',
|
||||
styleUrls: ['admin.scss'],
|
||||
providers: [
|
||||
{ provide: TooltipConfig, useFactory: tooltipConfig },
|
||||
{ provide: PopoverConfig, useFactory: popoverConfig },
|
||||
]
|
||||
})
|
||||
export class AdminApp {
|
||||
readonly spinnerIcon = faSpinner;
|
||||
readonly stateIcon = faSlidersH;
|
||||
readonly eventsIcon = faExclamationCircle;
|
||||
readonly accountsIcon = faUsers;
|
||||
readonly poniesIcon = faHorseHead;
|
||||
readonly originsIcon = faMapMarkerAlt;
|
||||
readonly reportsIcon = faChartPie;
|
||||
readonly otherIcon = faCog;
|
||||
constructor(public model: AdminModel, private router: Router) {
|
||||
}
|
||||
get loading() {
|
||||
if (!this.model.initialized) {
|
||||
return 'Initializing';
|
||||
} else if (!this.model.connected) {
|
||||
return 'Connecting';
|
||||
} else if (!this.model.loaded) {
|
||||
return 'Loading';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
get clients() {
|
||||
return this.model.state.gameServers.reduce((sum, s) => sum + s.online, 0);
|
||||
}
|
||||
get events() {
|
||||
return this.model.events.length;
|
||||
}
|
||||
get accounts() {
|
||||
return this.model.counts.accounts;
|
||||
}
|
||||
get ponies() {
|
||||
return this.model.counts.characters;
|
||||
}
|
||||
get origins() {
|
||||
return this.model.counts.origins;
|
||||
}
|
||||
get isSuperadmin() {
|
||||
return hasRole(this.model.account, 'superadmin');
|
||||
}
|
||||
@HostListener('window:go-to-account', ['$event'])
|
||||
goToAccount({ detail }: CustomEvent) {
|
||||
this.router.navigate(['/accounts', detail]);
|
||||
}
|
||||
signOut() {
|
||||
window.location.href = '/';
|
||||
}
|
||||
readonly spinnerIcon = faSpinner;
|
||||
readonly stateIcon = faSlidersH;
|
||||
readonly eventsIcon = faExclamationCircle;
|
||||
readonly accountsIcon = faUsers;
|
||||
readonly poniesIcon = faHorseHead;
|
||||
readonly originsIcon = faMapMarkerAlt;
|
||||
readonly reportsIcon = faChartPie;
|
||||
readonly otherIcon = faCog;
|
||||
constructor(public model: AdminModel, private router: Router) {
|
||||
}
|
||||
get loading() {
|
||||
if (!this.model.initialized) {
|
||||
return 'Initializing';
|
||||
} else if (!this.model.connected) {
|
||||
return 'Connecting';
|
||||
} else if (!this.model.loaded) {
|
||||
return 'Loading';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
get clients() {
|
||||
return this.model.state.gameServers.reduce((sum, s) => sum + s.online, 0);
|
||||
}
|
||||
get events() {
|
||||
return this.model.events.length;
|
||||
}
|
||||
get accounts() {
|
||||
return this.model.counts.accounts;
|
||||
}
|
||||
get ponies() {
|
||||
return this.model.counts.characters;
|
||||
}
|
||||
get origins() {
|
||||
return this.model.counts.origins;
|
||||
}
|
||||
get isSuperadmin() {
|
||||
return hasRole(this.model.account, 'superadmin');
|
||||
}
|
||||
@HostListener('window:go-to-account', ['$event'])
|
||||
goToAccount({ detail }: CustomEvent) {
|
||||
this.router.navigate(['/accounts', detail]);
|
||||
}
|
||||
signOut() {
|
||||
window.location.href = '/';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,100 +1,100 @@
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
export interface BaseTableState {
|
||||
search: string;
|
||||
currentPage: number;
|
||||
sortedBy: string;
|
||||
sortedAsc: boolean;
|
||||
search: string;
|
||||
currentPage: number;
|
||||
sortedBy: string;
|
||||
sortedAsc: boolean;
|
||||
}
|
||||
|
||||
export abstract class BaseTable<T> {
|
||||
itemsPerPage = 20;
|
||||
sorted: T[] = [];
|
||||
filtered: T[] = [];
|
||||
filteredOnPage: T[] = [];
|
||||
sortedBy = 'createdAt';
|
||||
sortedAsc = true;
|
||||
private _search = '';
|
||||
private _currentPage = 1;
|
||||
private execSearch = debounce(() => {
|
||||
this.updateFiltered();
|
||||
this.onChange();
|
||||
}, 500);
|
||||
get itemsFrom() {
|
||||
return (this.currentPage - 1) * this.itemsPerPage;
|
||||
}
|
||||
get items(): T[] {
|
||||
return [];
|
||||
}
|
||||
get search() {
|
||||
return this._search;
|
||||
}
|
||||
set search(value: string) {
|
||||
if (this._search !== value) {
|
||||
this._search = value;
|
||||
this.execSearch();
|
||||
}
|
||||
}
|
||||
get currentPage() {
|
||||
return this._currentPage;
|
||||
}
|
||||
set currentPage(value: number) {
|
||||
if (this._currentPage !== value) {
|
||||
this._currentPage = value;
|
||||
this.updatePage();
|
||||
this.onChange();
|
||||
}
|
||||
}
|
||||
sortBy(field: string) {
|
||||
if (this.sortedBy === field) {
|
||||
this.sortedAsc = !this.sortedAsc;
|
||||
} else {
|
||||
this.sortedBy = field;
|
||||
this.sortedAsc = true;
|
||||
}
|
||||
itemsPerPage = 20;
|
||||
sorted: T[] = [];
|
||||
filtered: T[] = [];
|
||||
filteredOnPage: T[] = [];
|
||||
sortedBy = 'createdAt';
|
||||
sortedAsc = true;
|
||||
private _search = '';
|
||||
private _currentPage = 1;
|
||||
private execSearch = debounce(() => {
|
||||
this.updateFiltered();
|
||||
this.onChange();
|
||||
}, 500);
|
||||
get itemsFrom() {
|
||||
return (this.currentPage - 1) * this.itemsPerPage;
|
||||
}
|
||||
get items(): T[] {
|
||||
return [];
|
||||
}
|
||||
get search() {
|
||||
return this._search;
|
||||
}
|
||||
set search(value: string) {
|
||||
if (this._search !== value) {
|
||||
this._search = value;
|
||||
this.execSearch();
|
||||
}
|
||||
}
|
||||
get currentPage() {
|
||||
return this._currentPage;
|
||||
}
|
||||
set currentPage(value: number) {
|
||||
if (this._currentPage !== value) {
|
||||
this._currentPage = value;
|
||||
this.updatePage();
|
||||
this.onChange();
|
||||
}
|
||||
}
|
||||
sortBy(field: string) {
|
||||
if (this.sortedBy === field) {
|
||||
this.sortedAsc = !this.sortedAsc;
|
||||
} else {
|
||||
this.sortedBy = field;
|
||||
this.sortedAsc = true;
|
||||
}
|
||||
|
||||
this.updateSorted();
|
||||
this.onChange();
|
||||
}
|
||||
sortedClass(field: string) {
|
||||
return this.sortedBy === field ? (this.sortedAsc ? 'sorted-asc' : 'sorted-desc') : undefined;
|
||||
}
|
||||
protected updateItems() {
|
||||
this.updateSorted();
|
||||
}
|
||||
protected updateSorted() {
|
||||
this.sorted = this.sortItems(this.items, this.sortedBy, this.sortedAsc);
|
||||
this.updateFiltered();
|
||||
}
|
||||
protected updateFiltered() {
|
||||
this.filtered = this.filterItems(this.sorted, this.search);
|
||||
this.updatePage();
|
||||
}
|
||||
protected updatePage() {
|
||||
this.filteredOnPage = this.filtered.slice(this.itemsFrom, this.itemsFrom + this.itemsPerPage);
|
||||
}
|
||||
protected sortItems(items: T[], _by: string, _asc: boolean) {
|
||||
return items;
|
||||
}
|
||||
protected filterItems(items: T[], _search: string) {
|
||||
return items;
|
||||
}
|
||||
protected onChange() {
|
||||
}
|
||||
protected getState(): BaseTableState {
|
||||
return {
|
||||
search: this.search,
|
||||
currentPage: this.currentPage,
|
||||
sortedBy: this.sortedBy,
|
||||
sortedAsc: this.sortedAsc,
|
||||
};
|
||||
}
|
||||
protected setState(state: BaseTableState | undefined) {
|
||||
if (state) {
|
||||
this._search = state.search;
|
||||
this._currentPage = state.currentPage || 1;
|
||||
this.sortedBy = state.sortedBy;
|
||||
this.sortedAsc = state.sortedAsc;
|
||||
}
|
||||
}
|
||||
this.updateSorted();
|
||||
this.onChange();
|
||||
}
|
||||
sortedClass(field: string) {
|
||||
return this.sortedBy === field ? (this.sortedAsc ? 'sorted-asc' : 'sorted-desc') : undefined;
|
||||
}
|
||||
protected updateItems() {
|
||||
this.updateSorted();
|
||||
}
|
||||
protected updateSorted() {
|
||||
this.sorted = this.sortItems(this.items, this.sortedBy, this.sortedAsc);
|
||||
this.updateFiltered();
|
||||
}
|
||||
protected updateFiltered() {
|
||||
this.filtered = this.filterItems(this.sorted, this.search);
|
||||
this.updatePage();
|
||||
}
|
||||
protected updatePage() {
|
||||
this.filteredOnPage = this.filtered.slice(this.itemsFrom, this.itemsFrom + this.itemsPerPage);
|
||||
}
|
||||
protected sortItems(items: T[], _by: string, _asc: boolean) {
|
||||
return items;
|
||||
}
|
||||
protected filterItems(items: T[], _search: string) {
|
||||
return items;
|
||||
}
|
||||
protected onChange() {
|
||||
}
|
||||
protected getState(): BaseTableState {
|
||||
return {
|
||||
search: this.search,
|
||||
currentPage: this.currentPage,
|
||||
sortedBy: this.sortedBy,
|
||||
sortedAsc: this.sortedAsc,
|
||||
};
|
||||
}
|
||||
protected setState(state: BaseTableState | undefined) {
|
||||
if (state) {
|
||||
this._search = state.search;
|
||||
this._currentPage = state.currentPage || 1;
|
||||
this.sortedBy = state.sortedBy;
|
||||
this.sortedAsc = state.sortedAsc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'keys',
|
||||
name: 'keys',
|
||||
})
|
||||
export class KeysPipe implements PipeTransform {
|
||||
transform(value: any) {
|
||||
return value ? Object.keys(value) : value;
|
||||
}
|
||||
transform(value: any) {
|
||||
return value ? Object.keys(value) : value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'orderBy',
|
||||
name: 'orderBy',
|
||||
})
|
||||
export class OrderByPipe implements PipeTransform {
|
||||
transform(value: any[] | undefined, compare?: any) {
|
||||
return value && value.slice().sort(compare);
|
||||
}
|
||||
transform(value: any[] | undefined, compare?: any) {
|
||||
return value && value.slice().sort(compare);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { transliterate } from 'transliteration';
|
||||
|
||||
@Pipe({
|
||||
name: 'translit',
|
||||
name: 'translit',
|
||||
})
|
||||
export class TranslitPipe implements PipeTransform {
|
||||
transform(value: string) {
|
||||
if (!value || /^[a-z0-9-_.,\[\]!@#$%^&*{}|\/\\ ]+$/i.test(value))
|
||||
return undefined;
|
||||
transform(value: string) {
|
||||
if (!value || /^[a-z0-9-_.,\[\]!@#$%^&*{}|\/\\ ]+$/i.test(value))
|
||||
return undefined;
|
||||
|
||||
const translit = transliterate(value);
|
||||
return translit !== value ? translit : undefined;
|
||||
}
|
||||
const translit = transliterate(value);
|
||||
return translit !== value ? translit : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,41 +4,41 @@ import { Subscription } from '../../../../common/interfaces';
|
||||
import { AdminModel } from '../../../services/adminModel';
|
||||
|
||||
@Component({
|
||||
selector: 'account-info-remote',
|
||||
templateUrl: 'account-info-remote.pug',
|
||||
selector: 'account-info-remote',
|
||||
templateUrl: 'account-info-remote.pug',
|
||||
})
|
||||
export class AccountInfoRemote implements OnDestroy {
|
||||
@Input() extendedAuths = false;
|
||||
@Input() popoverPlacement?: string;
|
||||
@Input() showDuplicates = false;
|
||||
@Input() basic = false;
|
||||
account?: Account;
|
||||
private _accountId?: string;
|
||||
private subscription?: Subscription;
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
get accountId() {
|
||||
return this._accountId;
|
||||
}
|
||||
@Input() set accountId(value) {
|
||||
if (this._accountId !== value) {
|
||||
this._accountId = value;
|
||||
this.account = undefined;
|
||||
this.updateSubscription();
|
||||
}
|
||||
}
|
||||
private updateSubscription() {
|
||||
if (this.subscription) {
|
||||
this.subscription.unsubscribe();
|
||||
this.subscription = undefined;
|
||||
}
|
||||
@Input() extendedAuths = false;
|
||||
@Input() popoverPlacement?: string;
|
||||
@Input() showDuplicates = false;
|
||||
@Input() basic = false;
|
||||
account?: Account;
|
||||
private _accountId?: string;
|
||||
private subscription?: Subscription;
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
get accountId() {
|
||||
return this._accountId;
|
||||
}
|
||||
@Input() set accountId(value) {
|
||||
if (this._accountId !== value) {
|
||||
this._accountId = value;
|
||||
this.account = undefined;
|
||||
this.updateSubscription();
|
||||
}
|
||||
}
|
||||
private updateSubscription() {
|
||||
if (this.subscription) {
|
||||
this.subscription.unsubscribe();
|
||||
this.subscription = undefined;
|
||||
}
|
||||
|
||||
if (this.accountId) {
|
||||
this.subscription = this.model.accounts
|
||||
.subscribe(this.accountId, account => this.account = account);
|
||||
}
|
||||
}
|
||||
if (this.accountId) {
|
||||
this.subscription = this.model.accounts
|
||||
.subscribe(this.accountId, account => this.account = account);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ import { compact } from 'lodash';
|
||||
import { DAY, MINUTE, HOUR } from '../../../../common/constants';
|
||||
import { hasFlag, fromNow, toInt, setFlag } from '../../../../common/utils';
|
||||
import {
|
||||
Account, AccountFlags, accountCounters, accountFlags, SupporterFlags, supporterFlags, BannedMuted, DuplicatesInfo
|
||||
Account, AccountFlags, accountCounters, accountFlags, SupporterFlags, supporterFlags, BannedMuted, DuplicatesInfo
|
||||
} from '../../../../common/adminInterfaces';
|
||||
import { supporterLevel, patreonSupporterLevel, getAge, isPastSupporter } from '../../../../common/adminUtils';
|
||||
import { AdminModel } from '../../../services/adminModel';
|
||||
import { AccountCounters } from '../../../../common/interfaces';
|
||||
import {
|
||||
faPatreon, faCog, faMinus, faPlus, faCheck, faFlag, faStickyNote, faCertificate, faIdBadge, faEnvelope, faFont,
|
||||
faClock, faBan, faMapMarkerAlt
|
||||
faPatreon, faCog, faMinus, faPlus, faCheck, faFlag, faStickyNote, faCertificate, faIdBadge, faEnvelope, faFont,
|
||||
faClock, faBan, faMapMarkerAlt
|
||||
} from '../../../../client/icons';
|
||||
|
||||
const EMPTY_ROLES: string[] = [];
|
||||
@@ -22,271 +22,271 @@ const accountDuplicatesIntervalTime = 10 * MINUTE;
|
||||
const accountDuplicates = new Map<string, DuplicatesInfo>();
|
||||
|
||||
const predefinedAlerts = [
|
||||
{
|
||||
name: 'erp:flagged',
|
||||
message: `Your account has been flagged for inappropriate bahavior on PG rated server. `
|
||||
+ `Continuing that behavior may result in permanent ban.`
|
||||
},
|
||||
{
|
||||
name: 'erp:timeout',
|
||||
message: `Your account has been timed out for inappropriate language and bahavior on PG rated server. `
|
||||
+ `Continuing that behavior may result in permanent ban.`
|
||||
},
|
||||
{
|
||||
name: 'dups',
|
||||
message: `Your account has been flagged for making multiple accounts. `
|
||||
+ `Continuing that behavior may result in permanent ban.`
|
||||
},
|
||||
{
|
||||
name: 'under',
|
||||
message: `Your account has been reported for being underage, please do NOT play on 18+ server. `
|
||||
+ `Continuing that may result in permanent ban.`
|
||||
},
|
||||
{
|
||||
name: 'erp:flagged',
|
||||
message: `Your account has been flagged for inappropriate bahavior on PG rated server. `
|
||||
+ `Continuing that behavior may result in permanent ban.`
|
||||
},
|
||||
{
|
||||
name: 'erp:timeout',
|
||||
message: `Your account has been timed out for inappropriate language and bahavior on PG rated server. `
|
||||
+ `Continuing that behavior may result in permanent ban.`
|
||||
},
|
||||
{
|
||||
name: 'dups',
|
||||
message: `Your account has been flagged for making multiple accounts. `
|
||||
+ `Continuing that behavior may result in permanent ban.`
|
||||
},
|
||||
{
|
||||
name: 'under',
|
||||
message: `Your account has been reported for being underage, please do NOT play on 18+ server. `
|
||||
+ `Continuing that may result in permanent ban.`
|
||||
},
|
||||
];
|
||||
|
||||
const alertExpires = [
|
||||
{ name: '1h', length: HOUR },
|
||||
{ name: '5h', length: 5 * HOUR },
|
||||
{ name: '12h', length: 12 * HOUR },
|
||||
{ name: '1d', length: DAY },
|
||||
{ name: '2d', length: 2 * DAY },
|
||||
{ name: '5d', length: 5 * DAY },
|
||||
{ name: '7d', length: 7 * DAY },
|
||||
{ name: '2w', length: 14 * DAY },
|
||||
{ name: '1h', length: HOUR },
|
||||
{ name: '5h', length: 5 * HOUR },
|
||||
{ name: '12h', length: 12 * HOUR },
|
||||
{ name: '1d', length: DAY },
|
||||
{ name: '2d', length: 2 * DAY },
|
||||
{ name: '5d', length: 5 * DAY },
|
||||
{ name: '7d', length: 7 * DAY },
|
||||
{ name: '2w', length: 14 * DAY },
|
||||
];
|
||||
|
||||
@Component({
|
||||
selector: 'account-info',
|
||||
templateUrl: 'account-info.pug',
|
||||
styleUrls: ['account-info.scss'],
|
||||
selector: 'account-info',
|
||||
templateUrl: 'account-info.pug',
|
||||
styleUrls: ['account-info.scss'],
|
||||
})
|
||||
export class AccountInfo implements OnInit, OnChanges {
|
||||
readonly counters = accountCounters;
|
||||
readonly flags = accountFlags;
|
||||
readonly supporterFlags = supporterFlags;
|
||||
readonly cogIcon = faCog;
|
||||
readonly minusIcon = faMinus;
|
||||
readonly plusIcon = faPlus;
|
||||
readonly checkIcon = faCheck;
|
||||
readonly flagIcon = faFlag;
|
||||
readonly noteIcon = faStickyNote;
|
||||
readonly newIcon = faCertificate;
|
||||
readonly duplicateBrowserIdIcon = faIdBadge;
|
||||
readonly duplicateEmailIcon = faEnvelope;
|
||||
readonly duplicateNameIcon = faFont;
|
||||
readonly duplicatePermaIcon = faBan;
|
||||
readonly teleportIcon = faMapMarkerAlt;
|
||||
predefinedAlerts = predefinedAlerts;
|
||||
alertExpires = alertExpires;
|
||||
alertExpire = alertExpires[3];
|
||||
alertMessage = '';
|
||||
@Input() account!: Account;
|
||||
@Input() extendedAuths = false;
|
||||
@Input() popoverPlacement?: string;
|
||||
@Input() showDuplicates = false;
|
||||
@ViewChild('alertModal', { static: true }) alertModal!: TemplateRef<any>;
|
||||
note?: string;
|
||||
duplicates?: DuplicatesInfo;
|
||||
private alertModalRef?: BsModalRef;
|
||||
private _isNoteOpen = false;
|
||||
constructor(private model: AdminModel, private modalService: BsModalService) {
|
||||
}
|
||||
ngOnInit() {
|
||||
this.updateDuplicates();
|
||||
}
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes.account) {
|
||||
this.updateDuplicates();
|
||||
}
|
||||
}
|
||||
get age() {
|
||||
return this.account.birthdate ? getAge(this.account.birthdate) : '-';
|
||||
}
|
||||
get alert() {
|
||||
const alert = this.account.alert;
|
||||
return (alert && alert.expires.getTime() > Date.now()) ? alert : undefined;
|
||||
}
|
||||
get isNew() {
|
||||
return this.account.createdAt && this.account.createdAt.getTime() > newTime;
|
||||
}
|
||||
get isNoteOpen() {
|
||||
return this._isNoteOpen;
|
||||
}
|
||||
set isNoteOpen(value: boolean) {
|
||||
if (this._isNoteOpen !== value) {
|
||||
this._isNoteOpen = value;
|
||||
readonly counters = accountCounters;
|
||||
readonly flags = accountFlags;
|
||||
readonly supporterFlags = supporterFlags;
|
||||
readonly cogIcon = faCog;
|
||||
readonly minusIcon = faMinus;
|
||||
readonly plusIcon = faPlus;
|
||||
readonly checkIcon = faCheck;
|
||||
readonly flagIcon = faFlag;
|
||||
readonly noteIcon = faStickyNote;
|
||||
readonly newIcon = faCertificate;
|
||||
readonly duplicateBrowserIdIcon = faIdBadge;
|
||||
readonly duplicateEmailIcon = faEnvelope;
|
||||
readonly duplicateNameIcon = faFont;
|
||||
readonly duplicatePermaIcon = faBan;
|
||||
readonly teleportIcon = faMapMarkerAlt;
|
||||
predefinedAlerts = predefinedAlerts;
|
||||
alertExpires = alertExpires;
|
||||
alertExpire = alertExpires[3];
|
||||
alertMessage = '';
|
||||
@Input() account!: Account;
|
||||
@Input() extendedAuths = false;
|
||||
@Input() popoverPlacement?: string;
|
||||
@Input() showDuplicates = false;
|
||||
@ViewChild('alertModal', { static: true }) alertModal!: TemplateRef<any>;
|
||||
note?: string;
|
||||
duplicates?: DuplicatesInfo;
|
||||
private alertModalRef?: BsModalRef;
|
||||
private _isNoteOpen = false;
|
||||
constructor(private model: AdminModel, private modalService: BsModalService) {
|
||||
}
|
||||
ngOnInit() {
|
||||
this.updateDuplicates();
|
||||
}
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes.account) {
|
||||
this.updateDuplicates();
|
||||
}
|
||||
}
|
||||
get age() {
|
||||
return this.account.birthdate ? getAge(this.account.birthdate) : '-';
|
||||
}
|
||||
get alert() {
|
||||
const alert = this.account.alert;
|
||||
return (alert && alert.expires.getTime() > Date.now()) ? alert : undefined;
|
||||
}
|
||||
get isNew() {
|
||||
return this.account.createdAt && this.account.createdAt.getTime() > newTime;
|
||||
}
|
||||
get isNoteOpen() {
|
||||
return this._isNoteOpen;
|
||||
}
|
||||
set isNoteOpen(value: boolean) {
|
||||
if (this._isNoteOpen !== value) {
|
||||
this._isNoteOpen = value;
|
||||
|
||||
if (value) {
|
||||
this.note = this.account.note;
|
||||
}
|
||||
}
|
||||
}
|
||||
get isInactive() {
|
||||
return this.account && this.account.lastVisit && this.account.lastVisit.getTime() < oldTime;
|
||||
}
|
||||
get roles() {
|
||||
return this.account.roles ? this.account.roles.filter(r => r !== 'superadmin') : EMPTY_ROLES;
|
||||
}
|
||||
get flagClass() {
|
||||
const counters = this.account.counters;
|
||||
if (value) {
|
||||
this.note = this.account.note;
|
||||
}
|
||||
}
|
||||
}
|
||||
get isInactive() {
|
||||
return this.account && this.account.lastVisit && this.account.lastVisit.getTime() < oldTime;
|
||||
}
|
||||
get roles() {
|
||||
return this.account.roles ? this.account.roles.filter(r => r !== 'superadmin') : EMPTY_ROLES;
|
||||
}
|
||||
get flagClass() {
|
||||
const counters = this.account.counters;
|
||||
|
||||
if (this.account.flags) {
|
||||
return 'text-banned';
|
||||
} else if (!counters && !this.account.supporter) {
|
||||
return 'text-muted';
|
||||
} else if (counters && ((counters.spam || 0) > 100 || (counters.swears || 0) > 10)) {
|
||||
return 'text-alert';
|
||||
} else {
|
||||
return 'text-present';
|
||||
}
|
||||
}
|
||||
get hasDuplicateNote() {
|
||||
return this.account.note && /duplicate/i.test(this.account.note);
|
||||
}
|
||||
hasFlag(value: AccountFlags) {
|
||||
return hasFlag(this.account.flags, value);
|
||||
}
|
||||
toggleFlag(value: AccountFlags) {
|
||||
this.model.setAccountFlags(this.account._id, this.account.flags ^ value);
|
||||
}
|
||||
toggleBan(field: keyof BannedMuted, value: number) {
|
||||
this.model.setAccountBanField(this.account._id, field, value);
|
||||
}
|
||||
kick() {
|
||||
this.model.kick(this.account._id);
|
||||
}
|
||||
report() {
|
||||
this.model.report(this.account._id);
|
||||
}
|
||||
blur() {
|
||||
this.model.setNote(this.account._id, this.note || '');
|
||||
this.isNoteOpen = false;
|
||||
}
|
||||
decrementCounter(name: keyof AccountCounters) {
|
||||
if (this.getCounter(name) > 0) {
|
||||
this.setCounter(name, this.getCounter(name) - 1);
|
||||
}
|
||||
}
|
||||
incrementCounter(name: keyof AccountCounters) {
|
||||
this.setCounter(name, this.getCounter(name) + 1);
|
||||
}
|
||||
getCounter(name: keyof AccountCounters) {
|
||||
const counters = this.account.counters;
|
||||
return toInt(counters && counters[name]);
|
||||
}
|
||||
setCounter(name: keyof AccountCounters, value: number) {
|
||||
const counters = this.account.counters || (this.account.counters = {});
|
||||
counters[name] = value;
|
||||
this.model.setAccountCounter(this.account._id, name, value);
|
||||
}
|
||||
removeAlert() {
|
||||
this.model.setAlert(this.account._id, '', 0);
|
||||
}
|
||||
setAlert() {
|
||||
this.alertMessage = this.alert ? this.alert.message : '';
|
||||
this.alertExpire = this.alertExpires[2];
|
||||
this.alertModalRef = this.modalService.show(this.alertModal, { ignoreBackdropClick: true });
|
||||
}
|
||||
cancelAlert() {
|
||||
this.alertModalRef && this.alertModalRef.hide();
|
||||
this.alertModalRef = undefined;
|
||||
}
|
||||
confirmAlert() {
|
||||
this.model.setAlert(this.account._id, this.alertMessage, this.alertExpire.length);
|
||||
this.cancelAlert();
|
||||
}
|
||||
private updateDuplicates() {
|
||||
const account = this.account;
|
||||
if (this.account.flags) {
|
||||
return 'text-banned';
|
||||
} else if (!counters && !this.account.supporter) {
|
||||
return 'text-muted';
|
||||
} else if (counters && ((counters.spam || 0) > 100 || (counters.swears || 0) > 10)) {
|
||||
return 'text-alert';
|
||||
} else {
|
||||
return 'text-present';
|
||||
}
|
||||
}
|
||||
get hasDuplicateNote() {
|
||||
return this.account.note && /duplicate/i.test(this.account.note);
|
||||
}
|
||||
hasFlag(value: AccountFlags) {
|
||||
return hasFlag(this.account.flags, value);
|
||||
}
|
||||
toggleFlag(value: AccountFlags) {
|
||||
this.model.setAccountFlags(this.account._id, this.account.flags ^ value);
|
||||
}
|
||||
toggleBan(field: keyof BannedMuted, value: number) {
|
||||
this.model.setAccountBanField(this.account._id, field, value);
|
||||
}
|
||||
kick() {
|
||||
this.model.kick(this.account._id);
|
||||
}
|
||||
report() {
|
||||
this.model.report(this.account._id);
|
||||
}
|
||||
blur() {
|
||||
this.model.setNote(this.account._id, this.note || '');
|
||||
this.isNoteOpen = false;
|
||||
}
|
||||
decrementCounter(name: keyof AccountCounters) {
|
||||
if (this.getCounter(name) > 0) {
|
||||
this.setCounter(name, this.getCounter(name) - 1);
|
||||
}
|
||||
}
|
||||
incrementCounter(name: keyof AccountCounters) {
|
||||
this.setCounter(name, this.getCounter(name) + 1);
|
||||
}
|
||||
getCounter(name: keyof AccountCounters) {
|
||||
const counters = this.account.counters;
|
||||
return toInt(counters && counters[name]);
|
||||
}
|
||||
setCounter(name: keyof AccountCounters, value: number) {
|
||||
const counters = this.account.counters || (this.account.counters = {});
|
||||
counters[name] = value;
|
||||
this.model.setAccountCounter(this.account._id, name, value);
|
||||
}
|
||||
removeAlert() {
|
||||
this.model.setAlert(this.account._id, '', 0);
|
||||
}
|
||||
setAlert() {
|
||||
this.alertMessage = this.alert ? this.alert.message : '';
|
||||
this.alertExpire = this.alertExpires[2];
|
||||
this.alertModalRef = this.modalService.show(this.alertModal, { ignoreBackdropClick: true });
|
||||
}
|
||||
cancelAlert() {
|
||||
this.alertModalRef && this.alertModalRef.hide();
|
||||
this.alertModalRef = undefined;
|
||||
}
|
||||
confirmAlert() {
|
||||
this.model.setAlert(this.account._id, this.alertMessage, this.alertExpire.length);
|
||||
this.cancelAlert();
|
||||
}
|
||||
private updateDuplicates() {
|
||||
const account = this.account;
|
||||
|
||||
if (this.showDuplicates && account) {
|
||||
const cached = accountDuplicates.get(account._id);
|
||||
const threshold = fromNow(-accountDuplicatesIntervalTime);
|
||||
if (this.showDuplicates && account) {
|
||||
const cached = accountDuplicates.get(account._id);
|
||||
const threshold = fromNow(-accountDuplicatesIntervalTime);
|
||||
|
||||
if (cached && cached.generatedAt > threshold.getTime()) {
|
||||
this.duplicates = cached;
|
||||
} else {
|
||||
this.model.getAllDuplicatesQuickInfo(account._id)
|
||||
.then(duplicates => {
|
||||
if (duplicates) {
|
||||
accountDuplicates.set(account._id, duplicates);
|
||||
this.duplicates = duplicates;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
teleportTo() {
|
||||
this.model.teleportTo(this.account._id);
|
||||
}
|
||||
// supporters
|
||||
get isPatreonOrSupporter() {
|
||||
return !!(this.account.patreon || this.account.supporter || this.account.supporterDeclinedSince);
|
||||
}
|
||||
get supporterClass() {
|
||||
return supporterLevel(this.account) ? 'badge-success' : 'badge-warning';
|
||||
}
|
||||
get supporterTitle() {
|
||||
const supporter = this.account.supporter!;
|
||||
const flagSupporter = (supporter & SupporterFlags.SupporterMask) !== 0;
|
||||
const patreonSupporter = patreonSupporterLevel(this.account);
|
||||
const ignorePatreon = hasFlag(supporter, SupporterFlags.IgnorePatreon);
|
||||
const pastSupporter = hasFlag(supporter, SupporterFlags.PastSupporter);
|
||||
if (cached && cached.generatedAt > threshold.getTime()) {
|
||||
this.duplicates = cached;
|
||||
} else {
|
||||
this.model.getAllDuplicatesQuickInfo(account._id)
|
||||
.then(duplicates => {
|
||||
if (duplicates) {
|
||||
accountDuplicates.set(account._id, duplicates);
|
||||
this.duplicates = duplicates;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
teleportTo() {
|
||||
this.model.teleportTo(this.account._id);
|
||||
}
|
||||
// supporters
|
||||
get isPatreonOrSupporter() {
|
||||
return !!(this.account.patreon || this.account.supporter || this.account.supporterDeclinedSince);
|
||||
}
|
||||
get supporterClass() {
|
||||
return supporterLevel(this.account) ? 'badge-success' : 'badge-warning';
|
||||
}
|
||||
get supporterTitle() {
|
||||
const supporter = this.account.supporter!;
|
||||
const flagSupporter = (supporter & SupporterFlags.SupporterMask) !== 0;
|
||||
const patreonSupporter = patreonSupporterLevel(this.account);
|
||||
const ignorePatreon = hasFlag(supporter, SupporterFlags.IgnorePatreon);
|
||||
const pastSupporter = hasFlag(supporter, SupporterFlags.PastSupporter);
|
||||
|
||||
return compact([
|
||||
flagSupporter && 'flags',
|
||||
patreonSupporter && `patreon`,
|
||||
ignorePatreon && 'ignore',
|
||||
!patreonSupporter && this.account.supporterDeclinedSince && 'declined',
|
||||
pastSupporter && 'past',
|
||||
]).join(', ');
|
||||
}
|
||||
get supporterIcon() {
|
||||
const hasPatreon = patreonSupporterLevel(this.account);
|
||||
const hasIgnoreFlag = hasFlag(this.account.supporter, SupporterFlags.IgnorePatreon);
|
||||
const hasDeclined = !!this.account.supporterDeclinedSince;
|
||||
return hasPatreon ? faPatreon : ((hasIgnoreFlag || !hasDeclined) ? faFlag : faClock);
|
||||
}
|
||||
get hasAnySupporter() {
|
||||
return (this.account.supporter! & SupporterFlags.SupporterMask) !== 0;
|
||||
}
|
||||
get hasPastSupporter() {
|
||||
return hasFlag(this.account.supporter, SupporterFlags.PastSupporter);
|
||||
}
|
||||
get supporterLevel() {
|
||||
return supporterLevel(this.account);
|
||||
}
|
||||
get supporterLevelString() {
|
||||
const level = supporterLevel(this.account);
|
||||
return level ? level : (isPastSupporter(this.account) ? 'P' : '');
|
||||
}
|
||||
isSupporter(level: number) {
|
||||
return (this.account.supporter! & SupporterFlags.SupporterMask) === level;
|
||||
}
|
||||
setSupporter(level: number) {
|
||||
const supporter = (this.account.supporter! & ~SupporterFlags.SupporterMask) | level;
|
||||
this.model.setSupporterFlags(this.account._id, supporter);
|
||||
}
|
||||
hasSupporterFlag(value: SupporterFlags) {
|
||||
return hasFlag(this.account.supporter, value);
|
||||
}
|
||||
toggleSupporterFlag(value: SupporterFlags) {
|
||||
this.model.setSupporterFlags(this.account._id, this.account.supporter! ^ value);
|
||||
}
|
||||
// past supporter
|
||||
get isForcePastSupporter() {
|
||||
return hasFlag(this.account.supporter, SupporterFlags.ForcePastSupporter);
|
||||
}
|
||||
get isIgnorePastSupporter() {
|
||||
return hasFlag(this.account.supporter, SupporterFlags.IgnorePastSupporter);
|
||||
}
|
||||
toggleForcePastSupporter() {
|
||||
const has = hasFlag(this.account.supporter, SupporterFlags.ForcePastSupporter);
|
||||
const supporter = setFlag(this.account.supporter, SupporterFlags.ForcePastSupporter, !has);
|
||||
this.model.setSupporterFlags(this.account._id, supporter);
|
||||
}
|
||||
toggleIgnorePastSupporter() {
|
||||
const has = hasFlag(this.account.supporter, SupporterFlags.IgnorePastSupporter);
|
||||
const supporter = setFlag(this.account.supporter, SupporterFlags.IgnorePastSupporter, !has);
|
||||
this.model.setSupporterFlags(this.account._id, supporter);
|
||||
}
|
||||
return compact([
|
||||
flagSupporter && 'flags',
|
||||
patreonSupporter && `patreon`,
|
||||
ignorePatreon && 'ignore',
|
||||
!patreonSupporter && this.account.supporterDeclinedSince && 'declined',
|
||||
pastSupporter && 'past',
|
||||
]).join(', ');
|
||||
}
|
||||
get supporterIcon() {
|
||||
const hasPatreon = patreonSupporterLevel(this.account);
|
||||
const hasIgnoreFlag = hasFlag(this.account.supporter, SupporterFlags.IgnorePatreon);
|
||||
const hasDeclined = !!this.account.supporterDeclinedSince;
|
||||
return hasPatreon ? faPatreon : ((hasIgnoreFlag || !hasDeclined) ? faFlag : faClock);
|
||||
}
|
||||
get hasAnySupporter() {
|
||||
return (this.account.supporter! & SupporterFlags.SupporterMask) !== 0;
|
||||
}
|
||||
get hasPastSupporter() {
|
||||
return hasFlag(this.account.supporter, SupporterFlags.PastSupporter);
|
||||
}
|
||||
get supporterLevel() {
|
||||
return supporterLevel(this.account);
|
||||
}
|
||||
get supporterLevelString() {
|
||||
const level = supporterLevel(this.account);
|
||||
return level ? level : (isPastSupporter(this.account) ? 'P' : '');
|
||||
}
|
||||
isSupporter(level: number) {
|
||||
return (this.account.supporter! & SupporterFlags.SupporterMask) === level;
|
||||
}
|
||||
setSupporter(level: number) {
|
||||
const supporter = (this.account.supporter! & ~SupporterFlags.SupporterMask) | level;
|
||||
this.model.setSupporterFlags(this.account._id, supporter);
|
||||
}
|
||||
hasSupporterFlag(value: SupporterFlags) {
|
||||
return hasFlag(this.account.supporter, value);
|
||||
}
|
||||
toggleSupporterFlag(value: SupporterFlags) {
|
||||
this.model.setSupporterFlags(this.account._id, this.account.supporter! ^ value);
|
||||
}
|
||||
// past supporter
|
||||
get isForcePastSupporter() {
|
||||
return hasFlag(this.account.supporter, SupporterFlags.ForcePastSupporter);
|
||||
}
|
||||
get isIgnorePastSupporter() {
|
||||
return hasFlag(this.account.supporter, SupporterFlags.IgnorePastSupporter);
|
||||
}
|
||||
toggleForcePastSupporter() {
|
||||
const has = hasFlag(this.account.supporter, SupporterFlags.ForcePastSupporter);
|
||||
const supporter = setFlag(this.account.supporter, SupporterFlags.ForcePastSupporter, !has);
|
||||
this.model.setSupporterFlags(this.account._id, supporter);
|
||||
}
|
||||
toggleIgnorePastSupporter() {
|
||||
const has = hasFlag(this.account.supporter, SupporterFlags.IgnorePastSupporter);
|
||||
const supporter = setFlag(this.account.supporter, SupporterFlags.IgnorePastSupporter, !has);
|
||||
this.model.setSupporterFlags(this.account._id, supporter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,25 +4,25 @@ import { Account, AccountStatus as IAccountStatus } from '../../../../common/adm
|
||||
import { faUserSecret } from '../../../../client/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'account-status',
|
||||
templateUrl: 'account-status.pug',
|
||||
selector: 'account-status',
|
||||
templateUrl: 'account-status.pug',
|
||||
})
|
||||
export class AccountStatus implements OnInit, OnChanges {
|
||||
readonly incognitoIcon = faUserSecret;
|
||||
@Input() account!: Account;
|
||||
@Input() verbose = false;
|
||||
status: IAccountStatus[] | undefined = undefined;
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
ngOnInit() {
|
||||
this.refresh();
|
||||
}
|
||||
ngOnChanges() {
|
||||
this.status = undefined;
|
||||
this.refresh();
|
||||
}
|
||||
refresh() {
|
||||
this.model.getAccountStatus(this.account._id)
|
||||
.then(status => this.status = status);
|
||||
}
|
||||
readonly incognitoIcon = faUserSecret;
|
||||
@Input() account!: Account;
|
||||
@Input() verbose = false;
|
||||
status: IAccountStatus[] | undefined = undefined;
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
ngOnInit() {
|
||||
this.refresh();
|
||||
}
|
||||
ngOnChanges() {
|
||||
this.status = undefined;
|
||||
this.refresh();
|
||||
}
|
||||
refresh() {
|
||||
this.model.getAccountStatus(this.account._id)
|
||||
.then(status => this.status = status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,16 @@ import { getAge } from '../../../../common/adminUtils';
|
||||
const year = (new Date()).getFullYear();
|
||||
|
||||
@Component({
|
||||
selector: 'account-tooltip',
|
||||
templateUrl: 'account-tooltip.pug',
|
||||
selector: 'account-tooltip',
|
||||
templateUrl: 'account-tooltip.pug',
|
||||
})
|
||||
export class AccountTooltip {
|
||||
@Input() account!: Account;
|
||||
@Input() extendedAuths = false;
|
||||
get age() {
|
||||
return this.account.birthdate ? getAge(this.account.birthdate) : '-';
|
||||
}
|
||||
get forceAge() {
|
||||
return this.account.birthyear ? (year - this.account.birthyear) : '';
|
||||
}
|
||||
@Input() account!: Account;
|
||||
@Input() extendedAuths = false;
|
||||
get age() {
|
||||
return this.account.birthdate ? getAge(this.account.birthdate) : '-';
|
||||
}
|
||||
get forceAge() {
|
||||
return this.account.birthyear ? (year - this.account.birthyear) : '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,175 +8,175 @@ import { removeAllNodes, appendAllNodes, showTextInNewTab } from '../../../../cl
|
||||
import { includes } from '../../../../common/utils';
|
||||
|
||||
@Component({
|
||||
selector: 'admin-chat-log',
|
||||
templateUrl: 'admin-chat-log.pug',
|
||||
styleUrls: ['admin-chat-log.scss'],
|
||||
selector: 'admin-chat-log',
|
||||
templateUrl: 'admin-chat-log.pug',
|
||||
styleUrls: ['admin-chat-log.scss'],
|
||||
})
|
||||
export class AdminChatLog implements OnDestroy {
|
||||
readonly searchIcon = faSearch;
|
||||
readonly spinnerIcon = faSpinner;
|
||||
readonly syncIcon = faSync;
|
||||
readonly fileIcon = faFileAlt;
|
||||
readonly closeIcon = faTimes;
|
||||
readonly chevronLeftIcon = faChevronLeft;
|
||||
readonly chevronRightIcon = faChevronRight;
|
||||
@Input() canClose = true;
|
||||
accounts: Account[] = [];
|
||||
search?: string;
|
||||
open = false;
|
||||
today: ChatDate = createChatDate(moment());
|
||||
dates: ChatDate[] = [/*{ value: 'all', label: 'All' },*/ ...createDateRange(new Date(), 14)];
|
||||
date?: ChatDate;
|
||||
chatRaw?: string;
|
||||
loading = false;
|
||||
private _account?: Account;
|
||||
private refreshInterval?: any;
|
||||
constructor(private model: AdminModel, private element: ElementRef) {
|
||||
}
|
||||
get autoRefresh() {
|
||||
return !!this.refreshInterval;
|
||||
}
|
||||
set autoRefresh(value: boolean) {
|
||||
if (value) {
|
||||
this.refreshInterval = this.refreshInterval || setInterval(() => this.refresh(), 10 * 1000);
|
||||
} else {
|
||||
this.stopInterval();
|
||||
}
|
||||
}
|
||||
get title() {
|
||||
return this.search || (this.account && this.account.name) || 'Chat';
|
||||
}
|
||||
get account() {
|
||||
return this._account;
|
||||
}
|
||||
@Input() set account(value) {
|
||||
const theSame = this._account === value || (value && this._account && value._id === this._account._id);
|
||||
this._account = value;
|
||||
readonly searchIcon = faSearch;
|
||||
readonly spinnerIcon = faSpinner;
|
||||
readonly syncIcon = faSync;
|
||||
readonly fileIcon = faFileAlt;
|
||||
readonly closeIcon = faTimes;
|
||||
readonly chevronLeftIcon = faChevronLeft;
|
||||
readonly chevronRightIcon = faChevronRight;
|
||||
@Input() canClose = true;
|
||||
accounts: Account[] = [];
|
||||
search?: string;
|
||||
open = false;
|
||||
today: ChatDate = createChatDate(moment());
|
||||
dates: ChatDate[] = [/*{ value: 'all', label: 'All' },*/ ...createDateRange(new Date(), 14)];
|
||||
date?: ChatDate;
|
||||
chatRaw?: string;
|
||||
loading = false;
|
||||
private _account?: Account;
|
||||
private refreshInterval?: any;
|
||||
constructor(private model: AdminModel, private element: ElementRef) {
|
||||
}
|
||||
get autoRefresh() {
|
||||
return !!this.refreshInterval;
|
||||
}
|
||||
set autoRefresh(value: boolean) {
|
||||
if (value) {
|
||||
this.refreshInterval = this.refreshInterval || setInterval(() => this.refresh(), 10 * 1000);
|
||||
} else {
|
||||
this.stopInterval();
|
||||
}
|
||||
}
|
||||
get title() {
|
||||
return this.search || (this.account && this.account.name) || 'Chat';
|
||||
}
|
||||
get account() {
|
||||
return this._account;
|
||||
}
|
||||
@Input() set account(value) {
|
||||
const theSame = this._account === value || (value && this._account && value._id === this._account._id);
|
||||
this._account = value;
|
||||
|
||||
if (!theSame) {
|
||||
this.date = undefined;
|
||||
this.setChatlogElements([]);
|
||||
}
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.close();
|
||||
}
|
||||
show(account?: Account, date?: ChatDate) {
|
||||
this.search = undefined;
|
||||
this.account = account;
|
||||
this.date = date || this.today;
|
||||
this.open = true;
|
||||
this.accounts = [];
|
||||
this.refresh();
|
||||
}
|
||||
add(account: Account) {
|
||||
if (!this.account) {
|
||||
this.show(account);
|
||||
} else if (account !== this.account && !includes(this.accounts, account)) {
|
||||
this.date = this.date || this.today;
|
||||
this.accounts.push(account);
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
removeAccount(index: number) {
|
||||
this.accounts.splice(index, 1);
|
||||
this.refresh();
|
||||
}
|
||||
showDate(date: ChatDate) {
|
||||
this.date = date;
|
||||
this.refresh();
|
||||
}
|
||||
prev() {
|
||||
this.switchDate(-1);
|
||||
}
|
||||
next() {
|
||||
this.switchDate(1);
|
||||
}
|
||||
all() {
|
||||
this.date = this.dates[0];
|
||||
this.refresh();
|
||||
}
|
||||
close() {
|
||||
this.account = undefined;
|
||||
this.date = undefined;
|
||||
this.open = false;
|
||||
this.setChatlogElements([]);
|
||||
this.stopInterval();
|
||||
}
|
||||
searchChat(search: string | undefined) {
|
||||
this.search = search;
|
||||
this.refresh();
|
||||
}
|
||||
refresh() {
|
||||
const date = this.date && this.date.value;
|
||||
if (!theSame) {
|
||||
this.date = undefined;
|
||||
this.setChatlogElements([]);
|
||||
}
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.close();
|
||||
}
|
||||
show(account?: Account, date?: ChatDate) {
|
||||
this.search = undefined;
|
||||
this.account = account;
|
||||
this.date = date || this.today;
|
||||
this.open = true;
|
||||
this.accounts = [];
|
||||
this.refresh();
|
||||
}
|
||||
add(account: Account) {
|
||||
if (!this.account) {
|
||||
this.show(account);
|
||||
} else if (account !== this.account && !includes(this.accounts, account)) {
|
||||
this.date = this.date || this.today;
|
||||
this.accounts.push(account);
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
removeAccount(index: number) {
|
||||
this.accounts.splice(index, 1);
|
||||
this.refresh();
|
||||
}
|
||||
showDate(date: ChatDate) {
|
||||
this.date = date;
|
||||
this.refresh();
|
||||
}
|
||||
prev() {
|
||||
this.switchDate(-1);
|
||||
}
|
||||
next() {
|
||||
this.switchDate(1);
|
||||
}
|
||||
all() {
|
||||
this.date = this.dates[0];
|
||||
this.refresh();
|
||||
}
|
||||
close() {
|
||||
this.account = undefined;
|
||||
this.date = undefined;
|
||||
this.open = false;
|
||||
this.setChatlogElements([]);
|
||||
this.stopInterval();
|
||||
}
|
||||
searchChat(search: string | undefined) {
|
||||
this.search = search;
|
||||
this.refresh();
|
||||
}
|
||||
refresh() {
|
||||
const date = this.date && this.date.value;
|
||||
|
||||
if (this.account) {
|
||||
const accounts = [this.account._id, ...this.accounts.map(a => a._id)];
|
||||
this.handleChat(this.model.accountsFormattedChat(accounts, date));
|
||||
} else if (this.search) {
|
||||
this.handleChat(this.model.searchFormattedChat(this.search, date));
|
||||
}
|
||||
}
|
||||
openLog() {
|
||||
showTextInNewTab(`${this.date ? this.date.label : 'none'}\n\n${(this.chatRaw || '').replace(/\t/g, ' ')}`);
|
||||
}
|
||||
private handleChat(promise: Promise<{ raw: string; html: HTMLElement[]; }>) {
|
||||
this.loading = true;
|
||||
if (this.account) {
|
||||
const accounts = [this.account._id, ...this.accounts.map(a => a._id)];
|
||||
this.handleChat(this.model.accountsFormattedChat(accounts, date));
|
||||
} else if (this.search) {
|
||||
this.handleChat(this.model.searchFormattedChat(this.search, date));
|
||||
}
|
||||
}
|
||||
openLog() {
|
||||
showTextInNewTab(`${this.date ? this.date.label : 'none'}\n\n${(this.chatRaw || '').replace(/\t/g, ' ')}`);
|
||||
}
|
||||
private handleChat(promise: Promise<{ raw: string; html: HTMLElement[]; }>) {
|
||||
this.loading = true;
|
||||
|
||||
promise
|
||||
.then(({ raw, html }) => {
|
||||
this.chatRaw = raw;
|
||||
this.setChatlogElements(html);
|
||||
})
|
||||
.finally(() => this.loading = false);
|
||||
}
|
||||
private switchDate(days: number) {
|
||||
const validDate = this.date && this.date.value !== 'all';
|
||||
this.date = validDate ? createChatDate(moment(this.date!.value).add(days, 'days')) : this.today;
|
||||
this.refresh();
|
||||
}
|
||||
private stopInterval() {
|
||||
clearInterval(this.refreshInterval);
|
||||
this.refreshInterval = undefined;
|
||||
}
|
||||
private getChatlogElement() {
|
||||
return (this.element.nativeElement as HTMLElement).querySelector('.chatlog');
|
||||
}
|
||||
private setChatlogElements(elements: HTMLElement[]) {
|
||||
const element = this.getChatlogElement();
|
||||
promise
|
||||
.then(({ raw, html }) => {
|
||||
this.chatRaw = raw;
|
||||
this.setChatlogElements(html);
|
||||
})
|
||||
.finally(() => this.loading = false);
|
||||
}
|
||||
private switchDate(days: number) {
|
||||
const validDate = this.date && this.date.value !== 'all';
|
||||
this.date = validDate ? createChatDate(moment(this.date!.value).add(days, 'days')) : this.today;
|
||||
this.refresh();
|
||||
}
|
||||
private stopInterval() {
|
||||
clearInterval(this.refreshInterval);
|
||||
this.refreshInterval = undefined;
|
||||
}
|
||||
private getChatlogElement() {
|
||||
return (this.element.nativeElement as HTMLElement).querySelector('.chatlog');
|
||||
}
|
||||
private setChatlogElements(elements: HTMLElement[]) {
|
||||
const element = this.getChatlogElement();
|
||||
|
||||
if (element) {
|
||||
removeAllNodes(element);
|
||||
appendAllNodes(element, elements);
|
||||
this.nodesToProcess = [
|
||||
...Array.from(element.getElementsByClassName('name')),
|
||||
...Array.from(element.getElementsByClassName('message')),
|
||||
] as HTMLElement[];
|
||||
this.atNode = 0;
|
||||
this.processNodes();
|
||||
}
|
||||
}
|
||||
private processNodes() {
|
||||
const processStep = 50;
|
||||
const nodes = this.nodesToProcess;
|
||||
if (element) {
|
||||
removeAllNodes(element);
|
||||
appendAllNodes(element, elements);
|
||||
this.nodesToProcess = [
|
||||
...Array.from(element.getElementsByClassName('name')),
|
||||
...Array.from(element.getElementsByClassName('message')),
|
||||
] as HTMLElement[];
|
||||
this.atNode = 0;
|
||||
this.processNodes();
|
||||
}
|
||||
}
|
||||
private processNodes() {
|
||||
const processStep = 50;
|
||||
const nodes = this.nodesToProcess;
|
||||
|
||||
cancelIdleCallback(this.processIdle);
|
||||
cancelIdleCallback(this.processIdle);
|
||||
|
||||
if (nodes && this.atNode < nodes.length) {
|
||||
let i = 0;
|
||||
if (nodes && this.atNode < nodes.length) {
|
||||
let i = 0;
|
||||
|
||||
while (i < processStep && (i + this.atNode) < nodes.length) {
|
||||
replaceSwears(nodes[i + this.atNode]);
|
||||
i++;
|
||||
}
|
||||
while (i < processStep && (i + this.atNode) < nodes.length) {
|
||||
replaceSwears(nodes[i + this.atNode]);
|
||||
i++;
|
||||
}
|
||||
|
||||
this.atNode += i;
|
||||
this.processIdle = requestIdleCallback(() => this.processNodes());
|
||||
} else {
|
||||
this.nodesToProcess = undefined;
|
||||
}
|
||||
}
|
||||
private nodesToProcess?: HTMLElement[];
|
||||
private atNode = 0;
|
||||
private processIdle = 0;
|
||||
this.atNode += i;
|
||||
this.processIdle = requestIdleCallback(() => this.processNodes());
|
||||
} else {
|
||||
this.nodesToProcess = undefined;
|
||||
}
|
||||
}
|
||||
private nodesToProcess?: HTMLElement[];
|
||||
private atNode = 0;
|
||||
private processIdle = 0;
|
||||
}
|
||||
|
||||
@@ -5,60 +5,60 @@ import { AdminModel } from '../../../services/adminModel';
|
||||
import { faInfo, faLock, faTrash, faEyeSlash, faArrowRight } from '../../../../client/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'auth-info-edit',
|
||||
templateUrl: 'auth-info-edit.pug',
|
||||
selector: 'auth-info-edit',
|
||||
templateUrl: 'auth-info-edit.pug',
|
||||
})
|
||||
export class AuthInfoEdit implements OnDestroy {
|
||||
readonly assignIcon = faArrowRight;
|
||||
readonly infoIcon = faInfo;
|
||||
readonly lockIcon = faLock;
|
||||
readonly trashIcon = faTrash;
|
||||
readonly eyeSlashIcon = faEyeSlash;
|
||||
@Input() duplicates?: DuplicateResult[];
|
||||
@Input() showName = false;
|
||||
auth?: Auth;
|
||||
private _authId?: string;
|
||||
private subscription?: Subscription;
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
get authId() {
|
||||
return this._authId;
|
||||
}
|
||||
@Input() set authId(value: string | undefined) {
|
||||
if (this.authId !== value) {
|
||||
this._authId = value;
|
||||
this.auth = undefined;
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
this.subscription = value ? this.model.auths.subscribe(value, auth => this.auth = auth) : undefined;
|
||||
}
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
removeAuth(auth: Auth | undefined) {
|
||||
if (auth && confirm('Are you sure?')) {
|
||||
this.model.removeAuth(auth._id);
|
||||
}
|
||||
}
|
||||
showAuthData(auth: Auth | undefined) {
|
||||
if (auth) {
|
||||
this.model.getAuth(auth._id)
|
||||
.then(x => console.log(x));
|
||||
}
|
||||
}
|
||||
toggleAuthDisabled(auth: Auth | undefined) {
|
||||
if (auth) {
|
||||
this.model.updateAuth(auth._id, { disabled: !auth.disabled });
|
||||
}
|
||||
}
|
||||
toggleAuthBanned(auth: Auth | undefined) {
|
||||
if (auth) {
|
||||
this.model.updateAuth(auth._id, { banned: !auth.banned });
|
||||
}
|
||||
}
|
||||
assignTo(accountId: string) {
|
||||
if (this.authId) {
|
||||
this.model.assignAuth(this.authId, accountId);
|
||||
}
|
||||
}
|
||||
readonly assignIcon = faArrowRight;
|
||||
readonly infoIcon = faInfo;
|
||||
readonly lockIcon = faLock;
|
||||
readonly trashIcon = faTrash;
|
||||
readonly eyeSlashIcon = faEyeSlash;
|
||||
@Input() duplicates?: DuplicateResult[];
|
||||
@Input() showName = false;
|
||||
auth?: Auth;
|
||||
private _authId?: string;
|
||||
private subscription?: Subscription;
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
get authId() {
|
||||
return this._authId;
|
||||
}
|
||||
@Input() set authId(value: string | undefined) {
|
||||
if (this.authId !== value) {
|
||||
this._authId = value;
|
||||
this.auth = undefined;
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
this.subscription = value ? this.model.auths.subscribe(value, auth => this.auth = auth) : undefined;
|
||||
}
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
removeAuth(auth: Auth | undefined) {
|
||||
if (auth && confirm('Are you sure?')) {
|
||||
this.model.removeAuth(auth._id);
|
||||
}
|
||||
}
|
||||
showAuthData(auth: Auth | undefined) {
|
||||
if (auth) {
|
||||
this.model.getAuth(auth._id)
|
||||
.then(x => console.log(x));
|
||||
}
|
||||
}
|
||||
toggleAuthDisabled(auth: Auth | undefined) {
|
||||
if (auth) {
|
||||
this.model.updateAuth(auth._id, { disabled: !auth.disabled });
|
||||
}
|
||||
}
|
||||
toggleAuthBanned(auth: Auth | undefined) {
|
||||
if (auth) {
|
||||
this.model.updateAuth(auth._id, { banned: !auth.banned });
|
||||
}
|
||||
}
|
||||
assignTo(accountId: string) {
|
||||
if (this.authId) {
|
||||
this.model.assignAuth(this.authId, accountId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,28 +4,28 @@ import { AdminModel } from '../../../services/adminModel';
|
||||
import { Auth } from '../../../../common/adminInterfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'auth-info-remote',
|
||||
templateUrl: 'auth-info-remote.pug',
|
||||
selector: 'auth-info-remote',
|
||||
templateUrl: 'auth-info-remote.pug',
|
||||
})
|
||||
export class AuthInfoRemote implements OnDestroy {
|
||||
@Input() showName = false;
|
||||
auth?: Auth;
|
||||
private _authId?: string;
|
||||
private subscription?: Subscription;
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
get authId() {
|
||||
return this._authId;
|
||||
}
|
||||
@Input() set authId(value: string | undefined) {
|
||||
if (this.authId !== value) {
|
||||
this._authId = value;
|
||||
this.auth = undefined;
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
this.subscription = value ? this.model.auths.subscribe(value, auth => this.auth = auth) : undefined;
|
||||
}
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
@Input() showName = false;
|
||||
auth?: Auth;
|
||||
private _authId?: string;
|
||||
private subscription?: Subscription;
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
get authId() {
|
||||
return this._authId;
|
||||
}
|
||||
@Input() set authId(value: string | undefined) {
|
||||
if (this.authId !== value) {
|
||||
this._authId = value;
|
||||
this.auth = undefined;
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
this.subscription = value ? this.model.auths.subscribe(value, auth => this.auth = auth) : undefined;
|
||||
}
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,26 +3,26 @@ import { Auth } from '../../../../common/adminInterfaces';
|
||||
import { oauthIcons, faGlobe } from '../../../../client/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'auth-info',
|
||||
templateUrl: 'auth-info.pug',
|
||||
styleUrls: ['auth-info.scss'],
|
||||
host: {
|
||||
'[class.deleted]': 'deleted',
|
||||
},
|
||||
selector: 'auth-info',
|
||||
templateUrl: 'auth-info.pug',
|
||||
styleUrls: ['auth-info.scss'],
|
||||
host: {
|
||||
'[class.deleted]': 'deleted',
|
||||
},
|
||||
})
|
||||
export class AuthInfo {
|
||||
@Input() auth?: Auth;
|
||||
@Input() showName = false;
|
||||
get deleted(): boolean {
|
||||
return !!(this.auth && (this.auth.disabled || this.auth.banned));
|
||||
}
|
||||
get name(): string {
|
||||
return this.auth && this.auth.name || '';
|
||||
}
|
||||
get icon() {
|
||||
return oauthIcons[this.auth && this.auth.provider || ''] || faGlobe;
|
||||
}
|
||||
get pledged() {
|
||||
return (this.auth && this.auth.pledged || 0) / 100;
|
||||
}
|
||||
@Input() auth?: Auth;
|
||||
@Input() showName = false;
|
||||
get deleted(): boolean {
|
||||
return !!(this.auth && (this.auth.disabled || this.auth.banned));
|
||||
}
|
||||
get name(): string {
|
||||
return this.auth && this.auth.name || '';
|
||||
}
|
||||
get icon() {
|
||||
return oauthIcons[this.auth && this.auth.provider || ''] || faGlobe;
|
||||
}
|
||||
get pledged() {
|
||||
return (this.auth && this.auth.pledged || 0) / 100;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,35 +3,35 @@ import { Subscription } from '../../../../common/interfaces';
|
||||
import { AdminModel } from '../../../services/adminModel';
|
||||
|
||||
@Component({
|
||||
selector: 'auth-list-remote',
|
||||
templateUrl: 'auth-list-remote.pug',
|
||||
styleUrls: ['auth-list-remote.scss'],
|
||||
selector: 'auth-list-remote',
|
||||
templateUrl: 'auth-list-remote.pug',
|
||||
styleUrls: ['auth-list-remote.scss'],
|
||||
})
|
||||
export class AuthListRemote implements OnDestroy {
|
||||
@Input() limit = 6;
|
||||
@Input() extended = false;
|
||||
auths: string[] = [];
|
||||
loading = false;
|
||||
private _accountId?: string;
|
||||
private subscription?: Subscription;
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
get accountId() {
|
||||
return this._accountId;
|
||||
}
|
||||
@Input() set accountId(value: string | undefined) {
|
||||
if (this.accountId !== value) {
|
||||
this._accountId = value;
|
||||
this.auths = [];
|
||||
this.loading = true;
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
this.subscription = value ? this.model.accountAuths.subscribe(value, auths => {
|
||||
this.auths = auths || [];
|
||||
this.loading = false;
|
||||
}) : undefined;
|
||||
}
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
@Input() limit = 6;
|
||||
@Input() extended = false;
|
||||
auths: string[] = [];
|
||||
loading = false;
|
||||
private _accountId?: string;
|
||||
private subscription?: Subscription;
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
get accountId() {
|
||||
return this._accountId;
|
||||
}
|
||||
@Input() set accountId(value: string | undefined) {
|
||||
if (this.accountId !== value) {
|
||||
this._accountId = value;
|
||||
this.auths = [];
|
||||
this.loading = true;
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
this.subscription = value ? this.model.accountAuths.subscribe(value, auths => {
|
||||
this.auths = auths || [];
|
||||
this.loading = false;
|
||||
}) : undefined;
|
||||
}
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,18 @@ import { Component, Input } from '@angular/core';
|
||||
import { Auth } from '../../../../common/adminInterfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'auth-list',
|
||||
templateUrl: 'auth-list.pug',
|
||||
styleUrls: ['auth-list.scss'],
|
||||
host: {
|
||||
'[class.extended]': 'extended',
|
||||
},
|
||||
selector: 'auth-list',
|
||||
templateUrl: 'auth-list.pug',
|
||||
styleUrls: ['auth-list.scss'],
|
||||
host: {
|
||||
'[class.extended]': 'extended',
|
||||
},
|
||||
})
|
||||
export class AuthList {
|
||||
@Input() limit = 6;
|
||||
@Input() extended = false;
|
||||
@Input() auths?: Auth[];
|
||||
get fixedAuths() {
|
||||
return this.auths || [];
|
||||
}
|
||||
@Input() limit = 6;
|
||||
@Input() extended = false;
|
||||
@Input() auths?: Auth[];
|
||||
get fixedAuths() {
|
||||
return this.auths || [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
Component, Input, EventEmitter, Output, ChangeDetectionStrategy, OnInit, OnDestroy, OnChanges, NgZone
|
||||
Component, Input, EventEmitter, Output, ChangeDetectionStrategy, OnInit, OnDestroy, OnChanges, NgZone
|
||||
} from '@angular/core';
|
||||
import { TIMEOUTS } from '../../../../common/constants';
|
||||
import { BannedMuted } from '../../../../common/adminInterfaces';
|
||||
@@ -7,71 +7,71 @@ import { IntervalUpdateService } from '../../../services/intervalUpdateService';
|
||||
import { faClock, faMicrophoneSlash, faEyeSlash, faBan } from '../../../../client/icons';
|
||||
|
||||
const ICONS = {
|
||||
mute: faMicrophoneSlash,
|
||||
shadow: faEyeSlash,
|
||||
ban: faBan,
|
||||
mute: faMicrophoneSlash,
|
||||
shadow: faEyeSlash,
|
||||
ban: faBan,
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'ban-icon',
|
||||
templateUrl: 'ban-icon.pug',
|
||||
styleUrls: ['ban-icon.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'ban-icon',
|
||||
templateUrl: 'ban-icon.pug',
|
||||
styleUrls: ['ban-icon.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BanIcon implements OnInit, OnDestroy, OnChanges {
|
||||
readonly timeouts = TIMEOUTS;
|
||||
readonly clockIcon = faClock;
|
||||
@Input() type: keyof BannedMuted = 'ban';
|
||||
@Input() value = 0;
|
||||
@Output() toggle = new EventEmitter<number>();
|
||||
get icon() {
|
||||
return ICONS[this.type] || ICONS.ban;
|
||||
}
|
||||
get isPerma() {
|
||||
return this.value === -1;
|
||||
}
|
||||
get isTimedOut() {
|
||||
return this.value > Date.now();
|
||||
}
|
||||
get className() {
|
||||
if (this.isPerma) {
|
||||
return 'text-banned';
|
||||
} else if (this.isTimedOut) {
|
||||
return 'text-alert';
|
||||
} else {
|
||||
return 'text-muted';
|
||||
}
|
||||
}
|
||||
private timedOut = false;
|
||||
private toggleUpdate: (on: boolean) => void;
|
||||
constructor(zone: NgZone, updateService: IntervalUpdateService) {
|
||||
this.toggleUpdate = updateService.toggle(() => {
|
||||
if (this.timedOut !== this.isTimedOut) {
|
||||
zone.run(() => this.timedOut = this.isTimedOut);
|
||||
this.toggleUpdate(this.isTimedOut);
|
||||
}
|
||||
});
|
||||
}
|
||||
ngOnInit() {
|
||||
this.toggleUpdate(this.isTimedOut);
|
||||
}
|
||||
ngOnChanges() {
|
||||
this.toggleUpdate(this.isTimedOut);
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.toggleUpdate(false);
|
||||
}
|
||||
clear() {
|
||||
this.setValue(0);
|
||||
}
|
||||
perma() {
|
||||
this.setValue(-1);
|
||||
}
|
||||
timeout(value: number) {
|
||||
this.setValue(Date.now() + value);
|
||||
}
|
||||
private setValue(value: number) {
|
||||
this.value = value;
|
||||
this.toggle.emit(value);
|
||||
}
|
||||
readonly timeouts = TIMEOUTS;
|
||||
readonly clockIcon = faClock;
|
||||
@Input() type: keyof BannedMuted = 'ban';
|
||||
@Input() value = 0;
|
||||
@Output() toggle = new EventEmitter<number>();
|
||||
get icon() {
|
||||
return ICONS[this.type] || ICONS.ban;
|
||||
}
|
||||
get isPerma() {
|
||||
return this.value === -1;
|
||||
}
|
||||
get isTimedOut() {
|
||||
return this.value > Date.now();
|
||||
}
|
||||
get className() {
|
||||
if (this.isPerma) {
|
||||
return 'text-banned';
|
||||
} else if (this.isTimedOut) {
|
||||
return 'text-alert';
|
||||
} else {
|
||||
return 'text-muted';
|
||||
}
|
||||
}
|
||||
private timedOut = false;
|
||||
private toggleUpdate: (on: boolean) => void;
|
||||
constructor(zone: NgZone, updateService: IntervalUpdateService) {
|
||||
this.toggleUpdate = updateService.toggle(() => {
|
||||
if (this.timedOut !== this.isTimedOut) {
|
||||
zone.run(() => this.timedOut = this.isTimedOut);
|
||||
this.toggleUpdate(this.isTimedOut);
|
||||
}
|
||||
});
|
||||
}
|
||||
ngOnInit() {
|
||||
this.toggleUpdate(this.isTimedOut);
|
||||
}
|
||||
ngOnChanges() {
|
||||
this.toggleUpdate(this.isTimedOut);
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.toggleUpdate(false);
|
||||
}
|
||||
clear() {
|
||||
this.setValue(0);
|
||||
}
|
||||
perma() {
|
||||
this.setValue(-1);
|
||||
}
|
||||
timeout(value: number) {
|
||||
this.setValue(Date.now() + value);
|
||||
}
|
||||
private setValue(value: number) {
|
||||
this.value = value;
|
||||
this.toggle.emit(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'email-list',
|
||||
templateUrl: 'email-list.pug',
|
||||
selector: 'email-list',
|
||||
templateUrl: 'email-list.pug',
|
||||
})
|
||||
export class EmailList {
|
||||
@Input() emails?: string[];
|
||||
limit = 3;
|
||||
get hasMore() {
|
||||
return this.emails && this.emails.length > this.limit;
|
||||
}
|
||||
showMore() {
|
||||
this.limit = 9999;
|
||||
}
|
||||
@Input() emails?: string[];
|
||||
limit = 3;
|
||||
get hasMore() {
|
||||
return this.emails && this.emails.length > this.limit;
|
||||
}
|
||||
showMore() {
|
||||
this.limit = 9999;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,52 +5,52 @@ import { faLanguage, faTrash, faComment, faClipboard } from '../../../../client/
|
||||
import { getTranslationUrl } from '../../../../common/adminUtils';
|
||||
|
||||
@Component({
|
||||
selector: 'events-table',
|
||||
templateUrl: 'events-table.pug',
|
||||
styleUrls: ['events-table.scss'],
|
||||
selector: 'events-table',
|
||||
templateUrl: 'events-table.pug',
|
||||
styleUrls: ['events-table.scss'],
|
||||
})
|
||||
export class EventsTable {
|
||||
readonly clipboardIcon = faClipboard;
|
||||
readonly langIcon = faLanguage;
|
||||
readonly trashIcon = faTrash;
|
||||
readonly commentIcon = faComment;
|
||||
@Input() events!: Event[];
|
||||
@Output() showChat = new EventEmitter<ChatEvent>();
|
||||
@Output() addChat = new EventEmitter<ChatEvent>();
|
||||
@Output() removedEvent = new EventEmitter<Event>();
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
serverLabel(e: Event) {
|
||||
return SERVER_LABELS[e.server] || 'badge-none';
|
||||
}
|
||||
remove(e: Event) {
|
||||
this.model.removeEvent(e._id);
|
||||
this.removedEvent.emit(e);
|
||||
}
|
||||
removeAll(e: Event) {
|
||||
this.model.events
|
||||
.filter(x => x.message === e.message)
|
||||
.forEach(x => this.model.removeEvent(x._id));
|
||||
}
|
||||
copyToNotes(e: Event, account: Account | undefined) {
|
||||
if (account) {
|
||||
const desc = e.desc ? `: ${e.desc}` : '';
|
||||
const count = e.count > 1 ? `[${e.count}] ` : '';
|
||||
const note = `${(account.note || '')}\r\n[${e.server}]${count}${e.message}${desc}`;
|
||||
this.model.setNote(account._id, note.trim());
|
||||
}
|
||||
}
|
||||
translateUrl(e: Event) {
|
||||
return getTranslationUrl(e.desc);
|
||||
}
|
||||
onShowChat(e: MouseEvent, event: Event, account: Account | undefined) {
|
||||
if (e.shiftKey) {
|
||||
this.addChat.emit({ event, account });
|
||||
} else {
|
||||
this.showChat.emit({ event, account });
|
||||
}
|
||||
}
|
||||
onAddChat(event: Event, account: Account | undefined) {
|
||||
this.addChat.emit({ event, account });
|
||||
}
|
||||
readonly clipboardIcon = faClipboard;
|
||||
readonly langIcon = faLanguage;
|
||||
readonly trashIcon = faTrash;
|
||||
readonly commentIcon = faComment;
|
||||
@Input() events!: Event[];
|
||||
@Output() showChat = new EventEmitter<ChatEvent>();
|
||||
@Output() addChat = new EventEmitter<ChatEvent>();
|
||||
@Output() removedEvent = new EventEmitter<Event>();
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
serverLabel(e: Event) {
|
||||
return SERVER_LABELS[e.server] || 'badge-none';
|
||||
}
|
||||
remove(e: Event) {
|
||||
this.model.removeEvent(e._id);
|
||||
this.removedEvent.emit(e);
|
||||
}
|
||||
removeAll(e: Event) {
|
||||
this.model.events
|
||||
.filter(x => x.message === e.message)
|
||||
.forEach(x => this.model.removeEvent(x._id));
|
||||
}
|
||||
copyToNotes(e: Event, account: Account | undefined) {
|
||||
if (account) {
|
||||
const desc = e.desc ? `: ${e.desc}` : '';
|
||||
const count = e.count > 1 ? `[${e.count}] ` : '';
|
||||
const note = `${(account.note || '')}\r\n[${e.server}]${count}${e.message}${desc}`;
|
||||
this.model.setNote(account._id, note.trim());
|
||||
}
|
||||
}
|
||||
translateUrl(e: Event) {
|
||||
return getTranslationUrl(e.desc);
|
||||
}
|
||||
onShowChat(e: MouseEvent, event: Event, account: Account | undefined) {
|
||||
if (e.shiftKey) {
|
||||
this.addChat.emit({ event, account });
|
||||
} else {
|
||||
this.showChat.emit({ event, account });
|
||||
}
|
||||
}
|
||||
onAddChat(event: Event, account: Account | undefined) {
|
||||
this.addChat.emit({ event, account });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,34 +3,34 @@ import * as moment from 'moment';
|
||||
import { IntervalUpdateService } from '../../services/intervalUpdateService';
|
||||
|
||||
@Component({
|
||||
selector: 'from-now',
|
||||
template: '<span></span>',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'from-now',
|
||||
template: '<span></span>',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FromNow implements OnInit, OnDestroy, OnChanges {
|
||||
@Input() time?: any;
|
||||
private moment?: moment.Moment;
|
||||
private text?: string;
|
||||
private unsubscribe?: () => void;
|
||||
constructor(private element: ElementRef, private updateService: IntervalUpdateService) {
|
||||
}
|
||||
ngOnChanges() {
|
||||
this.moment = this.time ? moment(this.time) : undefined;
|
||||
this.update();
|
||||
}
|
||||
ngOnInit() {
|
||||
this.unsubscribe = this.updateService.subscribe(() => this.update());
|
||||
this.update();
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.unsubscribe && this.unsubscribe();
|
||||
}
|
||||
private update() {
|
||||
const text = this.moment ? this.moment.fromNow(true).replace('seconds', 'secs') : '';
|
||||
@Input() time?: any;
|
||||
private moment?: moment.Moment;
|
||||
private text?: string;
|
||||
private unsubscribe?: () => void;
|
||||
constructor(private element: ElementRef, private updateService: IntervalUpdateService) {
|
||||
}
|
||||
ngOnChanges() {
|
||||
this.moment = this.time ? moment(this.time) : undefined;
|
||||
this.update();
|
||||
}
|
||||
ngOnInit() {
|
||||
this.unsubscribe = this.updateService.subscribe(() => this.update());
|
||||
this.update();
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.unsubscribe && this.unsubscribe();
|
||||
}
|
||||
private update() {
|
||||
const text = this.moment ? this.moment.fromNow(true).replace('seconds', 'secs') : '';
|
||||
|
||||
if (this.text !== text) {
|
||||
this.text = text;
|
||||
(this.element.nativeElement as HTMLElement).children[0].textContent = text;
|
||||
}
|
||||
}
|
||||
if (this.text !== text) {
|
||||
this.text = text;
|
||||
(this.element.nativeElement as HTMLElement).children[0].textContent = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'on-off-switch',
|
||||
templateUrl: 'on-off-switch.pug',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'on-off-switch',
|
||||
templateUrl: 'on-off-switch.pug',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class OnOffSwitch {
|
||||
@Input() on = false;
|
||||
@Input() disabled = false;
|
||||
@Input() onText = 'ON';
|
||||
@Input() offText = 'OFF';
|
||||
@Input() label = '';
|
||||
@Output() toggle = new EventEmitter<boolean>();
|
||||
onToggle(value: boolean) {
|
||||
if (value !== this.on) {
|
||||
this.toggle.emit(value);
|
||||
}
|
||||
}
|
||||
@Input() on = false;
|
||||
@Input() disabled = false;
|
||||
@Input() onText = 'ON';
|
||||
@Input() offText = 'OFF';
|
||||
@Input() label = '';
|
||||
@Output() toggle = new EventEmitter<boolean>();
|
||||
onToggle(value: boolean) {
|
||||
if (value !== this.on) {
|
||||
this.toggle.emit(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,28 +4,28 @@ import { AdminModel } from '../../../services/adminModel';
|
||||
import { Origin } from '../../../../common/adminInterfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'origin-info-remote',
|
||||
templateUrl: 'origin-info-remote.pug',
|
||||
selector: 'origin-info-remote',
|
||||
templateUrl: 'origin-info-remote.pug',
|
||||
})
|
||||
export class OriginInfoRemote implements OnDestroy {
|
||||
@Input() showName = false;
|
||||
origin?: Origin;
|
||||
private _originIP?: string;
|
||||
private subscription?: Subscription;
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
get originIP() {
|
||||
return this._originIP;
|
||||
}
|
||||
@Input() set originIP(value: string | undefined) {
|
||||
if (this.originIP !== value) {
|
||||
this._originIP = value;
|
||||
this.origin = undefined;
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
this.subscription = value ? this.model.origins.subscribe(value, origin => this.origin = origin) : undefined;
|
||||
}
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
@Input() showName = false;
|
||||
origin?: Origin;
|
||||
private _originIP?: string;
|
||||
private subscription?: Subscription;
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
get originIP() {
|
||||
return this._originIP;
|
||||
}
|
||||
@Input() set originIP(value: string | undefined) {
|
||||
if (this.originIP !== value) {
|
||||
this._originIP = value;
|
||||
this.origin = undefined;
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
this.subscription = value ? this.model.origins.subscribe(value, origin => this.origin = origin) : undefined;
|
||||
}
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,20 +4,20 @@ import { AdminModel } from '../../../services/adminModel';
|
||||
import { countryCodeToName } from '../../../../common/countries';
|
||||
|
||||
@Component({
|
||||
selector: 'origin-info',
|
||||
templateUrl: 'origin-info.pug',
|
||||
styleUrls: ['origin-info.scss'],
|
||||
selector: 'origin-info',
|
||||
templateUrl: 'origin-info.pug',
|
||||
styleUrls: ['origin-info.scss'],
|
||||
})
|
||||
export class OriginInfo {
|
||||
@Input() origin?: Origin;
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
get countryName() {
|
||||
return countryCodeToName[this.origin && this.origin.country || '??'] || 'Unknown';
|
||||
}
|
||||
toggleBan(field: keyof BannedMuted, value: number) {
|
||||
if (this.origin) {
|
||||
this.model.updateOrigin({ ip: this.origin.ip, country: this.origin.country, [field]: value });
|
||||
}
|
||||
}
|
||||
@Input() origin?: Origin;
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
get countryName() {
|
||||
return countryCodeToName[this.origin && this.origin.country || '??'] || 'Unknown';
|
||||
}
|
||||
toggleBan(field: keyof BannedMuted, value: number) {
|
||||
if (this.origin) {
|
||||
this.model.updateOrigin({ ip: this.origin.ip, country: this.origin.country, [field]: value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,29 +4,29 @@ import { AdminModel } from '../../../services/adminModel';
|
||||
import { OriginInfoBase } from '../../../../common/adminInterfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'origin-list-remote',
|
||||
templateUrl: 'origin-list-remote.pug',
|
||||
selector: 'origin-list-remote',
|
||||
templateUrl: 'origin-list-remote.pug',
|
||||
})
|
||||
export class OriginListRemote implements OnDestroy {
|
||||
@Input() limit = 2;
|
||||
@Input() extended = false;
|
||||
origins: OriginInfoBase[] = [];
|
||||
private _accountId?: string;
|
||||
private subscription?: Subscription;
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
get accountId() {
|
||||
return this._accountId;
|
||||
}
|
||||
@Input() set accountId(value: string | undefined) {
|
||||
if (this.accountId !== value) {
|
||||
this._accountId = value;
|
||||
this.origins = [];
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
this.subscription = value ? this.model.accountOrigins.subscribe(value, x => this.origins = x || []) : undefined;
|
||||
}
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
@Input() limit = 2;
|
||||
@Input() extended = false;
|
||||
origins: OriginInfoBase[] = [];
|
||||
private _accountId?: string;
|
||||
private subscription?: Subscription;
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
get accountId() {
|
||||
return this._accountId;
|
||||
}
|
||||
@Input() set accountId(value: string | undefined) {
|
||||
if (this.accountId !== value) {
|
||||
this._accountId = value;
|
||||
this.origins = [];
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
this.subscription = value ? this.model.accountOrigins.subscribe(value, x => this.origins = x || []) : undefined;
|
||||
}
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,29 +4,29 @@ import { AdminModel } from '../../../services/adminModel';
|
||||
import { Character } from '../../../../common/adminInterfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'pony-info-remote',
|
||||
templateUrl: 'pony-info-remote.pug',
|
||||
selector: 'pony-info-remote',
|
||||
templateUrl: 'pony-info-remote.pug',
|
||||
})
|
||||
export class PonyInfoRemote implements OnDestroy {
|
||||
@Input() highlight = false;
|
||||
@Input() showName = false;
|
||||
pony?: Character;
|
||||
private _ponyId?: string;
|
||||
private subscription?: Subscription;
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
get ponyId() {
|
||||
return this._ponyId;
|
||||
}
|
||||
@Input() set ponyId(value: string | undefined) {
|
||||
if (this.ponyId !== value) {
|
||||
this._ponyId = value;
|
||||
this.pony = undefined;
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
this.subscription = value ? this.model.ponies.subscribe(value, pony => this.pony = pony) : undefined;
|
||||
}
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
@Input() highlight = false;
|
||||
@Input() showName = false;
|
||||
pony?: Character;
|
||||
private _ponyId?: string;
|
||||
private subscription?: Subscription;
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
get ponyId() {
|
||||
return this._ponyId;
|
||||
}
|
||||
@Input() set ponyId(value: string | undefined) {
|
||||
if (this.ponyId !== value) {
|
||||
this._ponyId = value;
|
||||
this.pony = undefined;
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
this.subscription = value ? this.model.ponies.subscribe(value, pony => this.pony = pony) : undefined;
|
||||
}
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,38 +5,38 @@ import { isForbiddenName } from '../../../../common/security';
|
||||
import { AdminModel } from '../../../services/adminModel';
|
||||
|
||||
@Component({
|
||||
selector: 'pony-info',
|
||||
templateUrl: 'pony-info.pug',
|
||||
styleUrls: ['pony-info.scss'],
|
||||
selector: 'pony-info',
|
||||
templateUrl: 'pony-info.pug',
|
||||
styleUrls: ['pony-info.scss'],
|
||||
})
|
||||
export class PonyInfo implements OnChanges {
|
||||
@Input() pony?: Character;
|
||||
@Input() highlight = false;
|
||||
labelClass = 'badge-none';
|
||||
private promise?: Promise<void>;
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
get isBadCM() {
|
||||
return !!this.pony && hasFlag(this.pony.flags, CharacterFlags.BadCM);
|
||||
}
|
||||
ngOnChanges() {
|
||||
if (this.pony) {
|
||||
if (isForbiddenName(this.pony.name)) {
|
||||
this.labelClass = 'badge-forbidden';
|
||||
} else if (hasFlag(this.pony.flags, CharacterFlags.BadCM)) {
|
||||
this.labelClass = 'badge-danger';
|
||||
} else {
|
||||
this.labelClass = 'badge-none';
|
||||
}
|
||||
}
|
||||
}
|
||||
onShown() {
|
||||
if (this.pony && !this.pony.ponyInfo && !this.promise) {
|
||||
this.promise = this.model.getPonyInfo(this.pony)
|
||||
.finally(() => this.promise = undefined);
|
||||
}
|
||||
}
|
||||
click() {
|
||||
console.log(this.pony);
|
||||
}
|
||||
@Input() pony?: Character;
|
||||
@Input() highlight = false;
|
||||
labelClass = 'badge-none';
|
||||
private promise?: Promise<void>;
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
get isBadCM() {
|
||||
return !!this.pony && hasFlag(this.pony.flags, CharacterFlags.BadCM);
|
||||
}
|
||||
ngOnChanges() {
|
||||
if (this.pony) {
|
||||
if (isForbiddenName(this.pony.name)) {
|
||||
this.labelClass = 'badge-forbidden';
|
||||
} else if (hasFlag(this.pony.flags, CharacterFlags.BadCM)) {
|
||||
this.labelClass = 'badge-danger';
|
||||
} else {
|
||||
this.labelClass = 'badge-none';
|
||||
}
|
||||
}
|
||||
}
|
||||
onShown() {
|
||||
if (this.pony && !this.pony.ponyInfo && !this.promise) {
|
||||
this.promise = this.model.getPonyInfo(this.pony)
|
||||
.finally(() => this.promise = undefined);
|
||||
}
|
||||
}
|
||||
click() {
|
||||
console.log(this.pony);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,71 +5,71 @@ import { faTrash, faArrowRight } from '../../../../client/icons';
|
||||
import { Character, PonyIdDateName, DuplicateResult } from '../../../../common/adminInterfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'pony-list-remote',
|
||||
templateUrl: 'pony-list-remote.pug',
|
||||
styleUrls: ['pony-list-remote.scss'],
|
||||
selector: 'pony-list-remote',
|
||||
templateUrl: 'pony-list-remote.pug',
|
||||
styleUrls: ['pony-list-remote.scss'],
|
||||
})
|
||||
export class PonyListRemote implements OnDestroy {
|
||||
readonly trashIcon = faTrash;
|
||||
readonly assignIcon = faArrowRight;
|
||||
@Input() limit = 10;
|
||||
@Input() expanded = false;
|
||||
@Input() deletable = false;
|
||||
@Input() highlight: (pony: Character) => boolean = () => false;
|
||||
@Input() duplicates?: DuplicateResult[];
|
||||
full = false;
|
||||
ponies: string[] = [];
|
||||
loading = false;
|
||||
private ponyInfos: PonyIdDateName[] = [];
|
||||
private _accountId?: string;
|
||||
private subscription?: Subscription;
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
get limitTo() {
|
||||
return this.full ? 999999 : this.limit;
|
||||
}
|
||||
get accountId() {
|
||||
return this._accountId;
|
||||
}
|
||||
@Input() set accountId(value: string | undefined) {
|
||||
if (this.accountId !== value) {
|
||||
this._accountId = value;
|
||||
this.ponies = [];
|
||||
this.ponyInfos = [];
|
||||
this.loading = true;
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
this.subscription = value ? this.model.accountPonies.subscribe(value, (x = []) => {
|
||||
this.ponyInfos = x;
|
||||
this.updatePonies();
|
||||
this.loading = false;
|
||||
}) : undefined;
|
||||
}
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
remove(characterId: string) {
|
||||
if (confirm('Are you sure?')) {
|
||||
this.model.removePony(characterId);
|
||||
}
|
||||
}
|
||||
toggleFull() {
|
||||
this.full = !this.full;
|
||||
this.updatePonies();
|
||||
}
|
||||
assignTo(pony: string, account: string) {
|
||||
this.model.assignPony(pony, account);
|
||||
}
|
||||
private updatePonies() {
|
||||
const compare = this.full ? compareNames : compareDates;
|
||||
this.ponies = this.ponyInfos.sort(compare).map(p => p.id);
|
||||
}
|
||||
readonly trashIcon = faTrash;
|
||||
readonly assignIcon = faArrowRight;
|
||||
@Input() limit = 10;
|
||||
@Input() expanded = false;
|
||||
@Input() deletable = false;
|
||||
@Input() highlight: (pony: Character) => boolean = () => false;
|
||||
@Input() duplicates?: DuplicateResult[];
|
||||
full = false;
|
||||
ponies: string[] = [];
|
||||
loading = false;
|
||||
private ponyInfos: PonyIdDateName[] = [];
|
||||
private _accountId?: string;
|
||||
private subscription?: Subscription;
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
get limitTo() {
|
||||
return this.full ? 999999 : this.limit;
|
||||
}
|
||||
get accountId() {
|
||||
return this._accountId;
|
||||
}
|
||||
@Input() set accountId(value: string | undefined) {
|
||||
if (this.accountId !== value) {
|
||||
this._accountId = value;
|
||||
this.ponies = [];
|
||||
this.ponyInfos = [];
|
||||
this.loading = true;
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
this.subscription = value ? this.model.accountPonies.subscribe(value, (x = []) => {
|
||||
this.ponyInfos = x;
|
||||
this.updatePonies();
|
||||
this.loading = false;
|
||||
}) : undefined;
|
||||
}
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
remove(characterId: string) {
|
||||
if (confirm('Are you sure?')) {
|
||||
this.model.removePony(characterId);
|
||||
}
|
||||
}
|
||||
toggleFull() {
|
||||
this.full = !this.full;
|
||||
this.updatePonies();
|
||||
}
|
||||
assignTo(pony: string, account: string) {
|
||||
this.model.assignPony(pony, account);
|
||||
}
|
||||
private updatePonies() {
|
||||
const compare = this.full ? compareNames : compareDates;
|
||||
this.ponies = this.ponyInfos.sort(compare).map(p => p.id);
|
||||
}
|
||||
}
|
||||
|
||||
export function compareNames(a: { name: string }, b: { name: string }) {
|
||||
return a.name.localeCompare(b.name);
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
export function compareDates(a: { date: number }, b: { date: number }) {
|
||||
return b.date - a.date;
|
||||
return b.date - a.date;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'time-field',
|
||||
templateUrl: 'time-field.pug',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'time-field',
|
||||
templateUrl: 'time-field.pug',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TimeField {
|
||||
@Input() time: any;
|
||||
@Input() time: any;
|
||||
}
|
||||
|
||||
@@ -3,43 +3,43 @@ import { UAParser } from 'ua-parser-js';
|
||||
import { uaIcons, faQuestionCircle, faGlobe, faDesktop } from '../../../../client/icons';
|
||||
|
||||
function icon(value: string | undefined, defaultValue: any): any {
|
||||
return value && uaIcons[value] || defaultValue;
|
||||
return value && uaIcons[value] || defaultValue;
|
||||
}
|
||||
|
||||
const extensions = {
|
||||
browser: [
|
||||
[/(Amigo|YaBrowser)\/([\w\.]+)/i], [UAParser.BROWSER.NAME, UAParser.BROWSER.VERSION]
|
||||
],
|
||||
browser: [
|
||||
[/(Amigo|YaBrowser)\/([\w\.]+)/i], [UAParser.BROWSER.NAME, UAParser.BROWSER.VERSION]
|
||||
],
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'ua-info',
|
||||
templateUrl: 'ua-info.pug',
|
||||
styles: [`:host { display: inline-block; }`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'ua-info',
|
||||
templateUrl: 'ua-info.pug',
|
||||
styles: [`:host { display: inline-block; }`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UAInfo {
|
||||
osClass?: string;
|
||||
osVersion?: string;
|
||||
browserClass?: string;
|
||||
browserVersion?: string;
|
||||
deviceClass?: string;
|
||||
private _userAgent?: string;
|
||||
@Input() set userAgent(value: string | undefined) {
|
||||
if (this._userAgent !== value) {
|
||||
this._userAgent = value;
|
||||
osClass?: string;
|
||||
osVersion?: string;
|
||||
browserClass?: string;
|
||||
browserVersion?: string;
|
||||
deviceClass?: string;
|
||||
private _userAgent?: string;
|
||||
@Input() set userAgent(value: string | undefined) {
|
||||
if (this._userAgent !== value) {
|
||||
this._userAgent = value;
|
||||
|
||||
const parser = new UAParser(value, extensions);
|
||||
const { os, browser, device } = parser.getResult();
|
||||
const parser = new UAParser(value, extensions);
|
||||
const { os, browser, device } = parser.getResult();
|
||||
|
||||
this.osVersion = os.version;
|
||||
this.browserVersion = (browser.version || '').replace(/\..*$/, '');
|
||||
this.osClass = icon(os.name, faQuestionCircle);
|
||||
this.browserClass = icon(browser.name, faGlobe);
|
||||
this.deviceClass = icon(device.type, faDesktop);
|
||||
}
|
||||
}
|
||||
get userAgent() {
|
||||
return this._userAgent;
|
||||
}
|
||||
this.osVersion = os.version;
|
||||
this.browserVersion = (browser.version || '').replace(/\..*$/, '');
|
||||
this.osClass = icon(os.name, faQuestionCircle);
|
||||
this.browserClass = icon(browser.name, faGlobe);
|
||||
this.deviceClass = icon(device.type, faDesktop);
|
||||
}
|
||||
}
|
||||
get userAgent() {
|
||||
return this._userAgent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,25 +7,25 @@ import { SUPPORTER_REWARDS_LIST } from '../../../common/constants';
|
||||
import { supporterLink, contactEmail } from '../../../client/data';
|
||||
|
||||
function toCredit(credit: Credit) {
|
||||
return {
|
||||
...credit,
|
||||
background: `url(${getUrl('images/avatars.jpg')})`,
|
||||
position: `${(credit.avatarIndex % 4) * -82}px ${Math.floor(credit.avatarIndex / 4) * -82}px`,
|
||||
};
|
||||
return {
|
||||
...credit,
|
||||
background: `url(${getUrl('images/avatars.jpg')})`,
|
||||
position: `${(credit.avatarIndex % 4) * -82}px ${Math.floor(credit.avatarIndex / 4) * -82}px`,
|
||||
};
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'about',
|
||||
templateUrl: 'about.pug',
|
||||
styleUrls: ['about.scss'],
|
||||
selector: 'about',
|
||||
templateUrl: 'about.pug',
|
||||
styleUrls: ['about.scss'],
|
||||
})
|
||||
export class About {
|
||||
readonly title = document.title;
|
||||
readonly emotes = emojis;
|
||||
readonly credits = CREDITS.map(toCredit);
|
||||
readonly contributors = CONTRIBUTORS;
|
||||
readonly changelog = CHANGELOG;
|
||||
readonly rewards = SUPPORTER_REWARDS_LIST;
|
||||
readonly patreonLink = supporterLink;
|
||||
readonly contactEmail = contactEmail;
|
||||
readonly title = document.title;
|
||||
readonly emotes = emojis;
|
||||
readonly credits = CREDITS.map(toCredit);
|
||||
readonly contributors = CONTRIBUTORS;
|
||||
readonly changelog = CHANGELOG;
|
||||
readonly rewards = SUPPORTER_REWARDS_LIST;
|
||||
readonly patreonLink = supporterLink;
|
||||
readonly contactEmail = contactEmail;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ACCOUNT_NAME_MAX_LENGTH, ACCOUNT_NAME_MIN_LENGTH, HIDES_PER_PAGE } from '../../../common/constants';
|
||||
import { UpdateAccountData, SocialSiteInfo, OAuthProvider, HiddenPlayer } from '../../../common/interfaces';
|
||||
import {
|
||||
toSocialSiteInfo, cleanName, supporterTitle, supporterClass, isSupporterOrPastSupporter, supporterRewards
|
||||
toSocialSiteInfo, cleanName, supporterTitle, supporterClass, isSupporterOrPastSupporter, supporterRewards
|
||||
} from '../../../client/clientUtils';
|
||||
import { oauthProviders } from '../../../client/data';
|
||||
import { Model } from '../../services/model';
|
||||
@@ -10,121 +10,121 @@ import { getProviderIcon } from '../../shared/sign-in-box/sign-in-box';
|
||||
import { faStar, faExclamationCircle, faSync } from '../../../client/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'account',
|
||||
templateUrl: 'account.pug',
|
||||
styleUrls: ['account.scss'],
|
||||
selector: 'account',
|
||||
templateUrl: 'account.pug',
|
||||
styleUrls: ['account.scss'],
|
||||
})
|
||||
export class Account implements OnInit, OnDestroy {
|
||||
readonly refreshIcon = faSync;
|
||||
readonly starIcon = faStar;
|
||||
readonly alertIcon = faExclamationCircle;
|
||||
readonly providers = oauthProviders.filter(p => !p.disabled);
|
||||
readonly nameMinLength = ACCOUNT_NAME_MIN_LENGTH;
|
||||
readonly nameMaxLength = ACCOUNT_NAME_MAX_LENGTH;
|
||||
readonly hidesPerPage = HIDES_PER_PAGE;
|
||||
data: UpdateAccountData = {
|
||||
name: '',
|
||||
birthdate: '',
|
||||
};
|
||||
sites?: SocialSiteInfo[];
|
||||
password?: string;
|
||||
removingSite?: boolean;
|
||||
mergeError?: string;
|
||||
removedAccount?: boolean;
|
||||
accountError?: string;
|
||||
accountSaved = false;
|
||||
hides: HiddenPlayer[] | undefined = undefined;
|
||||
page = 0;
|
||||
constructor(private model: Model) {
|
||||
}
|
||||
ngOnInit() {
|
||||
const account = this.account!;
|
||||
this.sites = account.sites && account.sites.map(toSocialSiteInfo);
|
||||
this.data = {
|
||||
name: account.name,
|
||||
birthdate: account.birthdate,
|
||||
};
|
||||
readonly refreshIcon = faSync;
|
||||
readonly starIcon = faStar;
|
||||
readonly alertIcon = faExclamationCircle;
|
||||
readonly providers = oauthProviders.filter(p => !p.disabled);
|
||||
readonly nameMinLength = ACCOUNT_NAME_MIN_LENGTH;
|
||||
readonly nameMaxLength = ACCOUNT_NAME_MAX_LENGTH;
|
||||
readonly hidesPerPage = HIDES_PER_PAGE;
|
||||
data: UpdateAccountData = {
|
||||
name: '',
|
||||
birthdate: '',
|
||||
};
|
||||
sites?: SocialSiteInfo[];
|
||||
password?: string;
|
||||
removingSite?: boolean;
|
||||
mergeError?: string;
|
||||
removedAccount?: boolean;
|
||||
accountError?: string;
|
||||
accountSaved = false;
|
||||
hides: HiddenPlayer[] | undefined = undefined;
|
||||
page = 0;
|
||||
constructor(private model: Model) {
|
||||
}
|
||||
ngOnInit() {
|
||||
const account = this.account!;
|
||||
this.sites = account.sites && account.sites.map(toSocialSiteInfo);
|
||||
this.data = {
|
||||
name: account.name,
|
||||
birthdate: account.birthdate,
|
||||
};
|
||||
|
||||
this.pageChanged();
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.model.mergedAccount = false;
|
||||
}
|
||||
pageChanged() {
|
||||
this.model.getHides(this.page)
|
||||
.then(result => this.hides = result);
|
||||
}
|
||||
get authError() {
|
||||
return this.model.authError;
|
||||
}
|
||||
get mergedAccount() {
|
||||
return this.model.mergedAccount;
|
||||
}
|
||||
get account() {
|
||||
return this.model.account;
|
||||
}
|
||||
get supporter() {
|
||||
return this.model.supporter;
|
||||
}
|
||||
get showSupporter() {
|
||||
return isSupporterOrPastSupporter(this.account);
|
||||
}
|
||||
get canSubmit() {
|
||||
return this.account && this.data.name && !!cleanName(this.data.name).length;
|
||||
}
|
||||
get supporterTitle() {
|
||||
return supporterTitle(this.account);
|
||||
}
|
||||
get supporterClass() {
|
||||
return supporterClass(this.account);
|
||||
}
|
||||
get supporterRewards() {
|
||||
return supporterRewards(this.account);
|
||||
}
|
||||
get showSupporterInfo() {
|
||||
const account = this.account;
|
||||
return !!(!this.supporter && account && account.sites && account.sites.some(s => s.provider === 'patreon'));
|
||||
}
|
||||
get showAccountAlert() {
|
||||
return this.model.missingBirthdate;
|
||||
}
|
||||
icon(id: string) {
|
||||
return getProviderIcon(id);
|
||||
}
|
||||
submit() {
|
||||
if (this.canSubmit) {
|
||||
this.resetAllMessages();
|
||||
this.data.name = cleanName(this.data.name).substr(0, ACCOUNT_NAME_MAX_LENGTH);
|
||||
this.model.updateAccount(this.data)
|
||||
.catch((e: Error) => this.accountError = e.message)
|
||||
.then(() => this.accountSaved = true);
|
||||
}
|
||||
}
|
||||
removeSite(site: SocialSiteInfo) {
|
||||
if (confirm('Are you sure you want to remove this social account ?')) {
|
||||
this.removingSite = true;
|
||||
this.resetAllMessages();
|
||||
this.model.removeSite(site.id)
|
||||
.then(() => this.sites = this.account!.sites!.map(toSocialSiteInfo))
|
||||
.then(() => this.removedAccount = true)
|
||||
.catch((e: Error) => this.mergeError = e.message)
|
||||
.then(() => this.removingSite = false);
|
||||
}
|
||||
}
|
||||
connectSite(provider: OAuthProvider) {
|
||||
this.model.connectSite(provider);
|
||||
}
|
||||
private resetAllMessages() {
|
||||
this.accountSaved = false;
|
||||
this.mergeError = undefined;
|
||||
this.accountError = undefined;
|
||||
this.removedAccount = false;
|
||||
this.model.authError = undefined;
|
||||
this.model.mergedAccount = false;
|
||||
}
|
||||
unhidePlayer(player: HiddenPlayer) {
|
||||
this.model.unhidePlayer(player.id)
|
||||
.then(() => this.pageChanged())
|
||||
.catch((e: Error) => console.error(e));
|
||||
}
|
||||
this.pageChanged();
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.model.mergedAccount = false;
|
||||
}
|
||||
pageChanged() {
|
||||
this.model.getHides(this.page)
|
||||
.then(result => this.hides = result);
|
||||
}
|
||||
get authError() {
|
||||
return this.model.authError;
|
||||
}
|
||||
get mergedAccount() {
|
||||
return this.model.mergedAccount;
|
||||
}
|
||||
get account() {
|
||||
return this.model.account;
|
||||
}
|
||||
get supporter() {
|
||||
return this.model.supporter;
|
||||
}
|
||||
get showSupporter() {
|
||||
return isSupporterOrPastSupporter(this.account);
|
||||
}
|
||||
get canSubmit() {
|
||||
return this.account && this.data.name && !!cleanName(this.data.name).length;
|
||||
}
|
||||
get supporterTitle() {
|
||||
return supporterTitle(this.account);
|
||||
}
|
||||
get supporterClass() {
|
||||
return supporterClass(this.account);
|
||||
}
|
||||
get supporterRewards() {
|
||||
return supporterRewards(this.account);
|
||||
}
|
||||
get showSupporterInfo() {
|
||||
const account = this.account;
|
||||
return !!(!this.supporter && account && account.sites && account.sites.some(s => s.provider === 'patreon'));
|
||||
}
|
||||
get showAccountAlert() {
|
||||
return this.model.missingBirthdate;
|
||||
}
|
||||
icon(id: string) {
|
||||
return getProviderIcon(id);
|
||||
}
|
||||
submit() {
|
||||
if (this.canSubmit) {
|
||||
this.resetAllMessages();
|
||||
this.data.name = cleanName(this.data.name).substr(0, ACCOUNT_NAME_MAX_LENGTH);
|
||||
this.model.updateAccount(this.data)
|
||||
.catch((e: Error) => this.accountError = e.message)
|
||||
.then(() => this.accountSaved = true);
|
||||
}
|
||||
}
|
||||
removeSite(site: SocialSiteInfo) {
|
||||
if (confirm('Are you sure you want to remove this social account ?')) {
|
||||
this.removingSite = true;
|
||||
this.resetAllMessages();
|
||||
this.model.removeSite(site.id)
|
||||
.then(() => this.sites = this.account!.sites!.map(toSocialSiteInfo))
|
||||
.then(() => this.removedAccount = true)
|
||||
.catch((e: Error) => this.mergeError = e.message)
|
||||
.then(() => this.removingSite = false);
|
||||
}
|
||||
}
|
||||
connectSite(provider: OAuthProvider) {
|
||||
this.model.connectSite(provider);
|
||||
}
|
||||
private resetAllMessages() {
|
||||
this.accountSaved = false;
|
||||
this.mergeError = undefined;
|
||||
this.accountError = undefined;
|
||||
this.removedAccount = false;
|
||||
this.model.authError = undefined;
|
||||
this.model.mergedAccount = false;
|
||||
}
|
||||
unhidePlayer(player: HiddenPlayer) {
|
||||
this.model.unhidePlayer(player.id)
|
||||
.then(() => this.pageChanged())
|
||||
.catch((e: Error) => console.error(e));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,43 +24,43 @@ import { ErrorReporter } from '../services/errorReporter';
|
||||
import { RollbarErrorReporter } from '../services/rollbarErrorReporter';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: '', component: Home },
|
||||
{ path: 'help', component: Help },
|
||||
{ path: 'about', component: About },
|
||||
{ path: 'account', component: Account, canActivate: [AuthGuard] },
|
||||
{ path: 'character', component: Character, canActivate: [AuthGuard] },
|
||||
{ path: '**', redirectTo: '/', pathMatch: 'full' },
|
||||
{ path: '', component: Home },
|
||||
{ path: 'help', component: Help },
|
||||
{ path: 'about', component: About },
|
||||
{ path: 'account', component: Account, canActivate: [AuthGuard] },
|
||||
{ path: 'character', component: Character, canActivate: [AuthGuard] },
|
||||
{ path: '**', redirectTo: '/', pathMatch: 'full' },
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
RouterModule,
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
PopoverModule.forRoot(),
|
||||
ButtonsModule.forRoot(),
|
||||
TooltipModule.forRoot(),
|
||||
// TypeaheadModule.forRoot(),
|
||||
SharedModule,
|
||||
RouterModule.forRoot(routes),
|
||||
FontAwesomeModule,
|
||||
],
|
||||
declarations: [
|
||||
App,
|
||||
Home,
|
||||
Help,
|
||||
About,
|
||||
Account,
|
||||
Character,
|
||||
EditorBox,
|
||||
],
|
||||
providers: [
|
||||
{ provide: RollbarService, useFactory: rollbarFactory },
|
||||
{ provide: ErrorHandler, useClass: RollbarErrorHandler },
|
||||
{ provide: ErrorReporter, useClass: RollbarErrorReporter },
|
||||
],
|
||||
bootstrap: [App],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
RouterModule,
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
PopoverModule.forRoot(),
|
||||
ButtonsModule.forRoot(),
|
||||
TooltipModule.forRoot(),
|
||||
// TypeaheadModule.forRoot(),
|
||||
SharedModule,
|
||||
RouterModule.forRoot(routes),
|
||||
FontAwesomeModule,
|
||||
],
|
||||
declarations: [
|
||||
App,
|
||||
Home,
|
||||
Help,
|
||||
About,
|
||||
Account,
|
||||
Character,
|
||||
EditorBox,
|
||||
],
|
||||
providers: [
|
||||
{ provide: RollbarService, useFactory: rollbarFactory },
|
||||
{ provide: ErrorHandler, useClass: RollbarErrorHandler },
|
||||
{ provide: ErrorReporter, useClass: RollbarErrorReporter },
|
||||
],
|
||||
bootstrap: [App],
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
||||
|
||||
+167
-167
@@ -21,190 +21,190 @@ import { findEntityById } from '../../common/worldMap';
|
||||
import { isSelected } from '../../client/gameUtils';
|
||||
|
||||
export function tooltipConfig() {
|
||||
return Object.assign(new TooltipConfig(), { container: 'body' });
|
||||
return Object.assign(new TooltipConfig(), { container: 'body' });
|
||||
}
|
||||
|
||||
export function popoverConfig() {
|
||||
return Object.assign(new PopoverConfig(), { container: 'body' });
|
||||
return Object.assign(new PopoverConfig(), { container: 'body' });
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'pony-town-app',
|
||||
templateUrl: 'app.pug',
|
||||
styleUrls: ['app.scss'],
|
||||
providers: [
|
||||
{ provide: TooltipConfig, useFactory: tooltipConfig },
|
||||
{ provide: PopoverConfig, useFactory: popoverConfig },
|
||||
]
|
||||
selector: 'pony-town-app',
|
||||
templateUrl: 'app.pug',
|
||||
styleUrls: ['app.scss'],
|
||||
providers: [
|
||||
{ provide: TooltipConfig, useFactory: tooltipConfig },
|
||||
{ provide: PopoverConfig, useFactory: popoverConfig },
|
||||
]
|
||||
})
|
||||
export class App implements OnInit, OnDestroy {
|
||||
@ViewChild('announcer', { static: true }) announcer!: ElementRef;
|
||||
@ViewChild('announcerText', { static: true }) announcerText!: ElementRef;
|
||||
@ViewChild('reloadModal', { static: true }) reloadModal!: TemplateRef<any>;
|
||||
@ViewChild('signInModal', { static: true }) signInModal!: TemplateRef<any>;
|
||||
readonly version = version;
|
||||
readonly date = new Date();
|
||||
readonly emailIcon = faEnvelope;
|
||||
readonly twitterIcon = faTwitter;
|
||||
readonly patreonIcon = faPatreon;
|
||||
readonly cogIcon = faCog;
|
||||
readonly homeIcon = faHome;
|
||||
readonly helpIcon = faGamepad;
|
||||
readonly aboutIcon = faInfoCircle;
|
||||
readonly charactersIcon = faHorseHead;
|
||||
readonly contactEmail = contactEmail;
|
||||
readonly patreonLink = supporterLink;
|
||||
readonly twitterLink = twitterLink;
|
||||
readonly copyright = copyrightName;
|
||||
private url = location.pathname;
|
||||
private reloadModalRef?: BsModalRef;
|
||||
private reloadInterval?: any;
|
||||
private subscriptions: Subscription[] = [];
|
||||
constructor(
|
||||
private modalService: BsModalService,
|
||||
private gameService: GameService,
|
||||
private model: Model,
|
||||
private game: PonyTownGame,
|
||||
private router: Router,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private installService: InstallService,
|
||||
private errorReporter: ErrorReporter,
|
||||
) {
|
||||
}
|
||||
get canInstall() {
|
||||
return this.installService.canInstall;
|
||||
}
|
||||
get loading() {
|
||||
return this.model.loading;
|
||||
}
|
||||
get account() {
|
||||
return this.model.account;
|
||||
}
|
||||
get isMod() {
|
||||
return this.model.isMod;
|
||||
}
|
||||
get notifications() {
|
||||
return this.game.notifications;
|
||||
}
|
||||
get selected() {
|
||||
return this.gameService.selected;
|
||||
}
|
||||
get playing() {
|
||||
return this.gameService.playing;
|
||||
}
|
||||
get showActionBar() {
|
||||
return this.playing;
|
||||
}
|
||||
get editingActions() {
|
||||
return this.game.editingActions;
|
||||
}
|
||||
ngOnInit() {
|
||||
if (typeof ga !== 'undefined') {
|
||||
this.subscriptions.push(this.router.events.subscribe(event => {
|
||||
if (event instanceof NavigationEnd && this.url !== event.url) {
|
||||
ga('set', 'page', this.url = event.url);
|
||||
ga('send', 'pageview');
|
||||
}
|
||||
}));
|
||||
}
|
||||
@ViewChild('announcer', { static: true }) announcer!: ElementRef;
|
||||
@ViewChild('announcerText', { static: true }) announcerText!: ElementRef;
|
||||
@ViewChild('reloadModal', { static: true }) reloadModal!: TemplateRef<any>;
|
||||
@ViewChild('signInModal', { static: true }) signInModal!: TemplateRef<any>;
|
||||
readonly version = version;
|
||||
readonly date = new Date();
|
||||
readonly emailIcon = faEnvelope;
|
||||
readonly twitterIcon = faTwitter;
|
||||
readonly patreonIcon = faPatreon;
|
||||
readonly cogIcon = faCog;
|
||||
readonly homeIcon = faHome;
|
||||
readonly helpIcon = faGamepad;
|
||||
readonly aboutIcon = faInfoCircle;
|
||||
readonly charactersIcon = faHorseHead;
|
||||
readonly contactEmail = contactEmail;
|
||||
readonly patreonLink = supporterLink;
|
||||
readonly twitterLink = twitterLink;
|
||||
readonly copyright = copyrightName;
|
||||
private url = location.pathname;
|
||||
private reloadModalRef?: BsModalRef;
|
||||
private reloadInterval?: any;
|
||||
private subscriptions: Subscription[] = [];
|
||||
constructor(
|
||||
private modalService: BsModalService,
|
||||
private gameService: GameService,
|
||||
private model: Model,
|
||||
private game: PonyTownGame,
|
||||
private router: Router,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private installService: InstallService,
|
||||
private errorReporter: ErrorReporter,
|
||||
) {
|
||||
}
|
||||
get canInstall() {
|
||||
return this.installService.canInstall;
|
||||
}
|
||||
get loading() {
|
||||
return this.model.loading;
|
||||
}
|
||||
get account() {
|
||||
return this.model.account;
|
||||
}
|
||||
get isMod() {
|
||||
return this.model.isMod;
|
||||
}
|
||||
get notifications() {
|
||||
return this.game.notifications;
|
||||
}
|
||||
get selected() {
|
||||
return this.gameService.selected;
|
||||
}
|
||||
get playing() {
|
||||
return this.gameService.playing;
|
||||
}
|
||||
get showActionBar() {
|
||||
return this.playing;
|
||||
}
|
||||
get editingActions() {
|
||||
return this.game.editingActions;
|
||||
}
|
||||
ngOnInit() {
|
||||
if (typeof ga !== 'undefined') {
|
||||
this.subscriptions.push(this.router.events.subscribe(event => {
|
||||
if (event instanceof NavigationEnd && this.url !== event.url) {
|
||||
ga('set', 'page', this.url = event.url);
|
||||
ga('send', 'pageview');
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (isBrowserOutdated) {
|
||||
this.errorReporter.disable();
|
||||
}
|
||||
if (isBrowserOutdated) {
|
||||
this.errorReporter.disable();
|
||||
}
|
||||
|
||||
if (!DEVELOPMENT) {
|
||||
registerServiceWorker(`${host}sw.js`, () => {
|
||||
this.model.updating = true;
|
||||
setTimeout(() => {
|
||||
this.model.updatingTakesLongTime = true;
|
||||
}, 20 * SECOND);
|
||||
});
|
||||
}
|
||||
if (!DEVELOPMENT) {
|
||||
registerServiceWorker(`${host}sw.js`, () => {
|
||||
this.model.updating = true;
|
||||
setTimeout(() => {
|
||||
this.model.updatingTakesLongTime = true;
|
||||
}, 20 * SECOND);
|
||||
});
|
||||
}
|
||||
|
||||
if (DEVELOPMENT) {
|
||||
this.subscriptions.push(this.game.announcements.subscribe(message => {
|
||||
(this.announcer.nativeElement as HTMLElement).style.display = 'flex';
|
||||
const announcerText = this.announcerText.nativeElement as HTMLElement;
|
||||
announcerText.textContent = '';
|
||||
setTimeout(() => announcerText.textContent = message, 100);
|
||||
}));
|
||||
}
|
||||
if (DEVELOPMENT) {
|
||||
this.subscriptions.push(this.game.announcements.subscribe(message => {
|
||||
(this.announcer.nativeElement as HTMLElement).style.display = 'flex';
|
||||
const announcerText = this.announcerText.nativeElement as HTMLElement;
|
||||
announcerText.textContent = '';
|
||||
setTimeout(() => announcerText.textContent = message, 100);
|
||||
}));
|
||||
}
|
||||
|
||||
this.activatedRoute.queryParams.subscribe(({ error, merged, alert }) => {
|
||||
this.model.authError = error;
|
||||
this.model.accountAlert = alert;
|
||||
this.model.mergedAccount = !!merged;
|
||||
});
|
||||
this.activatedRoute.queryParams.subscribe(({ error, merged, alert }) => {
|
||||
this.model.authError = error;
|
||||
this.model.accountAlert = alert;
|
||||
this.model.mergedAccount = !!merged;
|
||||
});
|
||||
|
||||
this.subscriptions.push(this.model.protectionErrors.subscribe(() => {
|
||||
this.openReloadModal();
|
||||
}));
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.subscriptions.forEach(s => s.unsubscribe());
|
||||
}
|
||||
@HostListener('window:focus')
|
||||
focus() {
|
||||
this.model.verifyAccount();
|
||||
}
|
||||
signIn(provider: OAuthProvider) {
|
||||
this.model.signIn(provider);
|
||||
}
|
||||
signOut() {
|
||||
this.model.signOut();
|
||||
}
|
||||
openReloadModal() {
|
||||
if (!this.reloadModalRef) {
|
||||
this.reloadModalRef = this.modalService.show(
|
||||
this.reloadModal, { class: 'modal-lg', ignoreBackdropClick: true, keyboard: false });
|
||||
this.subscriptions.push(this.model.protectionErrors.subscribe(() => {
|
||||
this.openReloadModal();
|
||||
}));
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.subscriptions.forEach(s => s.unsubscribe());
|
||||
}
|
||||
@HostListener('window:focus')
|
||||
focus() {
|
||||
this.model.verifyAccount();
|
||||
}
|
||||
signIn(provider: OAuthProvider) {
|
||||
this.model.signIn(provider);
|
||||
}
|
||||
signOut() {
|
||||
this.model.signOut();
|
||||
}
|
||||
openReloadModal() {
|
||||
if (!this.reloadModalRef) {
|
||||
this.reloadModalRef = this.modalService.show(
|
||||
this.reloadModal, { class: 'modal-lg', ignoreBackdropClick: true, keyboard: false });
|
||||
|
||||
this.reloadInterval = setInterval(() => {
|
||||
if (checkIframeKey('reload-frame', 'gep84r9jshge4g')) {
|
||||
this.cancelReloadModal();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
cancelReloadModal() {
|
||||
if (this.reloadModalRef) {
|
||||
this.reloadModalRef.hide();
|
||||
this.reloadModalRef = undefined;
|
||||
}
|
||||
this.reloadInterval = setInterval(() => {
|
||||
if (checkIframeKey('reload-frame', 'gep84r9jshge4g')) {
|
||||
this.cancelReloadModal();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
cancelReloadModal() {
|
||||
if (this.reloadModalRef) {
|
||||
this.reloadModalRef.hide();
|
||||
this.reloadModalRef = undefined;
|
||||
}
|
||||
|
||||
clearInterval(this.reloadInterval);
|
||||
}
|
||||
chatLogNameClick(chatBox: ChatBox, message: ChatLogMessage) {
|
||||
if (!message.entityId) {
|
||||
return;
|
||||
}
|
||||
clearInterval(this.reloadInterval);
|
||||
}
|
||||
chatLogNameClick(chatBox: ChatBox, message: ChatLogMessage) {
|
||||
if (!message.entityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let entity = findEntityById(this.game.map, message.entityId);
|
||||
let entity = findEntityById(this.game.map, message.entityId);
|
||||
|
||||
if (entity && (!isPony(entity) || entity === this.game.player)) {
|
||||
return;
|
||||
}
|
||||
if (entity && (!isPony(entity) || entity === this.game.player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!entity) {
|
||||
entity = { fake: true, type: PONY_TYPE, id: message.entityId, name: message.name } as FakeEntity as any;
|
||||
}
|
||||
if (!entity) {
|
||||
entity = { fake: true, type: PONY_TYPE, id: message.entityId, name: message.name } as FakeEntity as any;
|
||||
}
|
||||
|
||||
if (isSelected(this.game, message.entityId)) {
|
||||
this.game.whisperTo = entity;
|
||||
chatBox.setChatType('whisper');
|
||||
} else {
|
||||
this.game.select(entity as Pony);
|
||||
}
|
||||
}
|
||||
messageToFriend(chatBox: ChatBox, friend: Friend) {
|
||||
if (friend.entityId) {
|
||||
const entity: any = { id: friend.entityId, name: friend.actualName || 'unknown' };
|
||||
this.messageToPony(chatBox, entity);
|
||||
}
|
||||
}
|
||||
messageToPony(chatBox: ChatBox, pony: Entity) {
|
||||
setTimeout(() => {
|
||||
this.game.whisperTo = pony;
|
||||
chatBox.setChatType('whisper');
|
||||
});
|
||||
}
|
||||
if (isSelected(this.game, message.entityId)) {
|
||||
this.game.whisperTo = entity;
|
||||
chatBox.setChatType('whisper');
|
||||
} else {
|
||||
this.game.select(entity as Pony);
|
||||
}
|
||||
}
|
||||
messageToFriend(chatBox: ChatBox, friend: Friend) {
|
||||
if (friend.entityId) {
|
||||
const entity: any = { id: friend.entityId, name: friend.actualName || 'unknown' };
|
||||
this.messageToPony(chatBox, entity);
|
||||
}
|
||||
}
|
||||
messageToPony(chatBox: ChatBox, pony: Entity) {
|
||||
setTimeout(() => {
|
||||
this.game.whisperTo = pony;
|
||||
chatBox.setChatType('whisper');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { clamp } from 'lodash';
|
||||
import { PLAYER_NAME_MAX_LENGTH, PLAYER_DESC_MAX_LENGTH } from '../../../common/constants';
|
||||
import {
|
||||
PonyInfo, PonyObject, PonyState, SocialSiteInfo, ColorExtraSet, ColorExtra, CharacterTag, PonyEye, Eye, Muzzle,
|
||||
Iris, ExpressionExtra
|
||||
PonyInfo, PonyObject, PonyState, SocialSiteInfo, ColorExtraSet, ColorExtra, CharacterTag, PonyEye, Eye, Muzzle,
|
||||
Iris, ExpressionExtra
|
||||
} from '../../../common/interfaces';
|
||||
import { findById, toInt, cloneDeep, delay } from '../../../common/utils';
|
||||
import {
|
||||
SLEEVED_ACCESSORIES, frontHooves, mergedBackManes, mergedManes, mergedFacialHair, mergedEarAccessories,
|
||||
mergedChestAccessories, mergedFaceAccessories, mergedBackAccessories, mergedExtraAccessories, mergedHeadAccessories
|
||||
SLEEVED_ACCESSORIES, frontHooves, mergedBackManes, mergedManes, mergedFacialHair, mergedEarAccessories,
|
||||
mergedChestAccessories, mergedFaceAccessories, mergedBackAccessories, mergedExtraAccessories, mergedHeadAccessories
|
||||
} from '../../../client/ponyUtils';
|
||||
import { defaultPonyState, defaultDrawPonyOptions } from '../../../client/ponyHelpers';
|
||||
import { toPalette, getBaseFill, syncLockedPonyInfo } from '../../../common/ponyInfo';
|
||||
@@ -33,17 +33,17 @@ const frontHoofTitles = ['', 'Fetlocks', 'Paws', 'Claws', ''];
|
||||
const backHoofTitles = ['', 'Fetlocks', 'Paws', '', ''];
|
||||
|
||||
const horns = addLabels(sprites.horns, [
|
||||
'None', 'Unicorn horn', 'Short unicorn horn', 'Curved unicorn horn', 'Tiny deer antlers',
|
||||
'Short deer antlers', 'Medium deer antlers', 'Large deer antlers', 'Raindeer antlers', 'Goat horns',
|
||||
'Ram horns', 'Buffalo horns', 'Moose horns', 'Bug antenna', 'Long unicorn horn',
|
||||
'None', 'Unicorn horn', 'Short unicorn horn', 'Curved unicorn horn', 'Tiny deer antlers',
|
||||
'Short deer antlers', 'Medium deer antlers', 'Large deer antlers', 'Raindeer antlers', 'Goat horns',
|
||||
'Ram horns', 'Buffalo horns', 'Moose horns', 'Bug antenna', 'Long unicorn horn',
|
||||
]);
|
||||
|
||||
const wings = addLabels(sprites.wings[0]!, [
|
||||
'None', 'Pegasus wings', 'Bat wings', 'Gryphon wings', 'Bug wings'
|
||||
'None', 'Pegasus wings', 'Bat wings', 'Gryphon wings', 'Bug wings'
|
||||
]);
|
||||
|
||||
const ears = addLabels(sprites.ears, [
|
||||
'Regular ears', 'Fluffy ears', 'Long feathered ears', 'Bug ears', 'Short feathered ears', 'Deer ears',
|
||||
'Regular ears', 'Fluffy ears', 'Long feathered ears', 'Bug ears', 'Short feathered ears', 'Deer ears',
|
||||
]);
|
||||
|
||||
const noses = addTitles(sprites.noses[0], ['Pony muzzle', 'Gryphon beak', 'Deer nose']);
|
||||
@@ -51,451 +51,451 @@ const noses = addTitles(sprites.noses[0], ['Pony muzzle', 'Gryphon beak', 'Deer
|
||||
const flyAnimations = [{ ...stand, name: 'fly' }, fly, fly, fly, { ...flyBug, name: 'fly' }];
|
||||
|
||||
function eyeSprite(e: PonyEye | undefined) {
|
||||
return createEyeSprite(e, 0, sprites.defaultPalette);
|
||||
return createEyeSprite(e, 0, sprites.defaultPalette);
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'character',
|
||||
templateUrl: 'character.pug',
|
||||
styleUrls: ['character.scss'],
|
||||
selector: 'character',
|
||||
templateUrl: 'character.pug',
|
||||
styleUrls: ['character.scss'],
|
||||
})
|
||||
export class Character implements OnInit, OnDestroy {
|
||||
readonly debug = DEVELOPMENT || BETA;
|
||||
readonly playIcon = faPlay;
|
||||
readonly lockIcon = faLock;
|
||||
readonly saveIcon = faSave;
|
||||
readonly codeIcon = faCode;
|
||||
readonly infoIcon = faInfoCircle;
|
||||
readonly maxNameLength = PLAYER_NAME_MAX_LENGTH;
|
||||
readonly maxDescLength = PLAYER_DESC_MAX_LENGTH;
|
||||
readonly horns = horns;
|
||||
readonly manes = mergedManes;
|
||||
readonly backManes = mergedBackManes;
|
||||
readonly tails = sprites.tails[0];
|
||||
readonly wings = wings;
|
||||
readonly ears = ears;
|
||||
readonly facialHair = mergedFacialHair;
|
||||
readonly headAccessories = mergedHeadAccessories;
|
||||
readonly earAccessories = mergedEarAccessories;
|
||||
readonly faceAccessories = mergedFaceAccessories;
|
||||
readonly neckAccessories = sprites.neckAccessories[1];
|
||||
readonly frontLegAccessories = sprites.frontLegAccessories[1];
|
||||
readonly backLegAccessories = sprites.backLegAccessories[1];
|
||||
readonly backAccessories = mergedBackAccessories;
|
||||
readonly chestAccessories = mergedChestAccessories;
|
||||
readonly sleeveAccessories = sprites.frontLegSleeves[1];
|
||||
readonly waistAccessories = sprites.waistAccessories[1];
|
||||
readonly extraAccessories = mergedExtraAccessories;
|
||||
readonly frontHooves = addTitles(frontHooves[1], frontHoofTitles);
|
||||
readonly backHooves = addTitles(sprites.backLegHooves[1], backHoofTitles);
|
||||
readonly animations = [
|
||||
() => stand,
|
||||
() => trot,
|
||||
() => boop,
|
||||
() => sitDownUp,
|
||||
() => lieDownUp,
|
||||
() => flyAnimations[this.previewInfo!.wings!.type || 0],
|
||||
];
|
||||
readonly eyelashes: ColorExtraSet = sprites.eyeLeft[1]!.map(eyeSprite);
|
||||
readonly eyesLeft: ColorExtraSet = sprites.eyeLeft.map(e => e && e[0]).map(eyeSprite);
|
||||
readonly eyesRight: ColorExtraSet = sprites.eyeRight.map(e => e && e[0]).map(eyeSprite);
|
||||
readonly noses = noses;
|
||||
readonly heads = sprites.head1[1];
|
||||
readonly buttMarkState: ButtMarkEditorState = {
|
||||
brushType: 'brush',
|
||||
brush: 'orange',
|
||||
};
|
||||
muzzles: ColorExtraSet;
|
||||
fangs: ColorExtraSet;
|
||||
tags: CharacterTag[] = [
|
||||
emptyTag,
|
||||
];
|
||||
state = defaultPonyState();
|
||||
saved: PonyInfo[] = [];
|
||||
activeAnimation = 0;
|
||||
loaded = false;
|
||||
playAnimation = true;
|
||||
deleting = false;
|
||||
fixed = false;
|
||||
previewExtra = false;
|
||||
previewPony: PonyObject | undefined = undefined;
|
||||
sites: SocialSiteInfo[] = [];
|
||||
error?: string;
|
||||
canSaveFiles = isFileSaverSupported();
|
||||
private savingLocked = false;
|
||||
private interval?: any;
|
||||
private syncTimeout?: any;
|
||||
private animationTime = 0;
|
||||
constructor(private gameService: GameService, private model: Model) {
|
||||
this.createMuzzles();
|
||||
this.updateMuzzles();
|
||||
}
|
||||
private getMuzzleType() {
|
||||
return clamp(toInt(this.info && this.info.nose && this.info.nose.type), 0, sprites.noses[0].length);
|
||||
}
|
||||
createMuzzles() {
|
||||
const type = this.getMuzzleType();
|
||||
const happy = sprites.noses[0][type][0];
|
||||
readonly debug = DEVELOPMENT || BETA;
|
||||
readonly playIcon = faPlay;
|
||||
readonly lockIcon = faLock;
|
||||
readonly saveIcon = faSave;
|
||||
readonly codeIcon = faCode;
|
||||
readonly infoIcon = faInfoCircle;
|
||||
readonly maxNameLength = PLAYER_NAME_MAX_LENGTH;
|
||||
readonly maxDescLength = PLAYER_DESC_MAX_LENGTH;
|
||||
readonly horns = horns;
|
||||
readonly manes = mergedManes;
|
||||
readonly backManes = mergedBackManes;
|
||||
readonly tails = sprites.tails[0];
|
||||
readonly wings = wings;
|
||||
readonly ears = ears;
|
||||
readonly facialHair = mergedFacialHair;
|
||||
readonly headAccessories = mergedHeadAccessories;
|
||||
readonly earAccessories = mergedEarAccessories;
|
||||
readonly faceAccessories = mergedFaceAccessories;
|
||||
readonly neckAccessories = sprites.neckAccessories[1];
|
||||
readonly frontLegAccessories = sprites.frontLegAccessories[1];
|
||||
readonly backLegAccessories = sprites.backLegAccessories[1];
|
||||
readonly backAccessories = mergedBackAccessories;
|
||||
readonly chestAccessories = mergedChestAccessories;
|
||||
readonly sleeveAccessories = sprites.frontLegSleeves[1];
|
||||
readonly waistAccessories = sprites.waistAccessories[1];
|
||||
readonly extraAccessories = mergedExtraAccessories;
|
||||
readonly frontHooves = addTitles(frontHooves[1], frontHoofTitles);
|
||||
readonly backHooves = addTitles(sprites.backLegHooves[1], backHoofTitles);
|
||||
readonly animations = [
|
||||
() => stand,
|
||||
() => trot,
|
||||
() => boop,
|
||||
() => sitDownUp,
|
||||
() => lieDownUp,
|
||||
() => flyAnimations[this.previewInfo!.wings!.type || 0],
|
||||
];
|
||||
readonly eyelashes: ColorExtraSet = sprites.eyeLeft[1]!.map(eyeSprite);
|
||||
readonly eyesLeft: ColorExtraSet = sprites.eyeLeft.map(e => e && e[0]).map(eyeSprite);
|
||||
readonly eyesRight: ColorExtraSet = sprites.eyeRight.map(e => e && e[0]).map(eyeSprite);
|
||||
readonly noses = noses;
|
||||
readonly heads = sprites.head1[1];
|
||||
readonly buttMarkState: ButtMarkEditorState = {
|
||||
brushType: 'brush',
|
||||
brush: 'orange',
|
||||
};
|
||||
muzzles: ColorExtraSet;
|
||||
fangs: ColorExtraSet;
|
||||
tags: CharacterTag[] = [
|
||||
emptyTag,
|
||||
];
|
||||
state = defaultPonyState();
|
||||
saved: PonyInfo[] = [];
|
||||
activeAnimation = 0;
|
||||
loaded = false;
|
||||
playAnimation = true;
|
||||
deleting = false;
|
||||
fixed = false;
|
||||
previewExtra = false;
|
||||
previewPony: PonyObject | undefined = undefined;
|
||||
sites: SocialSiteInfo[] = [];
|
||||
error?: string;
|
||||
canSaveFiles = isFileSaverSupported();
|
||||
private savingLocked = false;
|
||||
private interval?: any;
|
||||
private syncTimeout?: any;
|
||||
private animationTime = 0;
|
||||
constructor(private gameService: GameService, private model: Model) {
|
||||
this.createMuzzles();
|
||||
this.updateMuzzles();
|
||||
}
|
||||
private getMuzzleType() {
|
||||
return clamp(toInt(this.info && this.info.nose && this.info.nose.type), 0, sprites.noses[0].length);
|
||||
}
|
||||
createMuzzles() {
|
||||
const type = this.getMuzzleType();
|
||||
const happy = sprites.noses[0][type][0];
|
||||
|
||||
this.muzzles = sprites.noses
|
||||
.slice()
|
||||
.map(n => n[type][0])
|
||||
.map(({ color, colors, mouth }) => ({ color, colors, extra: mouth, palettes: [sprites.defaultPalette] } as ColorExtra));
|
||||
this.muzzles = sprites.noses
|
||||
.slice()
|
||||
.map(n => n[type][0])
|
||||
.map(({ color, colors, mouth }) => ({ color, colors, extra: mouth, palettes: [sprites.defaultPalette] } as ColorExtra));
|
||||
|
||||
this.fangs = [undefined, { color: happy.color, colors: 3, extra: happy.fangs, palettes: [sprites.defaultPalette] }];
|
||||
}
|
||||
updateMuzzles() {
|
||||
const type = this.getMuzzleType();
|
||||
const happy = sprites.noses[0][type][0];
|
||||
this.fangs = [undefined, { color: happy.color, colors: 3, extra: happy.fangs, palettes: [sprites.defaultPalette] }];
|
||||
}
|
||||
updateMuzzles() {
|
||||
const type = this.getMuzzleType();
|
||||
const happy = sprites.noses[0][type][0];
|
||||
|
||||
this.muzzles!.forEach((m, i) => {
|
||||
if (m) {
|
||||
const { color, colors, mouth } = sprites.noses[i][type][0];
|
||||
m.color = color;
|
||||
m.colors = colors;
|
||||
m.extra = mouth;
|
||||
m.timestamp = Date.now();
|
||||
}
|
||||
});
|
||||
this.muzzles!.forEach((m, i) => {
|
||||
if (m) {
|
||||
const { color, colors, mouth } = sprites.noses[i][type][0];
|
||||
m.color = color;
|
||||
m.colors = colors;
|
||||
m.extra = mouth;
|
||||
m.timestamp = Date.now();
|
||||
}
|
||||
});
|
||||
|
||||
const fangs = this.fangs![1]!;
|
||||
fangs.color = happy.color;
|
||||
fangs.extra = happy.fangs;
|
||||
fangs.timestamp = Date.now();
|
||||
}
|
||||
get account() {
|
||||
return this.model.account;
|
||||
}
|
||||
get loading() {
|
||||
return this.model.loading || this.model.updating;
|
||||
}
|
||||
get updateWarning() {
|
||||
return this.gameService.updateWarning;
|
||||
}
|
||||
get playing() {
|
||||
return this.gameService.playing;
|
||||
}
|
||||
get previewInfo() {
|
||||
return this.previewPony ? this.previewPony.ponyInfo : this.pony.ponyInfo;
|
||||
}
|
||||
get previewName() {
|
||||
return this.previewPony ? this.previewPony.name : this.pony.name;
|
||||
}
|
||||
get previewTag() {
|
||||
return getPonyTag(this.previewPony || this.pony, this.account);
|
||||
}
|
||||
get customOutlines() {
|
||||
return this.info.customOutlines;
|
||||
}
|
||||
get ponies() {
|
||||
return this.model.ponies;
|
||||
}
|
||||
get pony() {
|
||||
return this.model.pony;
|
||||
}
|
||||
set pony(value: PonyObject) {
|
||||
this.model.selectPony(value);
|
||||
}
|
||||
get info() {
|
||||
return this.pony.ponyInfo!;
|
||||
}
|
||||
get maneFill() {
|
||||
return getBaseFill(this.info.mane);
|
||||
}
|
||||
get coatFill() {
|
||||
return this.info.coatFill;
|
||||
}
|
||||
get hoovesFill() {
|
||||
return getBaseFill(this.info.frontHooves);
|
||||
}
|
||||
get canExport() {
|
||||
return DEVELOPMENT;
|
||||
}
|
||||
get site() {
|
||||
return findById(this.sites, this.pony.site) || this.sites[0];
|
||||
}
|
||||
set site(value: SocialSiteInfo) {
|
||||
this.pony.site = value.id;
|
||||
}
|
||||
get tag() {
|
||||
return findById(this.tags, this.pony.tag) || this.tags[0];
|
||||
}
|
||||
set tag(value: CharacterTag) {
|
||||
this.pony.tag = value.id;
|
||||
}
|
||||
get lockEyeWhites() {
|
||||
return !this.info.unlockEyeWhites;
|
||||
}
|
||||
set lockEyeWhites(value) {
|
||||
this.info.unlockEyeWhites = !value;
|
||||
}
|
||||
get darken() {
|
||||
return !this.info.freeOutlines;
|
||||
}
|
||||
get lockFrontLegAccessory() {
|
||||
return !this.info.unlockFrontLegAccessory;
|
||||
}
|
||||
set lockFrontLegAccessory(value) {
|
||||
this.info.unlockFrontLegAccessory = !value;
|
||||
}
|
||||
get lockBackLegAccessory() {
|
||||
return !this.info.unlockBackLegAccessory;
|
||||
}
|
||||
set lockBackLegAccessory(value) {
|
||||
this.info.unlockBackLegAccessory = !value;
|
||||
}
|
||||
get lockEyelashColor() {
|
||||
return !this.info.unlockEyelashColor;
|
||||
}
|
||||
set lockEyelashColor(value) {
|
||||
this.info.unlockEyelashColor = !value;
|
||||
}
|
||||
icon(id: string) {
|
||||
return getProviderIcon(id);
|
||||
}
|
||||
hasSleeves(type: number) {
|
||||
return SLEEVED_ACCESSORIES.indexOf(type) !== -1;
|
||||
}
|
||||
ngOnInit() {
|
||||
if (this.model.account) {
|
||||
this.tags.push(...getAvailableTags(this.model.account));
|
||||
}
|
||||
const fangs = this.fangs![1]!;
|
||||
fangs.color = happy.color;
|
||||
fangs.extra = happy.fangs;
|
||||
fangs.timestamp = Date.now();
|
||||
}
|
||||
get account() {
|
||||
return this.model.account;
|
||||
}
|
||||
get loading() {
|
||||
return this.model.loading || this.model.updating;
|
||||
}
|
||||
get updateWarning() {
|
||||
return this.gameService.updateWarning;
|
||||
}
|
||||
get playing() {
|
||||
return this.gameService.playing;
|
||||
}
|
||||
get previewInfo() {
|
||||
return this.previewPony ? this.previewPony.ponyInfo : this.pony.ponyInfo;
|
||||
}
|
||||
get previewName() {
|
||||
return this.previewPony ? this.previewPony.name : this.pony.name;
|
||||
}
|
||||
get previewTag() {
|
||||
return getPonyTag(this.previewPony || this.pony, this.account);
|
||||
}
|
||||
get customOutlines() {
|
||||
return this.info.customOutlines;
|
||||
}
|
||||
get ponies() {
|
||||
return this.model.ponies;
|
||||
}
|
||||
get pony() {
|
||||
return this.model.pony;
|
||||
}
|
||||
set pony(value: PonyObject) {
|
||||
this.model.selectPony(value);
|
||||
}
|
||||
get info() {
|
||||
return this.pony.ponyInfo!;
|
||||
}
|
||||
get maneFill() {
|
||||
return getBaseFill(this.info.mane);
|
||||
}
|
||||
get coatFill() {
|
||||
return this.info.coatFill;
|
||||
}
|
||||
get hoovesFill() {
|
||||
return getBaseFill(this.info.frontHooves);
|
||||
}
|
||||
get canExport() {
|
||||
return DEVELOPMENT;
|
||||
}
|
||||
get site() {
|
||||
return findById(this.sites, this.pony.site) || this.sites[0];
|
||||
}
|
||||
set site(value: SocialSiteInfo) {
|
||||
this.pony.site = value.id;
|
||||
}
|
||||
get tag() {
|
||||
return findById(this.tags, this.pony.tag) || this.tags[0];
|
||||
}
|
||||
set tag(value: CharacterTag) {
|
||||
this.pony.tag = value.id;
|
||||
}
|
||||
get lockEyeWhites() {
|
||||
return !this.info.unlockEyeWhites;
|
||||
}
|
||||
set lockEyeWhites(value) {
|
||||
this.info.unlockEyeWhites = !value;
|
||||
}
|
||||
get darken() {
|
||||
return !this.info.freeOutlines;
|
||||
}
|
||||
get lockFrontLegAccessory() {
|
||||
return !this.info.unlockFrontLegAccessory;
|
||||
}
|
||||
set lockFrontLegAccessory(value) {
|
||||
this.info.unlockFrontLegAccessory = !value;
|
||||
}
|
||||
get lockBackLegAccessory() {
|
||||
return !this.info.unlockBackLegAccessory;
|
||||
}
|
||||
set lockBackLegAccessory(value) {
|
||||
this.info.unlockBackLegAccessory = !value;
|
||||
}
|
||||
get lockEyelashColor() {
|
||||
return !this.info.unlockEyelashColor;
|
||||
}
|
||||
set lockEyelashColor(value) {
|
||||
this.info.unlockEyelashColor = !value;
|
||||
}
|
||||
icon(id: string) {
|
||||
return getProviderIcon(id);
|
||||
}
|
||||
hasSleeves(type: number) {
|
||||
return SLEEVED_ACCESSORIES.indexOf(type) !== -1;
|
||||
}
|
||||
ngOnInit() {
|
||||
if (this.model.account) {
|
||||
this.tags.push(...getAvailableTags(this.model.account));
|
||||
}
|
||||
|
||||
this.sites = this.model.sites.filter(s => !!s.name);
|
||||
this.updateMuzzles();
|
||||
this.sites = this.model.sites.filter(s => !!s.name);
|
||||
this.updateMuzzles();
|
||||
|
||||
let last = Date.now();
|
||||
let last = Date.now();
|
||||
|
||||
return loadAndInitSpriteSheets().then(() => {
|
||||
this.loaded = true;
|
||||
this.interval = setInterval(() => {
|
||||
const now = Date.now();
|
||||
this.update((now - last) / 1000);
|
||||
last = now;
|
||||
}, 1000 / 24);
|
||||
});
|
||||
}
|
||||
ngOnDestroy() {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
changed() {
|
||||
if (!this.syncTimeout) {
|
||||
this.syncTimeout = requestAnimationFrame(() => {
|
||||
this.syncTimeout = undefined;
|
||||
syncLockedPonyInfo(this.info);
|
||||
});
|
||||
}
|
||||
return loadAndInitSpriteSheets().then(() => {
|
||||
this.loaded = true;
|
||||
this.interval = setInterval(() => {
|
||||
const now = Date.now();
|
||||
this.update((now - last) / 1000);
|
||||
last = now;
|
||||
}, 1000 / 24);
|
||||
});
|
||||
}
|
||||
ngOnDestroy() {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
changed() {
|
||||
if (!this.syncTimeout) {
|
||||
this.syncTimeout = requestAnimationFrame(() => {
|
||||
this.syncTimeout = undefined;
|
||||
syncLockedPonyInfo(this.info);
|
||||
});
|
||||
}
|
||||
|
||||
if (DEVELOPMENT || BETA) {
|
||||
this.state.blushColor = blushColor(parseColorWithAlpha(this.coatFill || '', 1));
|
||||
}
|
||||
}
|
||||
update(delta: number) {
|
||||
this.animationTime += delta;
|
||||
if (DEVELOPMENT || BETA) {
|
||||
this.state.blushColor = blushColor(parseColorWithAlpha(this.coatFill || '', 1));
|
||||
}
|
||||
}
|
||||
update(delta: number) {
|
||||
this.animationTime += delta;
|
||||
|
||||
const animation = this.animations[this.activeAnimation]();
|
||||
this.state.animation = animation;
|
||||
const animation = this.animations[this.activeAnimation]();
|
||||
this.state.animation = animation;
|
||||
|
||||
if (this.playAnimation) {
|
||||
this.state.animationFrame = Math.floor(this.animationTime * animation.fps) % animation.frames.length;
|
||||
}
|
||||
}
|
||||
copyCoatColorToTail() {
|
||||
if (this.info.tail && this.info.tail.fills) {
|
||||
this.info.tail.fills[0] = this.info.coatFill;
|
||||
this.changed();
|
||||
}
|
||||
}
|
||||
eyeColorLockChanged(locked: boolean) {
|
||||
if (locked) {
|
||||
this.info.eyeColorLeft = this.info.eyeColorRight;
|
||||
}
|
||||
}
|
||||
eyeWhiteLockChanged(locked: boolean) {
|
||||
if (locked) {
|
||||
this.info.eyeWhitesLeft = this.info.eyeWhites;
|
||||
}
|
||||
}
|
||||
eyeOpennessChanged(locked: boolean) {
|
||||
if (locked) {
|
||||
this.info.eyeOpennessLeft = this.info.eyeOpennessRight;
|
||||
}
|
||||
}
|
||||
eyelashLockChanged(locked: boolean) {
|
||||
if (locked) {
|
||||
this.info.eyelashColorLeft = this.info.eyelashColor;
|
||||
}
|
||||
}
|
||||
select(pony: PonyObject | undefined) {
|
||||
if (pony) {
|
||||
this.deleting = false;
|
||||
this.pony = pony;
|
||||
}
|
||||
}
|
||||
setActiveAnimation(index: number) {
|
||||
this.activeAnimation = index;
|
||||
this.animationTime = 0;
|
||||
}
|
||||
freeOutlinesChanged(_free: boolean) {
|
||||
this.changed();
|
||||
}
|
||||
darkenLockedOutlinesChanged(_darken: boolean) {
|
||||
this.changed();
|
||||
}
|
||||
get canSave() {
|
||||
return !this.model.pending && !!this.pony && !!this.pony.name && !this.savingLocked;
|
||||
}
|
||||
save() {
|
||||
if (this.canSave) {
|
||||
this.error = undefined;
|
||||
this.deleting = false;
|
||||
this.savingLocked = true;
|
||||
if (this.playAnimation) {
|
||||
this.state.animationFrame = Math.floor(this.animationTime * animation.fps) % animation.frames.length;
|
||||
}
|
||||
}
|
||||
copyCoatColorToTail() {
|
||||
if (this.info.tail && this.info.tail.fills) {
|
||||
this.info.tail.fills[0] = this.info.coatFill;
|
||||
this.changed();
|
||||
}
|
||||
}
|
||||
eyeColorLockChanged(locked: boolean) {
|
||||
if (locked) {
|
||||
this.info.eyeColorLeft = this.info.eyeColorRight;
|
||||
}
|
||||
}
|
||||
eyeWhiteLockChanged(locked: boolean) {
|
||||
if (locked) {
|
||||
this.info.eyeWhitesLeft = this.info.eyeWhites;
|
||||
}
|
||||
}
|
||||
eyeOpennessChanged(locked: boolean) {
|
||||
if (locked) {
|
||||
this.info.eyeOpennessLeft = this.info.eyeOpennessRight;
|
||||
}
|
||||
}
|
||||
eyelashLockChanged(locked: boolean) {
|
||||
if (locked) {
|
||||
this.info.eyelashColorLeft = this.info.eyelashColor;
|
||||
}
|
||||
}
|
||||
select(pony: PonyObject | undefined) {
|
||||
if (pony) {
|
||||
this.deleting = false;
|
||||
this.pony = pony;
|
||||
}
|
||||
}
|
||||
setActiveAnimation(index: number) {
|
||||
this.activeAnimation = index;
|
||||
this.animationTime = 0;
|
||||
}
|
||||
freeOutlinesChanged(_free: boolean) {
|
||||
this.changed();
|
||||
}
|
||||
darkenLockedOutlinesChanged(_darken: boolean) {
|
||||
this.changed();
|
||||
}
|
||||
get canSave() {
|
||||
return !this.model.pending && !!this.pony && !!this.pony.name && !this.savingLocked;
|
||||
}
|
||||
save() {
|
||||
if (this.canSave) {
|
||||
this.error = undefined;
|
||||
this.deleting = false;
|
||||
this.savingLocked = true;
|
||||
|
||||
this.model.savePony(this.pony)
|
||||
.catch((e: Error) => this.error = e.message)
|
||||
.then(() => delay(2000))
|
||||
.then(() => this.savingLocked = false);
|
||||
}
|
||||
}
|
||||
get canRevert() {
|
||||
return !!findById(this.ponies, this.pony.id);
|
||||
}
|
||||
revert() {
|
||||
if (this.canRevert) {
|
||||
this.select(findById(this.ponies, this.pony.id));
|
||||
}
|
||||
}
|
||||
get canDuplicate() {
|
||||
return this.ponies.length < this.model.characterLimit;
|
||||
}
|
||||
duplicate() {
|
||||
if (this.canDuplicate) {
|
||||
this.deleting = false;
|
||||
this.pony = cloneDeep(this.pony);
|
||||
this.pony.name = '';
|
||||
this.pony.id = '';
|
||||
}
|
||||
}
|
||||
export(index?: number) {
|
||||
const frameWidth = 80;
|
||||
const frameHeight = 90;
|
||||
const animations = index === undefined ? this.animations.map(a => a()) : [this.animations[index]()];
|
||||
const frames = animations.reduce((sum, a) => sum + a.frames.length, 0);
|
||||
const info = toPalette(this.info);
|
||||
const options = defaultDrawPonyOptions();
|
||||
this.model.savePony(this.pony)
|
||||
.catch((e: Error) => this.error = e.message)
|
||||
.then(() => delay(2000))
|
||||
.then(() => this.savingLocked = false);
|
||||
}
|
||||
}
|
||||
get canRevert() {
|
||||
return !!findById(this.ponies, this.pony.id);
|
||||
}
|
||||
revert() {
|
||||
if (this.canRevert) {
|
||||
this.select(findById(this.ponies, this.pony.id));
|
||||
}
|
||||
}
|
||||
get canDuplicate() {
|
||||
return this.ponies.length < this.model.characterLimit;
|
||||
}
|
||||
duplicate() {
|
||||
if (this.canDuplicate) {
|
||||
this.deleting = false;
|
||||
this.pony = cloneDeep(this.pony);
|
||||
this.pony.name = '';
|
||||
this.pony.id = '';
|
||||
}
|
||||
}
|
||||
export(index?: number) {
|
||||
const frameWidth = 80;
|
||||
const frameHeight = 90;
|
||||
const animations = index === undefined ? this.animations.map(a => a()) : [this.animations[index]()];
|
||||
const frames = animations.reduce((sum, a) => sum + a.frames.length, 0);
|
||||
const info = toPalette(this.info);
|
||||
const options = defaultDrawPonyOptions();
|
||||
|
||||
const canvas = drawCanvas(frameWidth * frames, frameHeight, sprites.paletteSpriteSheet, TRANSPARENT, batch => {
|
||||
let i = 0;
|
||||
const canvas = drawCanvas(frameWidth * frames, frameHeight, sprites.paletteSpriteSheet, TRANSPARENT, batch => {
|
||||
let i = 0;
|
||||
|
||||
animations.forEach(a => {
|
||||
for (let f = 0; f < a.frames.length; f++ , i++) {
|
||||
const state: PonyState = {
|
||||
...defaultPonyState(),
|
||||
animation: a,
|
||||
animationFrame: f,
|
||||
blinkFrame: 1,
|
||||
};
|
||||
animations.forEach(a => {
|
||||
for (let f = 0; f < a.frames.length; f++ , i++) {
|
||||
const state: PonyState = {
|
||||
...defaultPonyState(),
|
||||
animation: a,
|
||||
animationFrame: f,
|
||||
blinkFrame: 1,
|
||||
};
|
||||
|
||||
drawPony(batch, info, state, i * frameWidth + frameWidth / 2, frameHeight - 10, options);
|
||||
}
|
||||
});
|
||||
});
|
||||
drawPony(batch, info, state, i * frameWidth + frameWidth / 2, frameHeight - 10, options);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const name = animations.length === 1 ? animations[0].name : 'all';
|
||||
saveCanvas(canvas, `${this.pony.name}-${name}.png`);
|
||||
}
|
||||
import() {
|
||||
if (DEVELOPMENT) {
|
||||
const data = prompt('enter data');
|
||||
const name = animations.length === 1 ? animations[0].name : 'all';
|
||||
saveCanvas(canvas, `${this.pony.name}-${name}.png`);
|
||||
}
|
||||
import() {
|
||||
if (DEVELOPMENT) {
|
||||
const data = prompt('enter data');
|
||||
|
||||
if (data) {
|
||||
this.importPony(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
private importPony(data: string) {
|
||||
if (DEVELOPMENT) {
|
||||
this.pony.ponyInfo = decompressPonyString(data, true);
|
||||
const t = decompressPonyString(data, false);
|
||||
console.log(JSON.stringify(t, undefined, 2));
|
||||
}
|
||||
}
|
||||
addBlush() {
|
||||
if (DEVELOPMENT || BETA) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
expression: createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Blush),
|
||||
};
|
||||
this.changed();
|
||||
}
|
||||
}
|
||||
testSize() {
|
||||
function stringifyValues(values: any[]): string {
|
||||
return values.map(x => JSON.stringify(x)).join(typeof values[0] === 'object' ? ',\n\t' : ', ');
|
||||
}
|
||||
if (data) {
|
||||
this.importPony(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
private importPony(data: string) {
|
||||
if (DEVELOPMENT) {
|
||||
this.pony.ponyInfo = decompressPonyString(data, true);
|
||||
const t = decompressPonyString(data, false);
|
||||
console.log(JSON.stringify(t, undefined, 2));
|
||||
}
|
||||
}
|
||||
addBlush() {
|
||||
if (DEVELOPMENT || BETA) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
expression: createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Blush),
|
||||
};
|
||||
this.changed();
|
||||
}
|
||||
}
|
||||
testSize() {
|
||||
function stringifyValues(values: any[]): string {
|
||||
return values.map(x => JSON.stringify(x)).join(typeof values[0] === 'object' ? ',\n\t' : ', ');
|
||||
}
|
||||
|
||||
if (DEVELOPMENT) {
|
||||
const compressed = compressPonyString(this.info);
|
||||
const regularSize = JSON.stringify(this.info).length;
|
||||
const ponyInfoNumber = decompressPony(compressed);
|
||||
const precomp = precompressPony(ponyInfoNumber, BLACK, x => x) as any;
|
||||
const details = Object.keys(precomp)
|
||||
.filter(key => key !== 'version')
|
||||
.map(key => ({ key, values: precomp[key] || [] as any[] }))
|
||||
.map(({ key, values }) => `${key}: [\n\t${stringifyValues(values)}\n]`)
|
||||
.join(',\n');
|
||||
const serialized = compressPonyString(this.info);
|
||||
if (DEVELOPMENT) {
|
||||
const compressed = compressPonyString(this.info);
|
||||
const regularSize = JSON.stringify(this.info).length;
|
||||
const ponyInfoNumber = decompressPony(compressed);
|
||||
const precomp = precompressPony(ponyInfoNumber, BLACK, x => x) as any;
|
||||
const details = Object.keys(precomp)
|
||||
.filter(key => key !== 'version')
|
||||
.map(key => ({ key, values: precomp[key] || [] as any[] }))
|
||||
.map(({ key, values }) => `${key}: [\n\t${stringifyValues(values)}\n]`)
|
||||
.join(',\n');
|
||||
const serialized = compressPonyString(this.info);
|
||||
|
||||
console.log(serialized);
|
||||
console.log(details);
|
||||
console.log(`${serialized.length} / ${regularSize}`);
|
||||
}
|
||||
}
|
||||
testJSON() {
|
||||
if (DEVELOPMENT) {
|
||||
console.log(JSON.stringify(this.info, undefined, 2));
|
||||
}
|
||||
}
|
||||
exportPony() {
|
||||
const data = ponyToExport(this.pony) + '\r\n';
|
||||
saveAs(new Blob([data], { type: 'text/plain;charset=utf-8' }), `${this.pony.name}.txt`);
|
||||
}
|
||||
exportPonies() {
|
||||
const data = this.ponies.map(ponyToExport).join('\r\n') + '\r\n';
|
||||
saveAs(new Blob([data], { type: 'text/plain;charset=utf-8' }), 'ponies.txt');
|
||||
}
|
||||
async importPonies(file: File | undefined) {
|
||||
if (file) {
|
||||
const text = await readFileAsText(file);
|
||||
const lines = text.split(/\r?\n/g);
|
||||
let imported = 0;
|
||||
console.log(serialized);
|
||||
console.log(details);
|
||||
console.log(`${serialized.length} / ${regularSize}`);
|
||||
}
|
||||
}
|
||||
testJSON() {
|
||||
if (DEVELOPMENT) {
|
||||
console.log(JSON.stringify(this.info, undefined, 2));
|
||||
}
|
||||
}
|
||||
exportPony() {
|
||||
const data = ponyToExport(this.pony) + '\r\n';
|
||||
saveAs(new Blob([data], { type: 'text/plain;charset=utf-8' }), `${this.pony.name}.txt`);
|
||||
}
|
||||
exportPonies() {
|
||||
const data = this.ponies.map(ponyToExport).join('\r\n') + '\r\n';
|
||||
saveAs(new Blob([data], { type: 'text/plain;charset=utf-8' }), 'ponies.txt');
|
||||
}
|
||||
async importPonies(file: File | undefined) {
|
||||
if (file) {
|
||||
const text = await readFileAsText(file);
|
||||
const lines = text.split(/\r?\n/g);
|
||||
let imported = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const [name, info, desc = ''] = line.split(/\t/g);
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const [name, info, desc = ''] = line.split(/\t/g);
|
||||
|
||||
if (name && info) {
|
||||
const pony: PonyObject = {
|
||||
name,
|
||||
id: '',
|
||||
info,
|
||||
desc,
|
||||
ponyInfo: decompressPonyString(info, true),
|
||||
};
|
||||
if (name && info) {
|
||||
const pony: PonyObject = {
|
||||
name,
|
||||
id: '',
|
||||
info,
|
||||
desc,
|
||||
ponyInfo: decompressPonyString(info, true),
|
||||
};
|
||||
|
||||
await this.model.savePony(pony, true);
|
||||
imported++;
|
||||
}
|
||||
} catch (e) {
|
||||
DEVELOPMENT && console.error(e);
|
||||
}
|
||||
}
|
||||
await this.model.savePony(pony, true);
|
||||
imported++;
|
||||
}
|
||||
} catch (e) {
|
||||
DEVELOPMENT && console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
alert(`Imported ${imported} ponies`);
|
||||
}
|
||||
}
|
||||
alert(`Imported ${imported} ponies`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ponyToExport(pony: PonyObject) {
|
||||
return `${pony.name}\t${pony.info}\t${pony.desc || ''}`.trim();
|
||||
return `${pony.name}\t${pony.info}\t${pony.desc || ''}`.trim();
|
||||
}
|
||||
|
||||
@@ -9,197 +9,197 @@ import { BLACK } from '../../../common/colors';
|
||||
import { Entity, Engine, EngineInfo, DebugFlags, tileTypeNames } from '../../../common/interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'editor-box',
|
||||
templateUrl: 'editor-box.pug',
|
||||
selector: 'editor-box',
|
||||
templateUrl: 'editor-box.pug',
|
||||
})
|
||||
export class EditorBox {
|
||||
readonly dev = DEVELOPMENT;
|
||||
readonly cogIcon = faCog;
|
||||
readonly editIcon = faEdit;
|
||||
readonly selectIcon = faDrawPolygon;
|
||||
readonly deleteIcon = faTrash;
|
||||
readonly checkIcon = faCheck;
|
||||
readonly emptyIcon = emptyIcon;
|
||||
readonly tiles = ['---', ...tileTypeNames];
|
||||
readonly engines = engines;
|
||||
readonly editorEntities: string[];
|
||||
readonly showFields: (keyof DebugFlags)[] = ['id', 'bounds', 'collider', 'cover', 'interact', 'trigger'];
|
||||
private showEditor = false;
|
||||
constructor(
|
||||
public model: Model,
|
||||
private game: PonyTownGame,
|
||||
private storage: StorageService,
|
||||
private zone: NgZone,
|
||||
) {
|
||||
this.game.editor.type = this.storage.getItem('editor-entity') || 'rock';
|
||||
this.showEditor = this.storage.getBoolean('show-editor');
|
||||
this.editorEntities = getEntityNames().slice().sort();
|
||||
}
|
||||
get editor() {
|
||||
return this.game.editor;
|
||||
}
|
||||
get hasElevation() {
|
||||
return this.game.engine === Engine.LayeredTiles;
|
||||
}
|
||||
get editorElevation() {
|
||||
return this.game.editor.elevation;
|
||||
}
|
||||
get editorSpecial() {
|
||||
return this.game.editor.special;
|
||||
}
|
||||
get editorEntity() {
|
||||
return this.game.editor.type;
|
||||
}
|
||||
set editorEntity(value: string) {
|
||||
this.game.editor.type = value;
|
||||
this.storage.setItem('editor-entity', value);
|
||||
}
|
||||
get editorTile() {
|
||||
return this.game.editor.tile;
|
||||
}
|
||||
set editorTile(value: number) {
|
||||
this.game.editor.tile = value;
|
||||
}
|
||||
get hasEditor() {
|
||||
return this.model.isMod && this.showEditor;
|
||||
}
|
||||
get oneEntity() {
|
||||
return this.editor.selectedEntities[0];
|
||||
}
|
||||
get singleEntity() {
|
||||
return this.editor.selectedEntities.length === 1;
|
||||
}
|
||||
get hasSelectedEntities() {
|
||||
return this.editor.selectedEntities.length > 0;
|
||||
}
|
||||
get isLightEntity() {
|
||||
return this.editor.selectedEntities.some(e => !!e.drawLight);
|
||||
}
|
||||
get isLightSpriteEntity() {
|
||||
return this.editor.selectedEntities.some(e => !!e.drawLightSprite);
|
||||
}
|
||||
get selectingEntities() {
|
||||
return this.game.editor.selectingEntities;
|
||||
}
|
||||
set selectingEntities(value) {
|
||||
this.game.editor.selectingEntities = value;
|
||||
}
|
||||
get shadowOpacity() {
|
||||
return getAlpha(this.game.shadowColor);
|
||||
}
|
||||
set shadowOpacity(value) {
|
||||
this.game.shadowColor = withAlpha(this.game.shadowColor, value);
|
||||
}
|
||||
private getEntityName(type: number) {
|
||||
return getEntityNameFromType(type);
|
||||
}
|
||||
private getEntityValue<T>(map: (entity: Entity) => T) {
|
||||
const entity = this.editor.selectedEntities[0] as any;
|
||||
return map(entity);
|
||||
}
|
||||
get entityName() {
|
||||
const entities = this.editor.selectedEntities;
|
||||
const types = entities.map(e => e.type);
|
||||
const names = uniq(types).map(type => this.getEntityName(type)).join(', ');
|
||||
return types.length === 1 ? `${names} [${entities[0].id}]` : names;
|
||||
}
|
||||
get entityLightColor() {
|
||||
return colorToHexRGB(this.getEntityValue(e => e && e.lightColor || BLACK));
|
||||
}
|
||||
set entityLightColor(value) {
|
||||
this.editor.selectedEntities.forEach(e => e.lightColor = parseColor(value));
|
||||
}
|
||||
get entityLightSpriteColor() {
|
||||
const entity = this.editor.selectedEntities[0];
|
||||
return colorToHexRGB(entity && entity.lightSpriteColor || BLACK);
|
||||
}
|
||||
set entityLightSpriteColor(value) {
|
||||
this.editor.selectedEntities.forEach(e => e.lightSpriteColor = parseColor(value));
|
||||
}
|
||||
get entityLightSpriteX() {
|
||||
const entity = this.editor.selectedEntities[0];
|
||||
return entity && entity.lightSpriteX || 0;
|
||||
}
|
||||
set entityLightSpriteX(value) {
|
||||
console.log('set x', value, this.editor.selectedEntities);
|
||||
this.editor.selectedEntities.forEach(e => e.lightSpriteX = value);
|
||||
}
|
||||
get entityLightSpriteY() {
|
||||
const entity = this.editor.selectedEntities[0];
|
||||
return entity && entity.lightSpriteY || 0;
|
||||
}
|
||||
set entityLightSpriteY(value) {
|
||||
this.editor.selectedEntities.forEach(e => e.lightSpriteY = value);
|
||||
}
|
||||
get entityLightScale() {
|
||||
return this.editor.selectedEntities.length ? this.editor.selectedEntities[0].lightScaleAdjust : 1;
|
||||
}
|
||||
set entityLightScale(value) {
|
||||
this.editor.selectedEntities.forEach(e => e.lightScaleAdjust = value);
|
||||
}
|
||||
get entityX() {
|
||||
return this.oneEntity.x;
|
||||
}
|
||||
set entityX(value) {
|
||||
this.oneEntity.x = value;
|
||||
const { id, x, y } = this.oneEntity;
|
||||
this.game.send(server => server.editorAction({
|
||||
type: 'move',
|
||||
entities: [{ id, x, y }],
|
||||
}));
|
||||
}
|
||||
get entityY() {
|
||||
return this.oneEntity.y;
|
||||
}
|
||||
set entityY(value) {
|
||||
this.oneEntity.y = value;
|
||||
const { id, x, y } = this.oneEntity;
|
||||
this.game.send(server => server.editorAction({
|
||||
type: 'move',
|
||||
entities: [{ id, x, y }],
|
||||
}));
|
||||
}
|
||||
editorClear() {
|
||||
this.game.send(server => server.editorAction({ type: 'clear' }));
|
||||
}
|
||||
clearLocalStorage() {
|
||||
this.storage.clear();
|
||||
}
|
||||
setEngine(engine: EngineInfo) {
|
||||
this.game.engine = engine.engine;
|
||||
}
|
||||
isActiveEngine(engine: EngineInfo) {
|
||||
return this.game.engine === engine.engine;
|
||||
}
|
||||
toggleEditor() {
|
||||
this.zone.run(() => {
|
||||
this.showEditor = !this.showEditor;
|
||||
this.storage.setBoolean('show-editor', this.showEditor);
|
||||
});
|
||||
}
|
||||
toggleSelecting() {
|
||||
this.selectingEntities = !this.selectingEntities;
|
||||
readonly dev = DEVELOPMENT;
|
||||
readonly cogIcon = faCog;
|
||||
readonly editIcon = faEdit;
|
||||
readonly selectIcon = faDrawPolygon;
|
||||
readonly deleteIcon = faTrash;
|
||||
readonly checkIcon = faCheck;
|
||||
readonly emptyIcon = emptyIcon;
|
||||
readonly tiles = ['---', ...tileTypeNames];
|
||||
readonly engines = engines;
|
||||
readonly editorEntities: string[];
|
||||
readonly showFields: (keyof DebugFlags)[] = ['id', 'bounds', 'collider', 'cover', 'interact', 'trigger'];
|
||||
private showEditor = false;
|
||||
constructor(
|
||||
public model: Model,
|
||||
private game: PonyTownGame,
|
||||
private storage: StorageService,
|
||||
private zone: NgZone,
|
||||
) {
|
||||
this.game.editor.type = this.storage.getItem('editor-entity') || 'rock';
|
||||
this.showEditor = this.storage.getBoolean('show-editor');
|
||||
this.editorEntities = getEntityNames().slice().sort();
|
||||
}
|
||||
get editor() {
|
||||
return this.game.editor;
|
||||
}
|
||||
get hasElevation() {
|
||||
return this.game.engine === Engine.LayeredTiles;
|
||||
}
|
||||
get editorElevation() {
|
||||
return this.game.editor.elevation;
|
||||
}
|
||||
get editorSpecial() {
|
||||
return this.game.editor.special;
|
||||
}
|
||||
get editorEntity() {
|
||||
return this.game.editor.type;
|
||||
}
|
||||
set editorEntity(value: string) {
|
||||
this.game.editor.type = value;
|
||||
this.storage.setItem('editor-entity', value);
|
||||
}
|
||||
get editorTile() {
|
||||
return this.game.editor.tile;
|
||||
}
|
||||
set editorTile(value: number) {
|
||||
this.game.editor.tile = value;
|
||||
}
|
||||
get hasEditor() {
|
||||
return this.model.isMod && this.showEditor;
|
||||
}
|
||||
get oneEntity() {
|
||||
return this.editor.selectedEntities[0];
|
||||
}
|
||||
get singleEntity() {
|
||||
return this.editor.selectedEntities.length === 1;
|
||||
}
|
||||
get hasSelectedEntities() {
|
||||
return this.editor.selectedEntities.length > 0;
|
||||
}
|
||||
get isLightEntity() {
|
||||
return this.editor.selectedEntities.some(e => !!e.drawLight);
|
||||
}
|
||||
get isLightSpriteEntity() {
|
||||
return this.editor.selectedEntities.some(e => !!e.drawLightSprite);
|
||||
}
|
||||
get selectingEntities() {
|
||||
return this.game.editor.selectingEntities;
|
||||
}
|
||||
set selectingEntities(value) {
|
||||
this.game.editor.selectingEntities = value;
|
||||
}
|
||||
get shadowOpacity() {
|
||||
return getAlpha(this.game.shadowColor);
|
||||
}
|
||||
set shadowOpacity(value) {
|
||||
this.game.shadowColor = withAlpha(this.game.shadowColor, value);
|
||||
}
|
||||
private getEntityName(type: number) {
|
||||
return getEntityNameFromType(type);
|
||||
}
|
||||
private getEntityValue<T>(map: (entity: Entity) => T) {
|
||||
const entity = this.editor.selectedEntities[0] as any;
|
||||
return map(entity);
|
||||
}
|
||||
get entityName() {
|
||||
const entities = this.editor.selectedEntities;
|
||||
const types = entities.map(e => e.type);
|
||||
const names = uniq(types).map(type => this.getEntityName(type)).join(', ');
|
||||
return types.length === 1 ? `${names} [${entities[0].id}]` : names;
|
||||
}
|
||||
get entityLightColor() {
|
||||
return colorToHexRGB(this.getEntityValue(e => e && e.lightColor || BLACK));
|
||||
}
|
||||
set entityLightColor(value) {
|
||||
this.editor.selectedEntities.forEach(e => e.lightColor = parseColor(value));
|
||||
}
|
||||
get entityLightSpriteColor() {
|
||||
const entity = this.editor.selectedEntities[0];
|
||||
return colorToHexRGB(entity && entity.lightSpriteColor || BLACK);
|
||||
}
|
||||
set entityLightSpriteColor(value) {
|
||||
this.editor.selectedEntities.forEach(e => e.lightSpriteColor = parseColor(value));
|
||||
}
|
||||
get entityLightSpriteX() {
|
||||
const entity = this.editor.selectedEntities[0];
|
||||
return entity && entity.lightSpriteX || 0;
|
||||
}
|
||||
set entityLightSpriteX(value) {
|
||||
console.log('set x', value, this.editor.selectedEntities);
|
||||
this.editor.selectedEntities.forEach(e => e.lightSpriteX = value);
|
||||
}
|
||||
get entityLightSpriteY() {
|
||||
const entity = this.editor.selectedEntities[0];
|
||||
return entity && entity.lightSpriteY || 0;
|
||||
}
|
||||
set entityLightSpriteY(value) {
|
||||
this.editor.selectedEntities.forEach(e => e.lightSpriteY = value);
|
||||
}
|
||||
get entityLightScale() {
|
||||
return this.editor.selectedEntities.length ? this.editor.selectedEntities[0].lightScaleAdjust : 1;
|
||||
}
|
||||
set entityLightScale(value) {
|
||||
this.editor.selectedEntities.forEach(e => e.lightScaleAdjust = value);
|
||||
}
|
||||
get entityX() {
|
||||
return this.oneEntity.x;
|
||||
}
|
||||
set entityX(value) {
|
||||
this.oneEntity.x = value;
|
||||
const { id, x, y } = this.oneEntity;
|
||||
this.game.send(server => server.editorAction({
|
||||
type: 'move',
|
||||
entities: [{ id, x, y }],
|
||||
}));
|
||||
}
|
||||
get entityY() {
|
||||
return this.oneEntity.y;
|
||||
}
|
||||
set entityY(value) {
|
||||
this.oneEntity.y = value;
|
||||
const { id, x, y } = this.oneEntity;
|
||||
this.game.send(server => server.editorAction({
|
||||
type: 'move',
|
||||
entities: [{ id, x, y }],
|
||||
}));
|
||||
}
|
||||
editorClear() {
|
||||
this.game.send(server => server.editorAction({ type: 'clear' }));
|
||||
}
|
||||
clearLocalStorage() {
|
||||
this.storage.clear();
|
||||
}
|
||||
setEngine(engine: EngineInfo) {
|
||||
this.game.engine = engine.engine;
|
||||
}
|
||||
isActiveEngine(engine: EngineInfo) {
|
||||
return this.game.engine === engine.engine;
|
||||
}
|
||||
toggleEditor() {
|
||||
this.zone.run(() => {
|
||||
this.showEditor = !this.showEditor;
|
||||
this.storage.setBoolean('show-editor', this.showEditor);
|
||||
});
|
||||
}
|
||||
toggleSelecting() {
|
||||
this.selectingEntities = !this.selectingEntities;
|
||||
|
||||
if (!this.selectingEntities) {
|
||||
this.editor.selectedEntities.length = 0;
|
||||
}
|
||||
}
|
||||
listEntities() {
|
||||
this.game.send(server => server.editorAction({ type: 'list' }));
|
||||
}
|
||||
deleteEntities() {
|
||||
const entities = this.editor.selectedEntities.map(e => e.id);
|
||||
this.game.send(server => server.editorAction({ type: 'remove', entities }));
|
||||
this.editor.selectedEntities.length = 0;
|
||||
}
|
||||
showEntitiesInfo() {
|
||||
console.log(this.editor.selectedEntities);
|
||||
}
|
||||
toggleShow(field: keyof DebugFlags) {
|
||||
(this.game.debug as any)[field] = !this.isShow(field);
|
||||
this.game.saveDebug();
|
||||
}
|
||||
isShow(field: keyof DebugFlags) {
|
||||
return !!this.game.debug[field];
|
||||
}
|
||||
if (!this.selectingEntities) {
|
||||
this.editor.selectedEntities.length = 0;
|
||||
}
|
||||
}
|
||||
listEntities() {
|
||||
this.game.send(server => server.editorAction({ type: 'list' }));
|
||||
}
|
||||
deleteEntities() {
|
||||
const entities = this.editor.selectedEntities.map(e => e.id);
|
||||
this.game.send(server => server.editorAction({ type: 'remove', entities }));
|
||||
this.editor.selectedEntities.length = 0;
|
||||
}
|
||||
showEntitiesInfo() {
|
||||
console.log(this.editor.selectedEntities);
|
||||
}
|
||||
toggleShow(field: keyof DebugFlags) {
|
||||
(this.game.debug as any)[field] = !this.isShow(field);
|
||||
this.game.saveDebug();
|
||||
}
|
||||
isShow(field: keyof DebugFlags) {
|
||||
return !!this.game.debug[field];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,16 @@ import { faArrowLeft, faArrowRight, faArrowUp, faArrowDown } from '../../../clie
|
||||
import { contactEmail } from '../../../client/data';
|
||||
|
||||
@Component({
|
||||
selector: 'help',
|
||||
templateUrl: 'help.pug',
|
||||
styleUrls: ['help.scss'],
|
||||
selector: 'help',
|
||||
templateUrl: 'help.pug',
|
||||
styleUrls: ['help.scss'],
|
||||
})
|
||||
export class Help {
|
||||
readonly leftIcon = faArrowLeft;
|
||||
readonly rightIcon = faArrowRight;
|
||||
readonly upIcon = faArrowUp;
|
||||
readonly downIcon = faArrowDown;
|
||||
readonly emotes = emojis.map(e => e.names[0]);
|
||||
readonly mac = /Macintosh/.test(navigator.userAgent);
|
||||
readonly contactEmail = contactEmail;
|
||||
readonly leftIcon = faArrowLeft;
|
||||
readonly rightIcon = faArrowRight;
|
||||
readonly upIcon = faArrowUp;
|
||||
readonly downIcon = faArrowDown;
|
||||
readonly emotes = emojis.map(e => e.names[0]);
|
||||
readonly mac = /Macintosh/.test(navigator.userAgent);
|
||||
readonly contactEmail = contactEmail;
|
||||
}
|
||||
|
||||
@@ -6,51 +6,51 @@ import { InstallService } from '../../services/installService';
|
||||
import { OAuthProvider, PonyObject } from '../../../common/interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'home',
|
||||
templateUrl: 'home.pug',
|
||||
styleUrls: ['home.scss'],
|
||||
selector: 'home',
|
||||
templateUrl: 'home.pug',
|
||||
styleUrls: ['home.scss'],
|
||||
})
|
||||
export class Home {
|
||||
state = defaultPonyState();
|
||||
previewPony: PonyObject | undefined = undefined;
|
||||
error?: string;
|
||||
constructor(
|
||||
private gameService: GameService,
|
||||
private model: Model,
|
||||
private installService: InstallService,
|
||||
) {
|
||||
}
|
||||
get authError() {
|
||||
return this.model.authError;
|
||||
}
|
||||
get accountAlert() {
|
||||
return this.model.accountAlert;
|
||||
}
|
||||
get canInstall() {
|
||||
return this.installService.canInstall;
|
||||
}
|
||||
get playing() {
|
||||
return this.gameService.playing;
|
||||
}
|
||||
get loading() {
|
||||
return this.model.loading || this.model.updating;
|
||||
}
|
||||
get account() {
|
||||
return this.model.account;
|
||||
}
|
||||
get pony() {
|
||||
return this.model.pony;
|
||||
}
|
||||
get previewInfo() {
|
||||
return this.previewPony ? this.previewPony.ponyInfo : this.pony.ponyInfo;
|
||||
}
|
||||
get previewName() {
|
||||
return this.previewPony ? this.previewPony.name : this.pony.name;
|
||||
}
|
||||
get previewTag() {
|
||||
return getPonyTag(this.previewPony || this.pony, this.account);
|
||||
}
|
||||
signIn(provider: OAuthProvider) {
|
||||
this.model.signIn(provider);
|
||||
}
|
||||
state = defaultPonyState();
|
||||
previewPony: PonyObject | undefined = undefined;
|
||||
error?: string;
|
||||
constructor(
|
||||
private gameService: GameService,
|
||||
private model: Model,
|
||||
private installService: InstallService,
|
||||
) {
|
||||
}
|
||||
get authError() {
|
||||
return this.model.authError;
|
||||
}
|
||||
get accountAlert() {
|
||||
return this.model.accountAlert;
|
||||
}
|
||||
get canInstall() {
|
||||
return this.installService.canInstall;
|
||||
}
|
||||
get playing() {
|
||||
return this.gameService.playing;
|
||||
}
|
||||
get loading() {
|
||||
return this.model.loading || this.model.updating;
|
||||
}
|
||||
get account() {
|
||||
return this.model.account;
|
||||
}
|
||||
get pony() {
|
||||
return this.model.pony;
|
||||
}
|
||||
get previewInfo() {
|
||||
return this.previewPony ? this.previewPony.ponyInfo : this.pony.ponyInfo;
|
||||
}
|
||||
get previewName() {
|
||||
return this.previewPony ? this.previewPony.name : this.pony.name;
|
||||
}
|
||||
get previewTag() {
|
||||
return getPonyTag(this.previewPony || this.pony, this.account);
|
||||
}
|
||||
signIn(provider: OAuthProvider) {
|
||||
this.model.signIn(provider);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+215
-215
@@ -5,247 +5,247 @@ import { Season, Holiday, MapType } from '../../common/interfaces';
|
||||
import { getUrl } from '../../client/rev';
|
||||
|
||||
interface Track {
|
||||
name: string;
|
||||
src: string[];
|
||||
howl?: Howl;
|
||||
name: string;
|
||||
src: string[];
|
||||
howl?: Howl;
|
||||
}
|
||||
|
||||
function getTracks(season: Season, holiday: Holiday, map: MapType) {
|
||||
switch (map) {
|
||||
case MapType.Island:
|
||||
return [
|
||||
'island',
|
||||
'sunny-island',
|
||||
];
|
||||
case MapType.House:
|
||||
return [
|
||||
'happy-house',
|
||||
'sweet-home',
|
||||
];
|
||||
case MapType.Cave:
|
||||
return [
|
||||
'cave-crystals',
|
||||
'cave-secrets',
|
||||
];
|
||||
default:
|
||||
return [
|
||||
//'largo',
|
||||
//'musicbox',
|
||||
//'unrest',
|
||||
'bossanova',
|
||||
'clop',
|
||||
'fivefour',
|
||||
'hypnosis',
|
||||
'scherzo',
|
||||
'trills',
|
||||
'waltzalt',
|
||||
...(season === Season.Winter ? [
|
||||
'trees-winter',
|
||||
'reindeer-winter',
|
||||
] : [
|
||||
'trees',
|
||||
'reindeer',
|
||||
]),
|
||||
'season',
|
||||
'ambient',
|
||||
'building',
|
||||
'school',
|
||||
'falling',
|
||||
'tio',
|
||||
'orchid',
|
||||
...(season === Season.Winter ? [
|
||||
'xmas-air',
|
||||
'xmas-horns',
|
||||
'xmas-presents',
|
||||
] : []),
|
||||
...(holiday === Holiday.Halloween ? [
|
||||
'ghost',
|
||||
'pumpkin',
|
||||
] : []),
|
||||
];
|
||||
}
|
||||
switch (map) {
|
||||
case MapType.Island:
|
||||
return [
|
||||
'island',
|
||||
'sunny-island',
|
||||
];
|
||||
case MapType.House:
|
||||
return [
|
||||
'happy-house',
|
||||
'sweet-home',
|
||||
];
|
||||
case MapType.Cave:
|
||||
return [
|
||||
'cave-crystals',
|
||||
'cave-secrets',
|
||||
];
|
||||
default:
|
||||
return [
|
||||
//'largo',
|
||||
//'musicbox',
|
||||
//'unrest',
|
||||
'bossanova',
|
||||
'clop',
|
||||
'fivefour',
|
||||
'hypnosis',
|
||||
'scherzo',
|
||||
'trills',
|
||||
'waltzalt',
|
||||
...(season === Season.Winter ? [
|
||||
'trees-winter',
|
||||
'reindeer-winter',
|
||||
] : [
|
||||
'trees',
|
||||
'reindeer',
|
||||
]),
|
||||
'season',
|
||||
'ambient',
|
||||
'building',
|
||||
'school',
|
||||
'falling',
|
||||
'tio',
|
||||
'orchid',
|
||||
...(season === Season.Winter ? [
|
||||
'xmas-air',
|
||||
'xmas-horns',
|
||||
'xmas-presents',
|
||||
] : []),
|
||||
...(holiday === Holiday.Halloween ? [
|
||||
'ghost',
|
||||
'pumpkin',
|
||||
] : []),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
const FADE_TRACKS = true;
|
||||
|
||||
function fadeOut(track: Track, id: number, volume: number) {
|
||||
const howl = track && track.howl;
|
||||
const howl = track && track.howl;
|
||||
|
||||
if (howl) {
|
||||
if (FADE_TRACKS) {
|
||||
howl
|
||||
.fade(volume, 0, 1000, id)
|
||||
.once('fade', () => howl.pause(id).stop(id), id);
|
||||
} else {
|
||||
howl
|
||||
.volume(0, id)
|
||||
.pause(id)
|
||||
.stop(id);
|
||||
}
|
||||
}
|
||||
if (howl) {
|
||||
if (FADE_TRACKS) {
|
||||
howl
|
||||
.fade(volume, 0, 1000, id)
|
||||
.once('fade', () => howl.pause(id).stop(id), id);
|
||||
} else {
|
||||
howl
|
||||
.volume(0, id)
|
||||
.pause(id)
|
||||
.stop(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fadeIn(track: Track, id: number, volume: number) {
|
||||
if (track && track.howl) {
|
||||
track.howl.fade(0, volume, 1000, id);
|
||||
}
|
||||
if (track && track.howl) {
|
||||
track.howl.fade(0, volume, 1000, id);
|
||||
}
|
||||
}
|
||||
|
||||
interface Instance {
|
||||
id: number;
|
||||
track: Track;
|
||||
id: number;
|
||||
track: Track;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class Audio {
|
||||
private tracks: Track[] = [];
|
||||
private volume = 0;
|
||||
private loops = 0;
|
||||
private playing = false;
|
||||
private stopped: Instance[] = [];
|
||||
private instance?: Instance;
|
||||
get trackName() {
|
||||
return this.instance && this.volume ? this.instance.track.name : '';
|
||||
}
|
||||
initTracks(season: Season, holiday: Holiday, map: MapType) {
|
||||
const tracks = getTracks(season, holiday, map);
|
||||
private tracks: Track[] = [];
|
||||
private volume = 0;
|
||||
private loops = 0;
|
||||
private playing = false;
|
||||
private stopped: Instance[] = [];
|
||||
private instance?: Instance;
|
||||
get trackName() {
|
||||
return this.instance && this.volume ? this.instance.track.name : '';
|
||||
}
|
||||
initTracks(season: Season, holiday: Holiday, map: MapType) {
|
||||
const tracks = getTracks(season, holiday, map);
|
||||
|
||||
// Make new tracks more frequent
|
||||
// const duplicateTracks = tracks.filter(t => t === 'ghost' || t === 'pumpkin');
|
||||
// tracks.push(...duplicateTracks);
|
||||
// tracks.push(...duplicateTracks);
|
||||
// Make new tracks more frequent
|
||||
// const duplicateTracks = tracks.filter(t => t === 'ghost' || t === 'pumpkin');
|
||||
// tracks.push(...duplicateTracks);
|
||||
// tracks.push(...duplicateTracks);
|
||||
|
||||
this.tracks = tracks.map(name => ({ name, src: [getUrl(`music/${name}.webm`), getUrl(`music/${name}.mp3`)] }));
|
||||
this.loops = 0;
|
||||
}
|
||||
setVolume(volume: number) {
|
||||
this.volume = volume / 100;
|
||||
this.tracks = tracks.map(name => ({ name, src: [getUrl(`music/${name}.webm`), getUrl(`music/${name}.mp3`)] }));
|
||||
this.loops = 0;
|
||||
}
|
||||
setVolume(volume: number) {
|
||||
this.volume = volume / 100;
|
||||
|
||||
if (this.playing) {
|
||||
if (this.instance) {
|
||||
this.setInstanceVolume(this.instance, this.volume);
|
||||
} else if (this.volume) {
|
||||
this.playRandomTrack();
|
||||
}
|
||||
}
|
||||
}
|
||||
play() {
|
||||
try {
|
||||
if (!this.playing) {
|
||||
this.playing = true;
|
||||
if (this.playing) {
|
||||
if (this.instance) {
|
||||
this.setInstanceVolume(this.instance, this.volume);
|
||||
} else if (this.volume) {
|
||||
this.playRandomTrack();
|
||||
}
|
||||
}
|
||||
}
|
||||
play() {
|
||||
try {
|
||||
if (!this.playing) {
|
||||
this.playing = true;
|
||||
|
||||
if (this.volume) {
|
||||
if (this.instance) {
|
||||
this.resumeInstance(this.instance);
|
||||
} else {
|
||||
this.playRandomTrack();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
playOrSwitchToRandomTrack() {
|
||||
if (FADE_TRACKS) {
|
||||
if (this.playing && this.volume) {
|
||||
this.playRandomTrack();
|
||||
} else {
|
||||
this.play();
|
||||
}
|
||||
} else {
|
||||
this.play();
|
||||
}
|
||||
}
|
||||
stop() {
|
||||
if (this.playing) {
|
||||
this.playing = false;
|
||||
this.stopInstance(this.instance);
|
||||
}
|
||||
}
|
||||
forcePlay() {
|
||||
if (!this.instance || !this.instance.track.howl!.playing(this.instance.id)) {
|
||||
this.playRandomTrack();
|
||||
}
|
||||
}
|
||||
touch() {
|
||||
this.stopInstances();
|
||||
this.setInstanceVolume(this.instance, this.volume);
|
||||
}
|
||||
private switchToTrack(track: Track) {
|
||||
if (this.instance && this.instance.track === track) {
|
||||
return false;
|
||||
} else {
|
||||
this.stopInstance(this.instance);
|
||||
this.instance = this.playTrack(track);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
playRandomTrack() {
|
||||
while (!this.switchToTrack(sample(this.tracks)!))
|
||||
;
|
||||
if (this.volume) {
|
||||
if (this.instance) {
|
||||
this.resumeInstance(this.instance);
|
||||
} else {
|
||||
this.playRandomTrack();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
playOrSwitchToRandomTrack() {
|
||||
if (FADE_TRACKS) {
|
||||
if (this.playing && this.volume) {
|
||||
this.playRandomTrack();
|
||||
} else {
|
||||
this.play();
|
||||
}
|
||||
} else {
|
||||
this.play();
|
||||
}
|
||||
}
|
||||
stop() {
|
||||
if (this.playing) {
|
||||
this.playing = false;
|
||||
this.stopInstance(this.instance);
|
||||
}
|
||||
}
|
||||
forcePlay() {
|
||||
if (!this.instance || !this.instance.track.howl!.playing(this.instance.id)) {
|
||||
this.playRandomTrack();
|
||||
}
|
||||
}
|
||||
touch() {
|
||||
this.stopInstances();
|
||||
this.setInstanceVolume(this.instance, this.volume);
|
||||
}
|
||||
private switchToTrack(track: Track) {
|
||||
if (this.instance && this.instance.track === track) {
|
||||
return false;
|
||||
} else {
|
||||
this.stopInstance(this.instance);
|
||||
this.instance = this.playTrack(track);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
playRandomTrack() {
|
||||
while (!this.switchToTrack(sample(this.tracks)!))
|
||||
;
|
||||
|
||||
this.loops = random(4, 7);
|
||||
}
|
||||
private playTrack(track: Track): Instance {
|
||||
this.prepareTrack(track);
|
||||
const id = track.howl!.play();
|
||||
fadeIn(track, id, this.volume);
|
||||
return { id, track };
|
||||
}
|
||||
private resumeInstance({ track, id }: Instance) {
|
||||
track.howl!.play(id);
|
||||
fadeIn(track, id, this.volume);
|
||||
}
|
||||
private stopInstance(instance: Instance | undefined) {
|
||||
if (instance) {
|
||||
this.stopped.push(instance);
|
||||
}
|
||||
this.loops = random(4, 7);
|
||||
}
|
||||
private playTrack(track: Track): Instance {
|
||||
this.prepareTrack(track);
|
||||
const id = track.howl!.play();
|
||||
fadeIn(track, id, this.volume);
|
||||
return { id, track };
|
||||
}
|
||||
private resumeInstance({ track, id }: Instance) {
|
||||
track.howl!.play(id);
|
||||
fadeIn(track, id, this.volume);
|
||||
}
|
||||
private stopInstance(instance: Instance | undefined) {
|
||||
if (instance) {
|
||||
this.stopped.push(instance);
|
||||
}
|
||||
|
||||
this.stopInstances();
|
||||
}
|
||||
private stopInstances() {
|
||||
this.stopped.forEach(({ track, id }) => fadeOut(track, id, this.volume));
|
||||
this.stopped = this.stopped.filter(({ track, id }) => track.howl!.playing(id));
|
||||
}
|
||||
private setInstanceVolume(instance: Instance | undefined, volume: number) {
|
||||
if (instance) {
|
||||
const howl = instance.track.howl!;
|
||||
howl.volume(volume, instance.id);
|
||||
this.stopInstances();
|
||||
}
|
||||
private stopInstances() {
|
||||
this.stopped.forEach(({ track, id }) => fadeOut(track, id, this.volume));
|
||||
this.stopped = this.stopped.filter(({ track, id }) => track.howl!.playing(id));
|
||||
}
|
||||
private setInstanceVolume(instance: Instance | undefined, volume: number) {
|
||||
if (instance) {
|
||||
const howl = instance.track.howl!;
|
||||
howl.volume(volume, instance.id);
|
||||
|
||||
if (volume && !howl.playing(instance.id)) {
|
||||
howl.play(instance.id);
|
||||
} else if (!volume && howl.playing(instance.id)) {
|
||||
howl.pause(instance.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
private prepareTrack(track: Track) {
|
||||
if (!track.howl) {
|
||||
track.howl = new Howl({
|
||||
src: track.src,
|
||||
loop: true,
|
||||
html5: true,
|
||||
});
|
||||
if (volume && !howl.playing(instance.id)) {
|
||||
howl.play(instance.id);
|
||||
} else if (!volume && howl.playing(instance.id)) {
|
||||
howl.pause(instance.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
private prepareTrack(track: Track) {
|
||||
if (!track.howl) {
|
||||
track.howl = new Howl({
|
||||
src: track.src,
|
||||
loop: true,
|
||||
html5: true,
|
||||
});
|
||||
|
||||
track.howl.on('end', id => this.onEnd(id));
|
||||
}
|
||||
}
|
||||
private handlingOnEnd = 0;
|
||||
private handlingOnEndAt = 0;
|
||||
private onEnd(id: number) {
|
||||
if (
|
||||
this.instance && this.instance.id === id && --this.loops < 0 &&
|
||||
(this.handlingOnEnd !== id || this.handlingOnEndAt < performance.now())
|
||||
) {
|
||||
this.handlingOnEnd = id;
|
||||
this.handlingOnEndAt = performance.now() + 500;
|
||||
track.howl.on('end', id => this.onEnd(id));
|
||||
}
|
||||
}
|
||||
private handlingOnEnd = 0;
|
||||
private handlingOnEndAt = 0;
|
||||
private onEnd(id: number) {
|
||||
if (
|
||||
this.instance && this.instance.id === id && --this.loops < 0 &&
|
||||
(this.handlingOnEnd !== id || this.handlingOnEndAt < performance.now())
|
||||
) {
|
||||
this.handlingOnEnd = id;
|
||||
this.handlingOnEndAt = performance.now() + 500;
|
||||
|
||||
if (this.volume && this.playing) {
|
||||
this.playRandomTrack();
|
||||
} else {
|
||||
this.stopInstance(this.instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.volume && this.playing) {
|
||||
this.playRandomTrack();
|
||||
} else {
|
||||
this.stopInstance(this.instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,20 +3,20 @@ import { Router, CanActivate } from '@angular/router';
|
||||
import { Model } from './model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(private router: Router, private model: Model) {
|
||||
}
|
||||
canActivate() {
|
||||
return this.model.accountPromise
|
||||
.then(account => {
|
||||
if (account) {
|
||||
return true;
|
||||
} else {
|
||||
this.router.navigate(['/']);
|
||||
return false;
|
||||
}
|
||||
}) as any;
|
||||
}
|
||||
constructor(private router: Router, private model: Model) {
|
||||
}
|
||||
canActivate() {
|
||||
return this.model.accountPromise
|
||||
.then(account => {
|
||||
if (account) {
|
||||
return true;
|
||||
} else {
|
||||
this.router.navigate(['/']);
|
||||
return false;
|
||||
}
|
||||
}) as any;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,44 +4,44 @@ import { Person } from '../../common/rollbar';
|
||||
|
||||
@Injectable()
|
||||
export class ErrorReporter {
|
||||
disable() {
|
||||
}
|
||||
configureUser(_person: Person) {
|
||||
}
|
||||
configureData(_data: any) {
|
||||
}
|
||||
captureEvent(_data: any) {
|
||||
}
|
||||
reportError(error: any, data?: any) {
|
||||
console.error(error, data);
|
||||
}
|
||||
createClientErrorHandler(socketOptions: ClientOptions): ClientErrorHandler {
|
||||
const handleRecvError = (error: Error, data: string | Uint8Array) => {
|
||||
if (error.message) {
|
||||
let method: string | undefined;
|
||||
disable() {
|
||||
}
|
||||
configureUser(_person: Person) {
|
||||
}
|
||||
configureData(_data: any) {
|
||||
}
|
||||
captureEvent(_data: any) {
|
||||
}
|
||||
reportError(error: any, data?: any) {
|
||||
console.error(error, data);
|
||||
}
|
||||
createClientErrorHandler(socketOptions: ClientOptions): ClientErrorHandler {
|
||||
const handleRecvError = (error: Error, data: string | Uint8Array) => {
|
||||
if (error.message) {
|
||||
let method: string | undefined;
|
||||
|
||||
if (data instanceof Uint8Array) {
|
||||
const bytes: number[] = [];
|
||||
const length = Math.min(data.length, 200);
|
||||
if (data instanceof Uint8Array) {
|
||||
const bytes: number[] = [];
|
||||
const length = Math.min(data.length, 200);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
bytes.push(data[i]);
|
||||
}
|
||||
for (let i = 0; i < length; i++) {
|
||||
bytes.push(data[i]);
|
||||
}
|
||||
|
||||
const trail = length < data.length ? '...' : '';
|
||||
const trail = length < data.length ? '...' : '';
|
||||
|
||||
if (data.length > 0) {
|
||||
const item = socketOptions.client[data[0]] as string | [string, any];
|
||||
method = typeof item === 'string' ? item : item[0];
|
||||
}
|
||||
if (data.length > 0) {
|
||||
const item = socketOptions.client[data[0]] as string | [string, any];
|
||||
method = typeof item === 'string' ? item : item[0];
|
||||
}
|
||||
|
||||
data = `<${bytes.toString()}${trail}>`;
|
||||
}
|
||||
data = `<${bytes.toString()}${trail}>`;
|
||||
}
|
||||
|
||||
this.reportError(error, { data, method });
|
||||
}
|
||||
};
|
||||
this.reportError(error, { data, method });
|
||||
}
|
||||
};
|
||||
|
||||
return { handleRecvError };
|
||||
}
|
||||
return { handleRecvError };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import { Injectable, NgZone } from '@angular/core';
|
||||
|
||||
export interface FrameLoop {
|
||||
init(): void;
|
||||
destroy(): void;
|
||||
init(): void;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class FrameService {
|
||||
constructor(private zone: NgZone) {
|
||||
}
|
||||
create(frame: (delta: number) => void) {
|
||||
const zone = this.zone;
|
||||
let ref = 0;
|
||||
let last = 0;
|
||||
constructor(private zone: NgZone) {
|
||||
}
|
||||
create(frame: (delta: number) => void) {
|
||||
const zone = this.zone;
|
||||
let ref = 0;
|
||||
let last = 0;
|
||||
|
||||
function tick(now: number) {
|
||||
ref = requestAnimationFrame(tick);
|
||||
frame((now - last) / 1000);
|
||||
last = now;
|
||||
}
|
||||
function tick(now: number) {
|
||||
ref = requestAnimationFrame(tick);
|
||||
frame((now - last) / 1000);
|
||||
last = now;
|
||||
}
|
||||
|
||||
return {
|
||||
init() {
|
||||
if (!ref) {
|
||||
last = performance.now();
|
||||
zone.runOutsideAngular(() => ref = requestAnimationFrame(tick));
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
cancelAnimationFrame(ref);
|
||||
ref = 0;
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
init() {
|
||||
if (!ref) {
|
||||
last = performance.now();
|
||||
zone.runOutsideAngular(() => ref = requestAnimationFrame(tick));
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
cancelAnimationFrame(ref);
|
||||
ref = 0;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,296 +17,296 @@ import { StorageService } from './storageService';
|
||||
export interface ClientSocketService extends SocketService<ClientActions, IServerActions> { }
|
||||
|
||||
function createSocket(
|
||||
gameService: GameService, game: PonyTownGame, model: Model, zone: NgZone, options: ClientOptions,
|
||||
token: string, errorHandler: ClientErrorHandler
|
||||
gameService: GameService, game: PonyTownGame, model: Model, zone: NgZone, options: ClientOptions,
|
||||
token: string, errorHandler: ClientErrorHandler
|
||||
): ClientSocketService {
|
||||
const socket = createClientSocket<ClientActions, IServerActions>(options, token, errorHandler);
|
||||
socket.client = new ClientActions(gameService, game, model, zone);
|
||||
const socket = createClientSocket<ClientActions, IServerActions>(options, token, errorHandler);
|
||||
socket.client = new ClientActions(gameService, game, model, zone);
|
||||
|
||||
if (!socket.supportsBinary) {
|
||||
throw new Error(BROWSER_NOT_SUPPORTED_ERROR);
|
||||
}
|
||||
if (!socket.supportsBinary) {
|
||||
throw new Error(BROWSER_NOT_SUPPORTED_ERROR);
|
||||
}
|
||||
|
||||
return socket;
|
||||
return socket;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class GameService {
|
||||
playing = false;
|
||||
joining = false;
|
||||
offline = false;
|
||||
protectionError = false;
|
||||
rateLimitError = false;
|
||||
versionError = false;
|
||||
version?: string;
|
||||
server?: ServerInfo;
|
||||
servers: ServerInfo[] = [];
|
||||
error?: string;
|
||||
leftMessage?: string;
|
||||
private safelyLeft = false;
|
||||
private gameLoop?: GameLoop;
|
||||
private disconnectedTimeout?: any;
|
||||
private initialized = false;
|
||||
private update?: boolean;
|
||||
private locked = false;
|
||||
constructor(
|
||||
private model: Model,
|
||||
private game: PonyTownGame,
|
||||
private zone: NgZone,
|
||||
private errorHandler: ErrorHandler,
|
||||
private errorReporter: ErrorReporter,
|
||||
private storage: StorageService,
|
||||
) {
|
||||
this.pollStatus();
|
||||
}
|
||||
get selected() {
|
||||
return this.game.selected;
|
||||
}
|
||||
get account() {
|
||||
return this.model.account;
|
||||
}
|
||||
get canPlay(): boolean {
|
||||
return !!this.model.pony &&
|
||||
!!this.model.pony.name &&
|
||||
!this.model.pending &&
|
||||
!this.joining &&
|
||||
!!this.server &&
|
||||
!this.server.offline &&
|
||||
!this.rateLimitError &&
|
||||
!this.versionError &&
|
||||
!this.locked;
|
||||
}
|
||||
get updateWarning(): boolean {
|
||||
return !!this.update;
|
||||
}
|
||||
get filterSwearWords(): boolean {
|
||||
return !!(this.server && this.server.filter)
|
||||
|| !!(this.account && this.account.settings && this.account.settings.filterSwearWords);
|
||||
}
|
||||
get wasPlaying() {
|
||||
return this.storage.getBoolean('playing');
|
||||
}
|
||||
join(ponyId: string) {
|
||||
this.errorReporter.captureEvent({ name: 'Join' });
|
||||
const server = this.server;
|
||||
playing = false;
|
||||
joining = false;
|
||||
offline = false;
|
||||
protectionError = false;
|
||||
rateLimitError = false;
|
||||
versionError = false;
|
||||
version?: string;
|
||||
server?: ServerInfo;
|
||||
servers: ServerInfo[] = [];
|
||||
error?: string;
|
||||
leftMessage?: string;
|
||||
private safelyLeft = false;
|
||||
private gameLoop?: GameLoop;
|
||||
private disconnectedTimeout?: any;
|
||||
private initialized = false;
|
||||
private update?: boolean;
|
||||
private locked = false;
|
||||
constructor(
|
||||
private model: Model,
|
||||
private game: PonyTownGame,
|
||||
private zone: NgZone,
|
||||
private errorHandler: ErrorHandler,
|
||||
private errorReporter: ErrorReporter,
|
||||
private storage: StorageService,
|
||||
) {
|
||||
this.pollStatus();
|
||||
}
|
||||
get selected() {
|
||||
return this.game.selected;
|
||||
}
|
||||
get account() {
|
||||
return this.model.account;
|
||||
}
|
||||
get canPlay(): boolean {
|
||||
return !!this.model.pony &&
|
||||
!!this.model.pony.name &&
|
||||
!this.model.pending &&
|
||||
!this.joining &&
|
||||
!!this.server &&
|
||||
!this.server.offline &&
|
||||
!this.rateLimitError &&
|
||||
!this.versionError &&
|
||||
!this.locked;
|
||||
}
|
||||
get updateWarning(): boolean {
|
||||
return !!this.update;
|
||||
}
|
||||
get filterSwearWords(): boolean {
|
||||
return !!(this.server && this.server.filter)
|
||||
|| !!(this.account && this.account.settings && this.account.settings.filterSwearWords);
|
||||
}
|
||||
get wasPlaying() {
|
||||
return this.storage.getBoolean('playing');
|
||||
}
|
||||
join(ponyId: string) {
|
||||
this.errorReporter.captureEvent({ name: 'Join' });
|
||||
const server = this.server;
|
||||
|
||||
if (this.playing || this.joining || !server) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (this.playing || this.joining || !server) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (typeof WebSocket === 'undefined' || typeof Float32Array === 'undefined') {
|
||||
return Promise.reject(new Error(BROWSER_NOT_SUPPORTED_ERROR));
|
||||
}
|
||||
if (typeof WebSocket === 'undefined' || typeof Float32Array === 'undefined') {
|
||||
return Promise.reject(new Error(BROWSER_NOT_SUPPORTED_ERROR));
|
||||
}
|
||||
|
||||
this.joining = true;
|
||||
this.leftMessage = undefined;
|
||||
this.safelyLeft = false;
|
||||
this.joining = true;
|
||||
this.leftMessage = undefined;
|
||||
this.safelyLeft = false;
|
||||
|
||||
return this.model.join(server.id, ponyId)
|
||||
.then(({ token, alert }) => {
|
||||
if (!this.joining) {
|
||||
return false;
|
||||
}
|
||||
return this.model.join(server.id, ponyId)
|
||||
.then(({ token, alert }) => {
|
||||
if (!this.joining) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
this.model.accountAlert = alert;
|
||||
this.joining = false;
|
||||
return false;
|
||||
}
|
||||
if (!token) {
|
||||
this.model.accountAlert = alert;
|
||||
this.joining = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.zone.runOutsideAngular(() => {
|
||||
const options = { ...socketOptions(), path: server.path, host: server.host };
|
||||
const errorHandler = this.errorReporter.createClientErrorHandler(options);
|
||||
const socket = createSocket(this, this.game, this.model, this.zone, options, token, errorHandler);
|
||||
return this.zone.runOutsideAngular(() => {
|
||||
const options = { ...socketOptions(), path: server.path, host: server.host };
|
||||
const errorHandler = this.errorReporter.createClientErrorHandler(options);
|
||||
const socket = createSocket(this, this.game, this.model, this.zone, options, token, errorHandler);
|
||||
|
||||
if (this.gameLoop) {
|
||||
this.gameLoop.cancel();
|
||||
}
|
||||
if (this.gameLoop) {
|
||||
this.gameLoop.cancel();
|
||||
}
|
||||
|
||||
this.game.startup(socket, this.model.isMod);
|
||||
this.gameLoop = startGameLoop(this.game, e => this.handleGameError(e));
|
||||
this.game.startup(socket, this.model.isMod);
|
||||
this.gameLoop = startGameLoop(this.game, e => this.handleGameError(e));
|
||||
|
||||
return this.gameLoop.started
|
||||
.then(() => {
|
||||
this.errorReporter.captureEvent({ name: 'gameLoop.started' });
|
||||
const socketConnected = this.pollUntilConnected(socket);
|
||||
socket.connect();
|
||||
return socketConnected;
|
||||
})
|
||||
.then(() => {
|
||||
this.errorReporter.captureEvent({ name: 'socketConnected' });
|
||||
return true;
|
||||
})
|
||||
.catch(e => {
|
||||
this.errorReporter.captureEvent({ name: 'socket.disconnect()', error: e.message });
|
||||
socket.disconnect();
|
||||
throw e;
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(joined => {
|
||||
this.errorReporter.captureEvent({ name: joined ? 'Joined game' : 'Not joined game' });
|
||||
})
|
||||
.catch((e: RequestError) => {
|
||||
this.errorReporter.captureEvent({ name: 'Failed to join game', error: e.message });
|
||||
return this.gameLoop.started
|
||||
.then(() => {
|
||||
this.errorReporter.captureEvent({ name: 'gameLoop.started' });
|
||||
const socketConnected = this.pollUntilConnected(socket);
|
||||
socket.connect();
|
||||
return socketConnected;
|
||||
})
|
||||
.then(() => {
|
||||
this.errorReporter.captureEvent({ name: 'socketConnected' });
|
||||
return true;
|
||||
})
|
||||
.catch(e => {
|
||||
this.errorReporter.captureEvent({ name: 'socket.disconnect()', error: e.message });
|
||||
socket.disconnect();
|
||||
throw e;
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(joined => {
|
||||
this.errorReporter.captureEvent({ name: joined ? 'Joined game' : 'Not joined game' });
|
||||
})
|
||||
.catch((e: RequestError) => {
|
||||
this.errorReporter.captureEvent({ name: 'Failed to join game', error: e.message });
|
||||
|
||||
// if (e.status && e.status > 500 && e.status < 500) {
|
||||
// this.rateLimitError = true;
|
||||
// setTimeout(() => this.rateLimitError = false, 5000);
|
||||
// }
|
||||
// if (e.status && e.status > 500 && e.status < 500) {
|
||||
// this.rateLimitError = true;
|
||||
// setTimeout(() => this.rateLimitError = false, 5000);
|
||||
// }
|
||||
|
||||
this.zone.run(() => this.left('join.catch'));
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
leave(reason: string) {
|
||||
this.errorReporter.captureEvent({ name: 'Leave', reason });
|
||||
this.game.leave();
|
||||
this.left('leave');
|
||||
}
|
||||
joined() {
|
||||
this.errorReporter.captureEvent({ name: 'Joined' });
|
||||
this.storage.setBoolean('playing', true);
|
||||
clearTimeout(this.disconnectedTimeout);
|
||||
setTimeout(() => {
|
||||
this.joining = false;
|
||||
this.playing = true;
|
||||
});
|
||||
}
|
||||
left(from: string, reason = LeaveReason.None) {
|
||||
this.errorReporter.captureEvent({ name: 'Left', from, reason });
|
||||
this.storage.setBoolean('playing', false);
|
||||
this.safelyLeft = true;
|
||||
this.zone.run(() => this.left('join.catch'));
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
leave(reason: string) {
|
||||
this.errorReporter.captureEvent({ name: 'Leave', reason });
|
||||
this.game.leave();
|
||||
this.left('leave');
|
||||
}
|
||||
joined() {
|
||||
this.errorReporter.captureEvent({ name: 'Joined' });
|
||||
this.storage.setBoolean('playing', true);
|
||||
clearTimeout(this.disconnectedTimeout);
|
||||
setTimeout(() => {
|
||||
this.joining = false;
|
||||
this.playing = true;
|
||||
});
|
||||
}
|
||||
left(from: string, reason = LeaveReason.None) {
|
||||
this.errorReporter.captureEvent({ name: 'Left', from, reason });
|
||||
this.storage.setBoolean('playing', false);
|
||||
this.safelyLeft = true;
|
||||
|
||||
if (reason === LeaveReason.Swearing) {
|
||||
this.leftMessage = 'Kicked for swearing or inappropriate language';
|
||||
this.locked = true;
|
||||
} else {
|
||||
this.leftMessage = undefined;
|
||||
}
|
||||
if (reason === LeaveReason.Swearing) {
|
||||
this.leftMessage = 'Kicked for swearing or inappropriate language';
|
||||
this.locked = true;
|
||||
} else {
|
||||
this.leftMessage = undefined;
|
||||
}
|
||||
|
||||
if (this.gameLoop) {
|
||||
this.errorReporter.captureEvent({ name: 'gameLoop.cancel()' });
|
||||
this.gameLoop.cancel();
|
||||
this.gameLoop = undefined;
|
||||
}
|
||||
if (this.gameLoop) {
|
||||
this.errorReporter.captureEvent({ name: 'gameLoop.cancel()' });
|
||||
this.gameLoop.cancel();
|
||||
this.gameLoop = undefined;
|
||||
}
|
||||
|
||||
clearTimeout(this.disconnectedTimeout);
|
||||
clearTimeout(this.disconnectedTimeout);
|
||||
|
||||
setTimeout(() => {
|
||||
this.joining = false;
|
||||
this.playing = false;
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.joining = false;
|
||||
this.playing = false;
|
||||
});
|
||||
|
||||
if (this.locked) {
|
||||
setTimeout(() => {
|
||||
this.locked = false;
|
||||
}, 7000);
|
||||
}
|
||||
if (this.locked) {
|
||||
setTimeout(() => {
|
||||
this.locked = false;
|
||||
}, 7000);
|
||||
}
|
||||
|
||||
if (this.model.friends) {
|
||||
for (const friend of this.model.friends) {
|
||||
friend.online = false;
|
||||
friend.entityId = 0;
|
||||
}
|
||||
}
|
||||
if (this.model.friends) {
|
||||
for (const friend of this.model.friends) {
|
||||
friend.online = false;
|
||||
friend.entityId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
this.game.release();
|
||||
this.game.onLeft.next();
|
||||
}
|
||||
disconnected() {
|
||||
this.errorReporter.captureEvent({ name: 'Disconnected' });
|
||||
clearTimeout(this.disconnectedTimeout);
|
||||
this.game.release();
|
||||
this.game.onLeft.next();
|
||||
}
|
||||
disconnected() {
|
||||
this.errorReporter.captureEvent({ name: 'Disconnected' });
|
||||
clearTimeout(this.disconnectedTimeout);
|
||||
|
||||
if (!this.safelyLeft) {
|
||||
this.disconnectedTimeout = setTimeout(() => this.left('disconnected.timeout'), 10000);
|
||||
}
|
||||
}
|
||||
private pollStatus() {
|
||||
return this.getAndUpdateStatus(this.account)
|
||||
.finally(() => {
|
||||
setTimeout(() => this.pollStatus(), this.initialized ? 10000 : 500);
|
||||
});
|
||||
}
|
||||
private getAndUpdateStatus(account: AccountData | undefined) {
|
||||
if (this.joining || this.playing || !account || !isFocused()) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return this.model.status(this.initialized)
|
||||
.then(status => this.updateStatus(account, status))
|
||||
.catch((e: RequestError) => {
|
||||
DEVELOPMENT && console.error(e);
|
||||
this.offline = e.message === OFFLINE_ERROR;
|
||||
this.versionError = e.message === VERSION_ERROR;
|
||||
this.protectionError = e.message === PROTECTION_ERROR;
|
||||
});
|
||||
}
|
||||
}
|
||||
private updateStatus(account: AccountData, status: GameStatus) {
|
||||
this.initialized = true;
|
||||
this.offline = false;
|
||||
this.version = status.version;
|
||||
this.update = status.update;
|
||||
if (!this.safelyLeft) {
|
||||
this.disconnectedTimeout = setTimeout(() => this.left('disconnected.timeout'), 10000);
|
||||
}
|
||||
}
|
||||
private pollStatus() {
|
||||
return this.getAndUpdateStatus(this.account)
|
||||
.finally(() => {
|
||||
setTimeout(() => this.pollStatus(), this.initialized ? 10000 : 500);
|
||||
});
|
||||
}
|
||||
private getAndUpdateStatus(account: AccountData | undefined) {
|
||||
if (this.joining || this.playing || !account || !isFocused()) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return this.model.status(this.initialized)
|
||||
.then(status => this.updateStatus(account, status))
|
||||
.catch((e: RequestError) => {
|
||||
DEVELOPMENT && console.error(e);
|
||||
this.offline = e.message === OFFLINE_ERROR;
|
||||
this.versionError = e.message === VERSION_ERROR;
|
||||
this.protectionError = e.message === PROTECTION_ERROR;
|
||||
});
|
||||
}
|
||||
}
|
||||
private updateStatus(account: AccountData, status: GameStatus) {
|
||||
this.initialized = true;
|
||||
this.offline = false;
|
||||
this.version = status.version;
|
||||
this.update = status.update;
|
||||
|
||||
for (const server of status.servers) {
|
||||
const existing = findById(this.servers, server.id);
|
||||
for (const server of status.servers) {
|
||||
const existing = findById(this.servers, server.id);
|
||||
|
||||
if (existing) {
|
||||
merge(existing, server);
|
||||
} else if ('name' in server) {
|
||||
const info = server as ServerInfo;
|
||||
info.countryFlags = info.flag && /^[a-z]{2}( [a-z]{2})*$/.test(info.flag) ? info.flag.split(/ /g) : [];
|
||||
if (existing) {
|
||||
merge(existing, server);
|
||||
} else if ('name' in server) {
|
||||
const info = server as ServerInfo;
|
||||
info.countryFlags = info.flag && /^[a-z]{2}( [a-z]{2})*$/.test(info.flag) ? info.flag.split(/ /g) : [];
|
||||
|
||||
if (info.name && account && meetsRequirement(account, info.require)) {
|
||||
this.servers.push(info);
|
||||
}
|
||||
} else {
|
||||
// got new server on the list
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
if (info.name && account && meetsRequirement(account, info.require)) {
|
||||
this.servers.push(info);
|
||||
}
|
||||
} else {
|
||||
// got new server on the list
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = this.servers.length - 1; i >= 0; i--) {
|
||||
if (!findById(status.servers, this.servers[i].id)) {
|
||||
this.servers.splice(i, 1);
|
||||
}
|
||||
}
|
||||
for (let i = this.servers.length - 1; i >= 0; i--) {
|
||||
if (!findById(status.servers, this.servers[i].id)) {
|
||||
this.servers.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (isLanguage('ru')) {
|
||||
this.servers.sort(sortServersForRussian);
|
||||
}
|
||||
if (isLanguage('ru')) {
|
||||
this.servers.sort(sortServersForRussian);
|
||||
}
|
||||
|
||||
if (!this.server && account.settings.defaultServer) {
|
||||
this.server = findById(this.servers, account.settings.defaultServer);
|
||||
if (!this.server && account.settings.defaultServer) {
|
||||
this.server = findById(this.servers, account.settings.defaultServer);
|
||||
|
||||
if (DEVELOPMENT && /join/.test(this.model.pony.name)) {
|
||||
setTimeout(() => this.join(this.model.pony.id));
|
||||
}
|
||||
}
|
||||
if (DEVELOPMENT && /join/.test(this.model.pony.name)) {
|
||||
setTimeout(() => this.join(this.model.pony.id));
|
||||
}
|
||||
}
|
||||
|
||||
if (!includes(this.servers, this.server)) {
|
||||
this.server = undefined;
|
||||
}
|
||||
}
|
||||
private handleGameError(error: Error) {
|
||||
this.errorReporter.captureEvent({ name: 'handleGameError', error: error.message });
|
||||
this.error = error.message;
|
||||
this.errorHandler.handleError(error);
|
||||
this.leave('handleGameError');
|
||||
}
|
||||
private pollUntilConnected(socket: ClientSocketService) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const interval = setInterval(() => {
|
||||
if (socket.isConnected) {
|
||||
clearInterval(interval);
|
||||
this.zone.run(resolve);
|
||||
} else if (!this.joining) {
|
||||
clearInterval(interval);
|
||||
this.zone.run(() => reject(new Error('Cancelled (poll)')));
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
if (!includes(this.servers, this.server)) {
|
||||
this.server = undefined;
|
||||
}
|
||||
}
|
||||
private handleGameError(error: Error) {
|
||||
this.errorReporter.captureEvent({ name: 'handleGameError', error: error.message });
|
||||
this.error = error.message;
|
||||
this.errorHandler.handleError(error);
|
||||
this.leave('handleGameError');
|
||||
}
|
||||
private pollUntilConnected(socket: ClientSocketService) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const interval = setInterval(() => {
|
||||
if (socket.isConnected) {
|
||||
clearInterval(interval);
|
||||
this.zone.run(resolve);
|
||||
} else if (!this.joining) {
|
||||
clearInterval(interval);
|
||||
this.zone.run(() => reject(new Error('Cancelled (poll)')));
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,40 +2,40 @@ import { Injectable } from '@angular/core';
|
||||
import { StorageService } from './storageService';
|
||||
|
||||
interface InstallEvent extends Event {
|
||||
prompt(): void;
|
||||
userChoice: Promise<'accepted' | 'dismissed'>;
|
||||
prompt(): void;
|
||||
userChoice: Promise<'accepted' | 'dismissed'>;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class InstallService {
|
||||
private installEvent?: InstallEvent;
|
||||
constructor(private storage: StorageService) {
|
||||
if (!this.storage.getBoolean('install-dismissed')) {
|
||||
window.addEventListener('beforeinstallprompt', event => {
|
||||
event.preventDefault();
|
||||
this.installEvent = event as any;
|
||||
});
|
||||
}
|
||||
}
|
||||
get canInstall() {
|
||||
return !!this.installEvent || (DEVELOPMENT && localStorage.getItem('install'));
|
||||
}
|
||||
install() {
|
||||
if (!this.installEvent) {
|
||||
return Promise.reject(new Error('Cannot install'));
|
||||
}
|
||||
private installEvent?: InstallEvent;
|
||||
constructor(private storage: StorageService) {
|
||||
if (!this.storage.getBoolean('install-dismissed')) {
|
||||
window.addEventListener('beforeinstallprompt', event => {
|
||||
event.preventDefault();
|
||||
this.installEvent = event as any;
|
||||
});
|
||||
}
|
||||
}
|
||||
get canInstall() {
|
||||
return !!this.installEvent || (DEVELOPMENT && localStorage.getItem('install'));
|
||||
}
|
||||
install() {
|
||||
if (!this.installEvent) {
|
||||
return Promise.reject(new Error('Cannot install'));
|
||||
}
|
||||
|
||||
this.installEvent.prompt();
|
||||
this.installEvent.prompt();
|
||||
|
||||
return this.installEvent.userChoice
|
||||
.finally(() => {
|
||||
this.installEvent = undefined;
|
||||
});
|
||||
}
|
||||
dismiss() {
|
||||
this.installEvent = undefined;
|
||||
this.storage.setBoolean('install-dismissed', true);
|
||||
}
|
||||
return this.installEvent.userChoice
|
||||
.finally(() => {
|
||||
this.installEvent = undefined;
|
||||
});
|
||||
}
|
||||
dismiss() {
|
||||
this.installEvent = undefined;
|
||||
this.storage.setBoolean('install-dismissed', true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,43 +2,43 @@ import { Injectable, NgZone } from '@angular/core';
|
||||
import { removeItem } from '../../common/utils';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class IntervalUpdateService {
|
||||
private interval: any;
|
||||
private actions: (() => void)[] = [];
|
||||
constructor(private zone: NgZone) {
|
||||
}
|
||||
subscribe(action: () => void) {
|
||||
this.actions.push(action);
|
||||
private interval: any;
|
||||
private actions: (() => void)[] = [];
|
||||
constructor(private zone: NgZone) {
|
||||
}
|
||||
subscribe(action: () => void) {
|
||||
this.actions.push(action);
|
||||
|
||||
if (!this.interval) {
|
||||
this.zone.runOutsideAngular(() => {
|
||||
this.interval = setInterval(() => {
|
||||
this.actions.forEach(a => a());
|
||||
}, 1000 * 10);
|
||||
});
|
||||
}
|
||||
if (!this.interval) {
|
||||
this.zone.runOutsideAngular(() => {
|
||||
this.interval = setInterval(() => {
|
||||
this.actions.forEach(a => a());
|
||||
}, 1000 * 10);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
removeItem(this.actions, action);
|
||||
return () => {
|
||||
removeItem(this.actions, action);
|
||||
|
||||
if (this.actions.length === 0) {
|
||||
clearInterval(this.interval);
|
||||
this.interval = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
toggle(action: () => void) {
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
if (this.actions.length === 0) {
|
||||
clearInterval(this.interval);
|
||||
this.interval = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
toggle(action: () => void) {
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
|
||||
return (on: boolean) => {
|
||||
if (on && !unsubscribe) {
|
||||
unsubscribe = this.subscribe(action);
|
||||
} else if (!on && unsubscribe) {
|
||||
unsubscribe();
|
||||
unsubscribe = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
return (on: boolean) => {
|
||||
if (on && !unsubscribe) {
|
||||
unsubscribe = this.subscribe(action);
|
||||
} else if (!on && unsubscribe) {
|
||||
unsubscribe();
|
||||
unsubscribe = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,142 +4,142 @@ import { Document, LiveResponse, IAdminServerActions, BaseValues } from '../../c
|
||||
import { ClientAdminActions } from '../../client/clientAdminActions';
|
||||
|
||||
export interface Options<T> {
|
||||
// collection
|
||||
beforeUpdate?: (updates: T[]) => void;
|
||||
onUpdated?: (added: T[], all: T[]) => void;
|
||||
onFinished?: () => void;
|
||||
// item
|
||||
decode: (fields: any[], base: BaseValues) => T;
|
||||
onUpdate?: (oldItem: T, newItem: T) => void;
|
||||
onDelete?: (item: T) => void;
|
||||
deleteItems?: boolean;
|
||||
ignore?: (item: T) => boolean;
|
||||
// collection
|
||||
beforeUpdate?: (updates: T[]) => void;
|
||||
onUpdated?: (added: T[], all: T[]) => void;
|
||||
onFinished?: () => void;
|
||||
// item
|
||||
decode: (fields: any[], base: BaseValues) => T;
|
||||
onUpdate?: (oldItem: T, newItem: T) => void;
|
||||
onDelete?: (item: T) => void;
|
||||
deleteItems?: boolean;
|
||||
ignore?: (item: T) => boolean;
|
||||
}
|
||||
|
||||
export class LiveCollection<T extends Document> {
|
||||
items: T[] = [];
|
||||
finished = false;
|
||||
private running = true;
|
||||
private itemsMap = new Map<string, T>();
|
||||
private liveTimeout: any;
|
||||
constructor(
|
||||
private name: 'events',
|
||||
private rate: number,
|
||||
private getKey: (item: T) => string,
|
||||
private options: Options<T>,
|
||||
private socket: SocketService<ClientAdminActions, IAdminServerActions>,
|
||||
private timestamp = (new Date(0)).toISOString(),
|
||||
private logError = (e: Error) => console.error(e.stack),
|
||||
) {
|
||||
}
|
||||
get(key: string) {
|
||||
return this.itemsMap.get(key);
|
||||
}
|
||||
push(item: T) {
|
||||
this.items.push(item);
|
||||
this.itemsMap.set(this.getKey(item), item);
|
||||
return item;
|
||||
}
|
||||
remove(key: string) {
|
||||
return this.server.removeItem(this.name, key)
|
||||
.then(() => this.removeItem(key, true, true));
|
||||
}
|
||||
removeItem(key: string, deleted = false, removeFromList = false) {
|
||||
const item = this.itemsMap.get(key);
|
||||
items: T[] = [];
|
||||
finished = false;
|
||||
private running = true;
|
||||
private itemsMap = new Map<string, T>();
|
||||
private liveTimeout: any;
|
||||
constructor(
|
||||
private name: 'events',
|
||||
private rate: number,
|
||||
private getKey: (item: T) => string,
|
||||
private options: Options<T>,
|
||||
private socket: SocketService<ClientAdminActions, IAdminServerActions>,
|
||||
private timestamp = (new Date(0)).toISOString(),
|
||||
private logError = (e: Error) => console.error(e.stack),
|
||||
) {
|
||||
}
|
||||
get(key: string) {
|
||||
return this.itemsMap.get(key);
|
||||
}
|
||||
push(item: T) {
|
||||
this.items.push(item);
|
||||
this.itemsMap.set(this.getKey(item), item);
|
||||
return item;
|
||||
}
|
||||
remove(key: string) {
|
||||
return this.server.removeItem(this.name, key)
|
||||
.then(() => this.removeItem(key, true, true));
|
||||
}
|
||||
removeItem(key: string, deleted = false, removeFromList = false) {
|
||||
const item = this.itemsMap.get(key);
|
||||
|
||||
if (item) {
|
||||
if (removeFromList || this.options.deleteItems) {
|
||||
removeItem(this.items, item);
|
||||
this.itemsMap.delete(key);
|
||||
} else if (deleted) {
|
||||
item.deleted = true;
|
||||
}
|
||||
if (item) {
|
||||
if (removeFromList || this.options.deleteItems) {
|
||||
removeItem(this.items, item);
|
||||
this.itemsMap.delete(key);
|
||||
} else if (deleted) {
|
||||
item.deleted = true;
|
||||
}
|
||||
|
||||
if (deleted && this.options.onDelete) {
|
||||
this.options.onDelete(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
assignAccount(id: string, account: string) {
|
||||
return this.server.assignAccount(this.name, id, account);
|
||||
}
|
||||
live(): Promise<void> {
|
||||
if (!this.running)
|
||||
return Promise.resolve();
|
||||
if (deleted && this.options.onDelete) {
|
||||
this.options.onDelete(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
assignAccount(id: string, account: string) {
|
||||
return this.server.assignAccount(this.name, id, account);
|
||||
}
|
||||
live(): Promise<void> {
|
||||
if (!this.running)
|
||||
return Promise.resolve();
|
||||
|
||||
clearTimeout(this.liveTimeout);
|
||||
clearTimeout(this.liveTimeout);
|
||||
|
||||
return this.update()
|
||||
.catch(this.logError)
|
||||
.then(more => {
|
||||
this.liveTimeout = setTimeout(() => this.live(), more ? 100 : this.rate);
|
||||
});
|
||||
}
|
||||
stop() {
|
||||
this.running = false;
|
||||
}
|
||||
read({ updates, deletes, base, more }: LiveResponse, liveFetch = true) {
|
||||
const items = updates.map(i => this.options.decode(i, base));
|
||||
return this.update()
|
||||
.catch(this.logError)
|
||||
.then(more => {
|
||||
this.liveTimeout = setTimeout(() => this.live(), more ? 100 : this.rate);
|
||||
});
|
||||
}
|
||||
stop() {
|
||||
this.running = false;
|
||||
}
|
||||
read({ updates, deletes, base, more }: LiveResponse, liveFetch = true) {
|
||||
const items = updates.map(i => this.options.decode(i, base));
|
||||
|
||||
if (liveFetch) {
|
||||
const timestamp = items
|
||||
.reduce((max, i) => max.getTime() < i.updatedAt.getTime() ? i.updatedAt : max, new Date(this.timestamp));
|
||||
this.timestamp = timestamp.toISOString();
|
||||
}
|
||||
if (liveFetch) {
|
||||
const timestamp = items
|
||||
.reduce((max, i) => max.getTime() < i.updatedAt.getTime() ? i.updatedAt : max, new Date(this.timestamp));
|
||||
this.timestamp = timestamp.toISOString();
|
||||
}
|
||||
|
||||
if (this.options.beforeUpdate) {
|
||||
this.options.beforeUpdate(items);
|
||||
}
|
||||
if (this.options.beforeUpdate) {
|
||||
this.options.beforeUpdate(items);
|
||||
}
|
||||
|
||||
const { added, all } = this.applyUpdates(items, liveFetch);
|
||||
const { added, all } = this.applyUpdates(items, liveFetch);
|
||||
|
||||
if (this.options.onUpdated && items.length) {
|
||||
this.options.onUpdated(added, all);
|
||||
}
|
||||
if (this.options.onUpdated && items.length) {
|
||||
this.options.onUpdated(added, all);
|
||||
}
|
||||
|
||||
deletes.forEach(key => this.removeItem(key, true));
|
||||
deletes.forEach(key => this.removeItem(key, true));
|
||||
|
||||
if (liveFetch) {
|
||||
const finished = this.finished || !more;
|
||||
if (liveFetch) {
|
||||
const finished = this.finished || !more;
|
||||
|
||||
if (!this.finished && finished) {
|
||||
this.finished = true;
|
||||
if (!this.finished && finished) {
|
||||
this.finished = true;
|
||||
|
||||
if (this.options.onFinished) {
|
||||
this.options.onFinished();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.options.onFinished) {
|
||||
this.options.onFinished();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return more;
|
||||
}
|
||||
private get server() {
|
||||
return this.socket.server;
|
||||
}
|
||||
private update() {
|
||||
return this.socket.isConnected ? this.server.getAll(this.name, this.timestamp).then(r => this.read(r)) : Promise.resolve(false);
|
||||
}
|
||||
private applyUpdates(updates: T[], liveFetch: boolean) {
|
||||
const added: T[] = [];
|
||||
const all: T[] = [];
|
||||
return more;
|
||||
}
|
||||
private get server() {
|
||||
return this.socket.server;
|
||||
}
|
||||
private update() {
|
||||
return this.socket.isConnected ? this.server.getAll(this.name, this.timestamp).then(r => this.read(r)) : Promise.resolve(false);
|
||||
}
|
||||
private applyUpdates(updates: T[], liveFetch: boolean) {
|
||||
const added: T[] = [];
|
||||
const all: T[] = [];
|
||||
|
||||
updates.forEach(update => {
|
||||
const doc = this.get(this.getKey(update));
|
||||
updates.forEach(update => {
|
||||
const doc = this.get(this.getKey(update));
|
||||
|
||||
if (doc) {
|
||||
if (this.options.onUpdate) {
|
||||
this.options.onUpdate(doc, update);
|
||||
} else {
|
||||
Object.assign(doc, update);
|
||||
}
|
||||
all.push(doc);
|
||||
} else if (!liveFetch || !this.options.ignore || !this.options.ignore(update)) {
|
||||
this.push(update);
|
||||
added.push(update);
|
||||
all.push(update);
|
||||
}
|
||||
});
|
||||
if (doc) {
|
||||
if (this.options.onUpdate) {
|
||||
this.options.onUpdate(doc, update);
|
||||
} else {
|
||||
Object.assign(doc, update);
|
||||
}
|
||||
all.push(doc);
|
||||
} else if (!liveFetch || !this.options.ignore || !this.options.ignore(update)) {
|
||||
this.push(update);
|
||||
added.push(update);
|
||||
all.push(update);
|
||||
}
|
||||
});
|
||||
|
||||
return { added, all };
|
||||
}
|
||||
return { added, all };
|
||||
}
|
||||
}
|
||||
|
||||
+385
-385
@@ -5,18 +5,18 @@ import { merge } from 'lodash';
|
||||
import { Subject } from 'rxjs';
|
||||
import { HASH } from '../../generated/hash';
|
||||
import {
|
||||
AccountData, UpdateAccountData, AccountSettings, GameStatus, SocialSiteInfo, PonyObject, JoinResponse,
|
||||
OAuthProvider, EntitiesEditorInfo, FriendData, PalettePonyInfo, HiddenPlayer
|
||||
AccountData, UpdateAccountData, AccountSettings, GameStatus, SocialSiteInfo, PonyObject, JoinResponse,
|
||||
OAuthProvider, EntitiesEditorInfo, FriendData, PalettePonyInfo, HiddenPlayer
|
||||
} from '../../common/interfaces';
|
||||
import { createDefaultPony, syncLockedPonyInfo, mockPaletteManager } from '../../common/ponyInfo';
|
||||
import { removeById, observableToPromise, delay, computeFriendsCRC } from '../../common/utils';
|
||||
import { isMod, getSupporterInviteLimit, getCharacterLimit } from '../../common/accountUtils';
|
||||
import {
|
||||
NAME_ERROR, ACCESS_ERROR, CHARACTER_SAVING_ERROR, NOT_AUTHENTICATED_ERROR, OFFLINE_ERROR, PROTECTION_ERROR
|
||||
NAME_ERROR, ACCESS_ERROR, CHARACTER_SAVING_ERROR, NOT_AUTHENTICATED_ERROR, OFFLINE_ERROR, PROTECTION_ERROR
|
||||
} from '../../common/errors';
|
||||
import { version, host } from '../../client/data';
|
||||
import {
|
||||
toSocialSiteInfo, cleanName, validatePonyName, isStandalone, attachDebugMethod
|
||||
toSocialSiteInfo, cleanName, validatePonyName, isStandalone, attachDebugMethod
|
||||
} from '../../client/clientUtils';
|
||||
import { ErrorReporter } from './errorReporter';
|
||||
import { randomString } from '../../common/stringUtils';
|
||||
@@ -26,463 +26,463 @@ import { SECOND, PLAYER_DESC_MAX_LENGTH } from '../../common/constants';
|
||||
import { canUseTag } from '../../common/tags';
|
||||
|
||||
export interface Friend extends FriendData {
|
||||
entityId: number;
|
||||
crc: number;
|
||||
online: boolean;
|
||||
ponyInfo: PalettePonyInfo | undefined;
|
||||
actualName: string;
|
||||
entityId: number;
|
||||
crc: number;
|
||||
online: boolean;
|
||||
ponyInfo: PalettePonyInfo | undefined;
|
||||
actualName: string;
|
||||
}
|
||||
|
||||
const LIMIT_ERROR = 'Request limit reached, please wait';
|
||||
const noneSite: SocialSiteInfo = { id: '', name: 'none', url: '', icon: '', color: '#222' };
|
||||
const modStatus = {
|
||||
mod: false,
|
||||
check: {} as any,
|
||||
editor: {
|
||||
names: [],
|
||||
typeToName: [],
|
||||
nameToTypes: [],
|
||||
} as EntitiesEditorInfo,
|
||||
mod: false,
|
||||
check: {} as any,
|
||||
editor: {
|
||||
names: [],
|
||||
typeToName: [],
|
||||
nameToTypes: [],
|
||||
} as EntitiesEditorInfo,
|
||||
};
|
||||
|
||||
function compareStrings(a: string | undefined, b: string | undefined) {
|
||||
return (a || '').localeCompare(b || '');
|
||||
return (a || '').localeCompare(b || '');
|
||||
}
|
||||
|
||||
function comparePonies(a: PonyObject, b: PonyObject) {
|
||||
return compareStrings(a.name, b.name) || compareStrings(a.id, b.id);
|
||||
return compareStrings(a.name, b.name) || compareStrings(a.id, b.id);
|
||||
}
|
||||
|
||||
function getDefaultPony(ponies: PonyObject[]) {
|
||||
let result = ponies[0];
|
||||
let result = ponies[0];
|
||||
|
||||
for (let i = 1; i < ponies.length; i++) {
|
||||
if (compareStrings(result.lastUsed, ponies[i].lastUsed) < 0) {
|
||||
result = ponies[i];
|
||||
}
|
||||
}
|
||||
for (let i = 1; i < ponies.length; i++) {
|
||||
if (compareStrings(result.lastUsed, ponies[i].lastUsed) < 0) {
|
||||
result = ponies[i];
|
||||
}
|
||||
}
|
||||
|
||||
return result || createDefaultPonyObject();
|
||||
return result || createDefaultPonyObject();
|
||||
}
|
||||
|
||||
export function createDefaultPonyObject(): PonyObject {
|
||||
return {
|
||||
id: '',
|
||||
name: '',
|
||||
info: '',
|
||||
ponyInfo: createDefaultPony(),
|
||||
};
|
||||
return {
|
||||
id: '',
|
||||
name: '',
|
||||
info: '',
|
||||
ponyInfo: createDefaultPony(),
|
||||
};
|
||||
}
|
||||
|
||||
export function getPonyTag(pony: PonyObject, account: AccountData | undefined) {
|
||||
if (account) {
|
||||
const tag = canUseTag(account, pony.tag || '') ? pony.tag : undefined;
|
||||
return (!tag && account.supporter && !pony.hideSupport) ? `sup${account.supporter}` : tag;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
if (account) {
|
||||
const tag = canUseTag(account, pony.tag || '') ? pony.tag : undefined;
|
||||
return (!tag && account.supporter && !pony.hideSupport) ? `sup${account.supporter}` : tag;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const entityTypeToName = new Map<number, string>();
|
||||
const entityNameToTypes = new Map<string, number[]>();
|
||||
|
||||
export function getEntityNames() {
|
||||
return modStatus.editor.names;
|
||||
return modStatus.editor.names;
|
||||
}
|
||||
|
||||
export function getEntityTypesFromName(name: string) {
|
||||
return entityNameToTypes.get(name);
|
||||
return entityNameToTypes.get(name);
|
||||
}
|
||||
|
||||
export function getEntityNameFromType(type: number) {
|
||||
return entityTypeToName.get(type);
|
||||
return entityTypeToName.get(type);
|
||||
}
|
||||
|
||||
export function compareFriends(a: Friend, b: Friend) {
|
||||
return a.online !== b.online ? (a.online ? -1 : 1) : a.accountName.localeCompare(b.accountName);
|
||||
return a.online !== b.online ? (a.online ? -1 : 1) : a.accountName.localeCompare(b.accountName);
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class Model {
|
||||
loading = true;
|
||||
loadingError?: string;
|
||||
account?: AccountData;
|
||||
ponies: PonyObject[] = [];
|
||||
pending = false;
|
||||
sites: SocialSiteInfo[] = [noneSite];
|
||||
accountPromise!: Promise<AccountData | undefined>;
|
||||
accountChanged = new Subject<void>();
|
||||
protectionErrors = new Subject<void>();
|
||||
authError?: string;
|
||||
accountAlert?: string;
|
||||
mergedAccount = false;
|
||||
updating = false;
|
||||
updatingTakesLongTime = false;
|
||||
suffix = '';
|
||||
friends: Friend[] | undefined = undefined;
|
||||
private _pony: PonyObject = createDefaultPonyObject();
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private router: Router,
|
||||
private storage: StorageService,
|
||||
private errorReporter: ErrorReporter,
|
||||
) {
|
||||
this.initialize();
|
||||
loading = true;
|
||||
loadingError?: string;
|
||||
account?: AccountData;
|
||||
ponies: PonyObject[] = [];
|
||||
pending = false;
|
||||
sites: SocialSiteInfo[] = [noneSite];
|
||||
accountPromise!: Promise<AccountData | undefined>;
|
||||
accountChanged = new Subject<void>();
|
||||
protectionErrors = new Subject<void>();
|
||||
authError?: string;
|
||||
accountAlert?: string;
|
||||
mergedAccount = false;
|
||||
updating = false;
|
||||
updatingTakesLongTime = false;
|
||||
suffix = '';
|
||||
friends: Friend[] | undefined = undefined;
|
||||
private _pony: PonyObject = createDefaultPonyObject();
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private router: Router,
|
||||
private storage: StorageService,
|
||||
private errorReporter: ErrorReporter,
|
||||
) {
|
||||
this.initialize();
|
||||
|
||||
// handle completed sign-in
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('message', event => {
|
||||
if (event.data && event.data.type === 'loaded-page') {
|
||||
const path = event.data.path;
|
||||
// handle completed sign-in
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('message', event => {
|
||||
if (event.data && event.data.type === 'loaded-page') {
|
||||
const path = event.data.path;
|
||||
|
||||
if (event.source && 'close' in event.source) {
|
||||
event.source.close();
|
||||
}
|
||||
if (event.source && 'close' in event.source) {
|
||||
event.source.close();
|
||||
}
|
||||
|
||||
this.initialize();
|
||||
this.accountPromise.then(() => router.navigateByUrl(path));
|
||||
}
|
||||
});
|
||||
}
|
||||
this.initialize();
|
||||
this.accountPromise.then(() => router.navigateByUrl(path));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (DEVELOPMENT) {
|
||||
attachDebugMethod('ddos', () => this.protectionErrors.next());
|
||||
attachDebugMethod('userModel', this);
|
||||
}
|
||||
}
|
||||
private initialize() {
|
||||
this.loading = true;
|
||||
this.account = undefined;
|
||||
this.loadingError = undefined;
|
||||
this.accountAlert = undefined;
|
||||
this.ponies = [];
|
||||
this.friends = undefined;
|
||||
this.sites = [noneSite];
|
||||
this._pony = createDefaultPonyObject();
|
||||
this.storage.setItem('bid', this.storage.getItem('bid') || randomString(20));
|
||||
this.accountPromise = this.initializeAccount();
|
||||
}
|
||||
private initializeAccount(): Promise<AccountData | undefined> {
|
||||
return this.getAccount()
|
||||
.then(account => {
|
||||
if (!account) {
|
||||
throw new Error(ACCESS_ERROR);
|
||||
}
|
||||
if (DEVELOPMENT) {
|
||||
attachDebugMethod('ddos', () => this.protectionErrors.next());
|
||||
attachDebugMethod('userModel', this);
|
||||
}
|
||||
}
|
||||
private initialize() {
|
||||
this.loading = true;
|
||||
this.account = undefined;
|
||||
this.loadingError = undefined;
|
||||
this.accountAlert = undefined;
|
||||
this.ponies = [];
|
||||
this.friends = undefined;
|
||||
this.sites = [noneSite];
|
||||
this._pony = createDefaultPonyObject();
|
||||
this.storage.setItem('bid', this.storage.getItem('bid') || randomString(20));
|
||||
this.accountPromise = this.initializeAccount();
|
||||
}
|
||||
private initializeAccount(): Promise<AccountData | undefined> {
|
||||
return this.getAccount()
|
||||
.then(account => {
|
||||
if (!account) {
|
||||
throw new Error(ACCESS_ERROR);
|
||||
}
|
||||
|
||||
if ('limit' in account) {
|
||||
throw new Error(LIMIT_ERROR);
|
||||
}
|
||||
if ('limit' in account) {
|
||||
throw new Error(LIMIT_ERROR);
|
||||
}
|
||||
|
||||
this.errorReporter.configureUser({ id: account.id, username: account.name });
|
||||
this.errorReporter.configureUser({ id: account.id, username: account.name });
|
||||
|
||||
try {
|
||||
modStatus.mod = isMod(account);
|
||||
modStatus.check = account.check;
|
||||
modStatus.editor = account.editor || modStatus.editor;
|
||||
} catch { }
|
||||
try {
|
||||
modStatus.mod = isMod(account);
|
||||
modStatus.check = account.check;
|
||||
modStatus.editor = account.editor || modStatus.editor;
|
||||
} catch { }
|
||||
|
||||
if (modStatus.editor) {
|
||||
modStatus.editor.typeToName.forEach(({ type, name }) => entityTypeToName.set(type, name));
|
||||
modStatus.editor.nameToTypes.forEach(({ types, name }) => entityNameToTypes.set(name, types));
|
||||
}
|
||||
if (modStatus.editor) {
|
||||
modStatus.editor.typeToName.forEach(({ type, name }) => entityTypeToName.set(type, name));
|
||||
modStatus.editor.nameToTypes.forEach(({ types, name }) => entityNameToTypes.set(name, types));
|
||||
}
|
||||
|
||||
this.account = account;
|
||||
this.sites = [noneSite, ...(account.sites || []).map(toSocialSiteInfo)];
|
||||
this.ponies = account.ponies ? account.ponies.sort(comparePonies) : [];
|
||||
this.friends = undefined;
|
||||
this.account = account;
|
||||
this.sites = [noneSite, ...(account.sites || []).map(toSocialSiteInfo)];
|
||||
this.ponies = account.ponies ? account.ponies.sort(comparePonies) : [];
|
||||
this.friends = undefined;
|
||||
|
||||
this.selectPony(getDefaultPony(this.ponies));
|
||||
this.storage.setItem('vid', account.id);
|
||||
this.loading = false;
|
||||
this.accountAlert = account.alert;
|
||||
this.accountChanged.next();
|
||||
this.fetchFriends();
|
||||
this.selectPony(getDefaultPony(this.ponies));
|
||||
this.storage.setItem('vid', account.id);
|
||||
this.loading = false;
|
||||
this.accountAlert = account.alert;
|
||||
this.accountChanged.next();
|
||||
this.fetchFriends();
|
||||
|
||||
return account;
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
if (e.message === ACCESS_ERROR) {
|
||||
this.loading = false;
|
||||
this.storage.setItem('vid', '---');
|
||||
} else if (e.message === LIMIT_ERROR) {
|
||||
this.loadingError = 'request-limit';
|
||||
return delay(5000).then(() => this.initializeAccount());
|
||||
} else if (e.message === OFFLINE_ERROR) {
|
||||
this.loadingError = 'cannot-connect';
|
||||
return delay(5000).then(() => this.initializeAccount());
|
||||
} else if (e.message === PROTECTION_ERROR) {
|
||||
this.loadingError = 'cloudflare-error';
|
||||
this.protectionErrors.next();
|
||||
// } else if (e.message === VERSION_ERROR) {
|
||||
// this.updating = true;
|
||||
} else {
|
||||
setTimeout(() => this.loadingError = 'unexpected-error', 5 * SECOND);
|
||||
console.error(e);
|
||||
}
|
||||
return account;
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
if (e.message === ACCESS_ERROR) {
|
||||
this.loading = false;
|
||||
this.storage.setItem('vid', '---');
|
||||
} else if (e.message === LIMIT_ERROR) {
|
||||
this.loadingError = 'request-limit';
|
||||
return delay(5000).then(() => this.initializeAccount());
|
||||
} else if (e.message === OFFLINE_ERROR) {
|
||||
this.loadingError = 'cannot-connect';
|
||||
return delay(5000).then(() => this.initializeAccount());
|
||||
} else if (e.message === PROTECTION_ERROR) {
|
||||
this.loadingError = 'cloudflare-error';
|
||||
this.protectionErrors.next();
|
||||
// } else if (e.message === VERSION_ERROR) {
|
||||
// this.updating = true;
|
||||
} else {
|
||||
setTimeout(() => this.loadingError = 'unexpected-error', 5 * SECOND);
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
private fetchFriends() {
|
||||
this.getFriends()
|
||||
.then(friends => {
|
||||
this.friends = friends.map(f => ({
|
||||
...f,
|
||||
online: false,
|
||||
entityId: 0,
|
||||
crc: 0,
|
||||
ponyInfo: f.pony && decodePonyInfo(f.pony, mockPaletteManager) || undefined,
|
||||
actualName: '',
|
||||
})).sort(compareFriends);
|
||||
})
|
||||
.catch(e => {
|
||||
DEVELOPMENT && console.error(e);
|
||||
setTimeout(() => this.fetchFriends(), 5000);
|
||||
});
|
||||
}
|
||||
get characterLimit() {
|
||||
return this.account ? getCharacterLimit(this.account) : 0;
|
||||
}
|
||||
get supporterInviteLimit() {
|
||||
return this.account ? getSupporterInviteLimit(this.account) : 0;
|
||||
}
|
||||
get isMod() {
|
||||
return modStatus.mod;
|
||||
}
|
||||
get modCheck() {
|
||||
return modStatus.check;
|
||||
}
|
||||
get editorInfo() {
|
||||
return modStatus.editor;
|
||||
}
|
||||
get pony() {
|
||||
return this._pony;
|
||||
}
|
||||
get supporter() {
|
||||
return this.account && this.account.supporter || 0;
|
||||
}
|
||||
get missingBirthdate() {
|
||||
return !!this.account && !this.account.birthdate;
|
||||
}
|
||||
computeFriendsCRC() {
|
||||
return this.friends ? computeFriendsCRC(this.friends.map(f => f.accountId)) : 0;
|
||||
}
|
||||
parsePonyObject(pony: PonyObject): PonyObject {
|
||||
try {
|
||||
const ponyInfo = decompressPonyString(pony.info, true);
|
||||
return { ponyInfo, ...pony };
|
||||
} catch (e) {
|
||||
this.errorReporter.reportError(e, { ponyInfo: pony.info });
|
||||
this.errorReporter.reportError('Pony info reading error', { originalError: e.message, ponyInfo: pony.info });
|
||||
throw new Error('Error while reading pony info');
|
||||
}
|
||||
}
|
||||
selectPony(pony: PonyObject) {
|
||||
const copy = this.parsePonyObject(pony);
|
||||
copy.ponyInfo && syncLockedPonyInfo(copy.ponyInfo);
|
||||
this._pony = copy;
|
||||
}
|
||||
// account
|
||||
signIn(provider: OAuthProvider) {
|
||||
this.authError = undefined;
|
||||
this.openAuth(provider.url!);
|
||||
}
|
||||
connectSite(provider: OAuthProvider) {
|
||||
this.authError = undefined;
|
||||
this.openAuth(`${provider.url}/merge`);
|
||||
}
|
||||
signOut() {
|
||||
this.authError = undefined;
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
private fetchFriends() {
|
||||
this.getFriends()
|
||||
.then(friends => {
|
||||
this.friends = friends.map(f => ({
|
||||
...f,
|
||||
online: false,
|
||||
entityId: 0,
|
||||
crc: 0,
|
||||
ponyInfo: f.pony && decodePonyInfo(f.pony, mockPaletteManager) || undefined,
|
||||
actualName: '',
|
||||
})).sort(compareFriends);
|
||||
})
|
||||
.catch(e => {
|
||||
DEVELOPMENT && console.error(e);
|
||||
setTimeout(() => this.fetchFriends(), 5000);
|
||||
});
|
||||
}
|
||||
get characterLimit() {
|
||||
return this.account ? getCharacterLimit(this.account) : 0;
|
||||
}
|
||||
get supporterInviteLimit() {
|
||||
return this.account ? getSupporterInviteLimit(this.account) : 0;
|
||||
}
|
||||
get isMod() {
|
||||
return modStatus.mod;
|
||||
}
|
||||
get modCheck() {
|
||||
return modStatus.check;
|
||||
}
|
||||
get editorInfo() {
|
||||
return modStatus.editor;
|
||||
}
|
||||
get pony() {
|
||||
return this._pony;
|
||||
}
|
||||
get supporter() {
|
||||
return this.account && this.account.supporter || 0;
|
||||
}
|
||||
get missingBirthdate() {
|
||||
return !!this.account && !this.account.birthdate;
|
||||
}
|
||||
computeFriendsCRC() {
|
||||
return this.friends ? computeFriendsCRC(this.friends.map(f => f.accountId)) : 0;
|
||||
}
|
||||
parsePonyObject(pony: PonyObject): PonyObject {
|
||||
try {
|
||||
const ponyInfo = decompressPonyString(pony.info, true);
|
||||
return { ponyInfo, ...pony };
|
||||
} catch (e) {
|
||||
this.errorReporter.reportError(e, { ponyInfo: pony.info });
|
||||
this.errorReporter.reportError('Pony info reading error', { originalError: e.message, ponyInfo: pony.info });
|
||||
throw new Error('Error while reading pony info');
|
||||
}
|
||||
}
|
||||
selectPony(pony: PonyObject) {
|
||||
const copy = this.parsePonyObject(pony);
|
||||
copy.ponyInfo && syncLockedPonyInfo(copy.ponyInfo);
|
||||
this._pony = copy;
|
||||
}
|
||||
// account
|
||||
signIn(provider: OAuthProvider) {
|
||||
this.authError = undefined;
|
||||
this.openAuth(provider.url!);
|
||||
}
|
||||
connectSite(provider: OAuthProvider) {
|
||||
this.authError = undefined;
|
||||
this.openAuth(`${provider.url}/merge`);
|
||||
}
|
||||
signOut() {
|
||||
this.authError = undefined;
|
||||
|
||||
return this.post<AccountData | undefined>('/auth/sign-out', {}, false)
|
||||
.catch(e => console.error(e))
|
||||
.then(() => this.initialize())
|
||||
.then(() => this.router.navigate(['/']));
|
||||
}
|
||||
private openAuth(url: string) {
|
||||
url = `${host.replace(/\/$/, '')}${url}`;
|
||||
return this.post<AccountData | undefined>('/auth/sign-out', {}, false)
|
||||
.catch(e => console.error(e))
|
||||
.then(() => this.initialize())
|
||||
.then(() => this.router.navigate(['/']));
|
||||
}
|
||||
private openAuth(url: string) {
|
||||
url = `${host.replace(/\/$/, '')}${url}`;
|
||||
|
||||
if (isStandalone()) {
|
||||
window.open(url);
|
||||
} else {
|
||||
location.href = url;
|
||||
}
|
||||
}
|
||||
getAccount() {
|
||||
return this.post<AccountData | { limit: true; } | undefined>('/api1/account', {}, false);
|
||||
}
|
||||
getAccountCharacters() {
|
||||
return this.post<PonyObject[]>('/api/account-characters', {});
|
||||
}
|
||||
updateAccount(account: Partial<UpdateAccountData>) {
|
||||
return this.post<AccountData>('/api/account-update', { account })
|
||||
.then(a => merge(this.account, a));
|
||||
}
|
||||
saveSettings(settings: AccountSettings) {
|
||||
return this.post<AccountData>('/api/account-settings', { settings })
|
||||
.then(a => merge(this.account, a));
|
||||
}
|
||||
removeSite(siteId: string) {
|
||||
return this.post('/api/remove-site', { siteId })
|
||||
.then(() => {
|
||||
if (this.account && this.account.sites) {
|
||||
removeById(this.account.sites, siteId);
|
||||
}
|
||||
});
|
||||
}
|
||||
unhidePlayer(hideId: string) {
|
||||
return this.post('/api/remove-hide', { hideId });
|
||||
}
|
||||
verifyAccount() {
|
||||
const verificationId = this.storage.getItem('vid');
|
||||
const accountId = this.account && this.account.id || '---';
|
||||
if (isStandalone()) {
|
||||
window.open(url);
|
||||
} else {
|
||||
location.href = url;
|
||||
}
|
||||
}
|
||||
getAccount() {
|
||||
return this.post<AccountData | { limit: true; } | undefined>('/api1/account', {}, false);
|
||||
}
|
||||
getAccountCharacters() {
|
||||
return this.post<PonyObject[]>('/api/account-characters', {});
|
||||
}
|
||||
updateAccount(account: Partial<UpdateAccountData>) {
|
||||
return this.post<AccountData>('/api/account-update', { account })
|
||||
.then(a => merge(this.account, a));
|
||||
}
|
||||
saveSettings(settings: AccountSettings) {
|
||||
return this.post<AccountData>('/api/account-settings', { settings })
|
||||
.then(a => merge(this.account, a));
|
||||
}
|
||||
removeSite(siteId: string) {
|
||||
return this.post('/api/remove-site', { siteId })
|
||||
.then(() => {
|
||||
if (this.account && this.account.sites) {
|
||||
removeById(this.account.sites, siteId);
|
||||
}
|
||||
});
|
||||
}
|
||||
unhidePlayer(hideId: string) {
|
||||
return this.post('/api/remove-hide', { hideId });
|
||||
}
|
||||
verifyAccount() {
|
||||
const verificationId = this.storage.getItem('vid');
|
||||
const accountId = this.account && this.account.id || '---';
|
||||
|
||||
if (!this.loading && verificationId && accountId !== verificationId) {
|
||||
this.initialize();
|
||||
}
|
||||
}
|
||||
getHides(page: number) {
|
||||
return this.post<HiddenPlayer[]>('/api/get-hides', { page });
|
||||
}
|
||||
getFriends() {
|
||||
return this.post<FriendData[]>('/api/get-friends', {});
|
||||
}
|
||||
// ponies
|
||||
savePony(pony: PonyObject, fast = false) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
if (this.pending) {
|
||||
throw new Error('Saving in progress');
|
||||
}
|
||||
if (!this.loading && verificationId && accountId !== verificationId) {
|
||||
this.initialize();
|
||||
}
|
||||
}
|
||||
getHides(page: number) {
|
||||
return this.post<HiddenPlayer[]>('/api/get-hides', { page });
|
||||
}
|
||||
getFriends() {
|
||||
return this.post<FriendData[]>('/api/get-friends', {});
|
||||
}
|
||||
// ponies
|
||||
savePony(pony: PonyObject, fast = false) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
if (this.pending) {
|
||||
throw new Error('Saving in progress');
|
||||
}
|
||||
|
||||
pony.name = cleanName(pony.name);
|
||||
pony.desc = pony.desc && pony.desc.substr(0, PLAYER_DESC_MAX_LENGTH) || '';
|
||||
pony.name = cleanName(pony.name);
|
||||
pony.desc = pony.desc && pony.desc.substr(0, PLAYER_DESC_MAX_LENGTH) || '';
|
||||
|
||||
if (!validatePonyName(pony.name)) {
|
||||
throw new Error(NAME_ERROR);
|
||||
}
|
||||
if (!validatePonyName(pony.name)) {
|
||||
throw new Error(NAME_ERROR);
|
||||
}
|
||||
|
||||
if (pony.ponyInfo) {
|
||||
pony.info = compressPonyString(pony.ponyInfo);
|
||||
}
|
||||
if (pony.ponyInfo) {
|
||||
pony.info = compressPonyString(pony.ponyInfo);
|
||||
}
|
||||
|
||||
const { id, name, desc, site, tag, info, hideSupport, respawnAtSpawn } = pony;
|
||||
const { id, name, desc, site, tag, info, hideSupport, respawnAtSpawn } = pony;
|
||||
|
||||
if (!fast) {
|
||||
this.pending = true;
|
||||
}
|
||||
if (!fast) {
|
||||
this.pending = true;
|
||||
}
|
||||
|
||||
return this.post<PonyObject | undefined>('/api/pony/save', {
|
||||
pony: { id, name, desc, site, tag, info, hideSupport, respawnAtSpawn }
|
||||
});
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
if (e.message === CHARACTER_SAVING_ERROR) {
|
||||
this.errorReporter.reportError(e, { pony });
|
||||
}
|
||||
return this.post<PonyObject | undefined>('/api/pony/save', {
|
||||
pony: { id, name, desc, site, tag, info, hideSupport, respawnAtSpawn }
|
||||
});
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
if (e.message === CHARACTER_SAVING_ERROR) {
|
||||
this.errorReporter.reportError(e, { pony });
|
||||
}
|
||||
|
||||
throw e;
|
||||
})
|
||||
.then(newPony => {
|
||||
if (!newPony) {
|
||||
throw new Error('Failed to save pony');
|
||||
}
|
||||
throw e;
|
||||
})
|
||||
.then(newPony => {
|
||||
if (!newPony) {
|
||||
throw new Error('Failed to save pony');
|
||||
}
|
||||
|
||||
if (pony.id) {
|
||||
removeById(this.ponies, pony.id);
|
||||
} else {
|
||||
this.account!.characterCount++;
|
||||
}
|
||||
if (pony.id) {
|
||||
removeById(this.ponies, pony.id);
|
||||
} else {
|
||||
this.account!.characterCount++;
|
||||
}
|
||||
|
||||
this.ponies.push(newPony);
|
||||
this.ponies.sort(comparePonies);
|
||||
this.ponies.push(newPony);
|
||||
this.ponies.sort(comparePonies);
|
||||
|
||||
if (this.pony === pony) {
|
||||
this.selectPony(newPony);
|
||||
}
|
||||
if (this.pony === pony) {
|
||||
this.selectPony(newPony);
|
||||
}
|
||||
|
||||
return newPony;
|
||||
})
|
||||
.finally(() => this.pending = false);
|
||||
}
|
||||
removePony(pony: PonyObject) {
|
||||
return this.post('/api/pony/remove', { id: pony.id })
|
||||
.then(() => {
|
||||
removeById(this.ponies, pony.id);
|
||||
this.account!.characterCount--;
|
||||
return newPony;
|
||||
})
|
||||
.finally(() => this.pending = false);
|
||||
}
|
||||
removePony(pony: PonyObject) {
|
||||
return this.post('/api/pony/remove', { id: pony.id })
|
||||
.then(() => {
|
||||
removeById(this.ponies, pony.id);
|
||||
this.account!.characterCount--;
|
||||
|
||||
if (this.pony === pony) {
|
||||
this.selectPony(getDefaultPony(this.ponies));
|
||||
}
|
||||
});
|
||||
}
|
||||
loadPonies() {
|
||||
return this.getAccountCharacters()
|
||||
.then(ponies => {
|
||||
if (this.account) {
|
||||
this.account.ponies = ponies || [];
|
||||
this.ponies = this.account.ponies.sort(comparePonies);
|
||||
}
|
||||
});
|
||||
}
|
||||
sortPonies() {
|
||||
this.ponies.sort(comparePonies);
|
||||
}
|
||||
// game
|
||||
status(short: boolean): Promise<GameStatus> {
|
||||
let age = 6;
|
||||
if (this.pony === pony) {
|
||||
this.selectPony(getDefaultPony(this.ponies));
|
||||
}
|
||||
});
|
||||
}
|
||||
loadPonies() {
|
||||
return this.getAccountCharacters()
|
||||
.then(ponies => {
|
||||
if (this.account) {
|
||||
this.account.ponies = ponies || [];
|
||||
this.ponies = this.account.ponies.sort(comparePonies);
|
||||
}
|
||||
});
|
||||
}
|
||||
sortPonies() {
|
||||
this.ponies.sort(comparePonies);
|
||||
}
|
||||
// game
|
||||
status(short: boolean): Promise<GameStatus> {
|
||||
let age = 6;
|
||||
|
||||
if (this.account) {
|
||||
const now = new Date();
|
||||
const currentYear = now.getFullYear();
|
||||
const currentMonth = now.getMonth() + 1;
|
||||
if (this.account) {
|
||||
const now = new Date();
|
||||
const currentYear = now.getFullYear();
|
||||
const currentMonth = now.getMonth() + 1;
|
||||
|
||||
if (this.account.birthyear) {
|
||||
age = currentYear - this.account.birthyear;
|
||||
} else if (this.account.birthdate) {
|
||||
const [year, month] = this.account.birthdate.split('-');
|
||||
const before = parseInt(month, 10) > currentMonth;
|
||||
age = Math.max(0, currentYear - parseInt(year, 10) - (before ? 1 : 0));
|
||||
}
|
||||
}
|
||||
if (this.account.birthyear) {
|
||||
age = currentYear - this.account.birthyear;
|
||||
} else if (this.account.birthdate) {
|
||||
const [year, month] = this.account.birthdate.split('-');
|
||||
const before = parseInt(month, 10) > currentMonth;
|
||||
age = Math.max(0, currentYear - parseInt(year, 10) - (before ? 1 : 0));
|
||||
}
|
||||
}
|
||||
|
||||
const params = new HttpParams()
|
||||
.set('short', short.toString())
|
||||
.set('d', age.toString())
|
||||
.set('t', (Date.now() % 0x10000).toString(16));
|
||||
const params = new HttpParams()
|
||||
.set('short', short.toString())
|
||||
.set('d', age.toString())
|
||||
.set('t', (Date.now() % 0x10000).toString(16));
|
||||
|
||||
return observableToPromise(this.http.get<GameStatus>('/api2/game/status', { params }));
|
||||
}
|
||||
join(serverId: string, ponyId: string): Promise<JoinResponse> {
|
||||
if (this.pending)
|
||||
return Promise.reject(new Error('Joining in progress'));
|
||||
if (!serverId)
|
||||
return Promise.reject(new Error('Invalid server ID'));
|
||||
if (!ponyId)
|
||||
return Promise.reject(new Error('Invalid pony ID'));
|
||||
return observableToPromise(this.http.get<GameStatus>('/api2/game/status', { params }));
|
||||
}
|
||||
join(serverId: string, ponyId: string): Promise<JoinResponse> {
|
||||
if (this.pending)
|
||||
return Promise.reject(new Error('Joining in progress'));
|
||||
if (!serverId)
|
||||
return Promise.reject(new Error('Invalid server ID'));
|
||||
if (!ponyId)
|
||||
return Promise.reject(new Error('Invalid pony ID'));
|
||||
|
||||
this.pending = true;
|
||||
this.pending = true;
|
||||
|
||||
const alert = !!this.accountAlert ? 'y' : '';
|
||||
const alert = !!this.accountAlert ? 'y' : '';
|
||||
|
||||
return this.post<JoinResponse>('/api/game/join', { version, ponyId, serverId, alert, url: location.href })
|
||||
.finally(() => this.pending = false);
|
||||
}
|
||||
private post<T = void>(url: string, data: any, authenticate = true): Promise<T> {
|
||||
if (authenticate) {
|
||||
if (!this.account) {
|
||||
return Promise.reject(new Error(NOT_AUTHENTICATED_ERROR));
|
||||
}
|
||||
return this.post<JoinResponse>('/api/game/join', { version, ponyId, serverId, alert, url: location.href })
|
||||
.finally(() => this.pending = false);
|
||||
}
|
||||
private post<T = void>(url: string, data: any, authenticate = true): Promise<T> {
|
||||
if (authenticate) {
|
||||
if (!this.account) {
|
||||
return Promise.reject(new Error(NOT_AUTHENTICATED_ERROR));
|
||||
}
|
||||
|
||||
const accountId = this.account.id + this.suffix;
|
||||
const accountName = this.account.name + this.suffix;
|
||||
data = { accountId, accountName, ...data };
|
||||
}
|
||||
const accountId = this.account.id + this.suffix;
|
||||
const accountName = this.account.name + this.suffix;
|
||||
data = { accountId, accountName, ...data };
|
||||
}
|
||||
|
||||
const params = new HttpParams()
|
||||
.set('t', (Date.now() % 0x10000).toString(16));
|
||||
const headers = new HttpHeaders({ 'api-version': HASH, 'api-bid': this.storage.getItem('bid') || '-' });
|
||||
const params = new HttpParams()
|
||||
.set('t', (Date.now() % 0x10000).toString(16));
|
||||
const headers = new HttpHeaders({ 'api-version': HASH, 'api-bid': this.storage.getItem('bid') || '-' });
|
||||
|
||||
return observableToPromise(this.http.post<T>(url, data, { params, headers }));
|
||||
}
|
||||
return observableToPromise(this.http.post<T>(url, data, { params, headers }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,99 +9,99 @@ import { MINUTE } from '../../common/constants';
|
||||
type OnModel<T> = (item: T | undefined) => void;
|
||||
|
||||
interface ModelSubscriberConfig<T> {
|
||||
fix?: (item: T) => void;
|
||||
fix?: (item: T) => void;
|
||||
}
|
||||
|
||||
interface ModelSubscription<T> {
|
||||
value: T | undefined;
|
||||
timeout: any;
|
||||
callbacks: OnModel<T>[];
|
||||
value: T | undefined;
|
||||
timeout: any;
|
||||
callbacks: OnModel<T>[];
|
||||
}
|
||||
|
||||
const unsubscribeTimeout = 1 * MINUTE;
|
||||
|
||||
export class ModelSubscriber<T> {
|
||||
private subscriptions = new Map<string, ModelSubscription<T>>();
|
||||
// private observables = new Map<string, ModelSubscription<T>>();
|
||||
constructor(
|
||||
private type: ModelTypes,
|
||||
private socket: SocketService<ClientAdminActions, IAdminServerActions>,
|
||||
private config: ModelSubscriberConfig<T> = {},
|
||||
private defaultValue: T | undefined = undefined,
|
||||
) {
|
||||
}
|
||||
// for(id: string) {
|
||||
// return this.createObservable(id);
|
||||
// }
|
||||
// private createObservable(id: string) {
|
||||
// return new Observable<T | undefined>(observer => {
|
||||
// this.socket.server.subscribe(this.model, id);
|
||||
private subscriptions = new Map<string, ModelSubscription<T>>();
|
||||
// private observables = new Map<string, ModelSubscription<T>>();
|
||||
constructor(
|
||||
private type: ModelTypes,
|
||||
private socket: SocketService<ClientAdminActions, IAdminServerActions>,
|
||||
private config: ModelSubscriberConfig<T> = {},
|
||||
private defaultValue: T | undefined = undefined,
|
||||
) {
|
||||
}
|
||||
// for(id: string) {
|
||||
// return this.createObservable(id);
|
||||
// }
|
||||
// private createObservable(id: string) {
|
||||
// return new Observable<T | undefined>(observer => {
|
||||
// this.socket.server.subscribe(this.model, id);
|
||||
|
||||
// return () => {
|
||||
// this.socket.server.unsubscribe(this.model, id);
|
||||
// };
|
||||
// });
|
||||
// }
|
||||
get(id: string) {
|
||||
const subscription = this.subscriptions.get(id);
|
||||
return subscription && subscription.value;
|
||||
}
|
||||
subscribe(id: string, callback: OnModel<T>): Subscription {
|
||||
const subscription = this.subscriptions.get(id);
|
||||
// return () => {
|
||||
// this.socket.server.unsubscribe(this.model, id);
|
||||
// };
|
||||
// });
|
||||
// }
|
||||
get(id: string) {
|
||||
const subscription = this.subscriptions.get(id);
|
||||
return subscription && subscription.value;
|
||||
}
|
||||
subscribe(id: string, callback: OnModel<T>): Subscription {
|
||||
const subscription = this.subscriptions.get(id);
|
||||
|
||||
if (subscription) {
|
||||
if (subscription.timeout) {
|
||||
clearTimeout(subscription.timeout);
|
||||
subscription.timeout = 0;
|
||||
}
|
||||
if (subscription) {
|
||||
if (subscription.timeout) {
|
||||
clearTimeout(subscription.timeout);
|
||||
subscription.timeout = 0;
|
||||
}
|
||||
|
||||
subscription.callbacks.push(callback);
|
||||
subscription.callbacks.push(callback);
|
||||
|
||||
if (subscription.value !== undefined) {
|
||||
callback(subscription.value);
|
||||
}
|
||||
} else {
|
||||
this.socket.server.subscribe(this.type, id);
|
||||
this.subscriptions.set(id, {
|
||||
value: this.defaultValue,
|
||||
timeout: 0,
|
||||
callbacks: [callback],
|
||||
});
|
||||
}
|
||||
if (subscription.value !== undefined) {
|
||||
callback(subscription.value);
|
||||
}
|
||||
} else {
|
||||
this.socket.server.subscribe(this.type, id);
|
||||
this.subscriptions.set(id, {
|
||||
value: this.defaultValue,
|
||||
timeout: 0,
|
||||
callbacks: [callback],
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
unsubscribe: () => this.unsubscribe(id, callback),
|
||||
};
|
||||
}
|
||||
unsubscribe(id: string, callback: OnModel<T>) {
|
||||
const subscription = this.subscriptions.get(id);
|
||||
return {
|
||||
unsubscribe: () => this.unsubscribe(id, callback),
|
||||
};
|
||||
}
|
||||
unsubscribe(id: string, callback: OnModel<T>) {
|
||||
const subscription = this.subscriptions.get(id);
|
||||
|
||||
if (subscription) {
|
||||
removeItem(subscription.callbacks, callback);
|
||||
if (subscription) {
|
||||
removeItem(subscription.callbacks, callback);
|
||||
|
||||
if (subscription.callbacks.length === 0) {
|
||||
subscription.timeout = setTimeout(() => {
|
||||
this.socket.server.unsubscribe(this.type, id);
|
||||
this.subscriptions.delete(id);
|
||||
}, unsubscribeTimeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
update(id: string, update: T) {
|
||||
const subscription = this.subscriptions.get(id);
|
||||
if (subscription.callbacks.length === 0) {
|
||||
subscription.timeout = setTimeout(() => {
|
||||
this.socket.server.unsubscribe(this.type, id);
|
||||
this.subscriptions.delete(id);
|
||||
}, unsubscribeTimeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
update(id: string, update: T) {
|
||||
const subscription = this.subscriptions.get(id);
|
||||
|
||||
if (update !== undefined && this.config.fix) {
|
||||
this.config.fix(update);
|
||||
}
|
||||
if (update !== undefined && this.config.fix) {
|
||||
this.config.fix(update);
|
||||
}
|
||||
|
||||
if (subscription) {
|
||||
subscription.value = update;
|
||||
subscription.callbacks.forEach(c => c(update));
|
||||
}
|
||||
}
|
||||
connected() {
|
||||
this.subscriptions.forEach((_, id) => {
|
||||
this.socket.server.subscribe(this.type, id);
|
||||
});
|
||||
}
|
||||
if (subscription) {
|
||||
subscription.value = update;
|
||||
subscription.callbacks.forEach(c => c(update));
|
||||
}
|
||||
}
|
||||
connected() {
|
||||
this.subscriptions.forEach((_, id) => {
|
||||
this.socket.server.subscribe(this.type, id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,54 +8,54 @@ import { rollbarCheckIgnore, isIgnoredError } from '../../common/rollbar';
|
||||
const host = typeof location === 'undefined' ? '' : location.host;
|
||||
|
||||
const rollbarConfig = {
|
||||
environment: ROLLBAR_ENV,
|
||||
accessToken: ROLLBAR_TOKEN,
|
||||
ignoredMessages: ['disconnected'],
|
||||
hostWhiteList: [host],
|
||||
captureUncaught: true,
|
||||
captureUnhandleRejections: true,
|
||||
// checkIgnore,
|
||||
enabled: true,
|
||||
payload: {
|
||||
environment: ROLLBAR_ENV,
|
||||
version: version, // NOTE: workaround for compilation issue
|
||||
client: {
|
||||
javascript: {
|
||||
source_map_enabled: true,
|
||||
guess_uncaught_frames: true,
|
||||
code_version: HASH,
|
||||
},
|
||||
},
|
||||
},
|
||||
environment: ROLLBAR_ENV,
|
||||
accessToken: ROLLBAR_TOKEN,
|
||||
ignoredMessages: ['disconnected'],
|
||||
hostWhiteList: [host],
|
||||
captureUncaught: true,
|
||||
captureUnhandleRejections: true,
|
||||
// checkIgnore,
|
||||
enabled: true,
|
||||
payload: {
|
||||
environment: ROLLBAR_ENV,
|
||||
version: version, // NOTE: workaround for compilation issue
|
||||
client: {
|
||||
javascript: {
|
||||
source_map_enabled: true,
|
||||
guess_uncaught_frames: true,
|
||||
code_version: HASH,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const RollbarService = new InjectionToken<Rollbar>('rollbar');
|
||||
|
||||
export function rollbarFactory() {
|
||||
if (DEVELOPMENT) {
|
||||
return undefined;
|
||||
} else {
|
||||
const rollbar = Rollbar.init(rollbarConfig);
|
||||
rollbar.configure({ checkIgnore: rollbarCheckIgnore });
|
||||
return rollbar;
|
||||
}
|
||||
if (DEVELOPMENT) {
|
||||
return undefined;
|
||||
} else {
|
||||
const rollbar = Rollbar.init(rollbarConfig);
|
||||
rollbar.configure({ checkIgnore: rollbarCheckIgnore });
|
||||
return rollbar;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class RollbarErrorHandler extends ErrorHandler {
|
||||
constructor(private injector: Injector) {
|
||||
super();
|
||||
}
|
||||
handleError(error: any) {
|
||||
super.handleError(error);
|
||||
constructor(private injector: Injector) {
|
||||
super();
|
||||
}
|
||||
handleError(error: any) {
|
||||
super.handleError(error);
|
||||
|
||||
if (!DEVELOPMENT && rollbarConfig.accessToken) {
|
||||
const rollbar = this.injector.get(RollbarService);
|
||||
const err = error.originalError || error || {};
|
||||
if (!DEVELOPMENT && rollbarConfig.accessToken) {
|
||||
const rollbar = this.injector.get(RollbarService);
|
||||
const err = error.originalError || error || {};
|
||||
|
||||
if (!isIgnoredError(err)) {
|
||||
rollbar.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isIgnoredError(err)) {
|
||||
rollbar.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,34 +6,34 @@ import { ErrorReporter } from './errorReporter';
|
||||
|
||||
@Injectable()
|
||||
export class RollbarErrorReporter extends ErrorReporter {
|
||||
constructor(@Inject(RollbarService) private rollbar?: Rollbar) {
|
||||
super();
|
||||
}
|
||||
configureUser(person: Person) {
|
||||
if (this.rollbar) {
|
||||
this.rollbar.configure({ payload: { person }, checkIgnore: rollbarCheckIgnore });
|
||||
}
|
||||
}
|
||||
configureData(data: any) {
|
||||
if (this.rollbar) {
|
||||
this.rollbar.configure({ payload: data, checkIgnore: rollbarCheckIgnore });
|
||||
}
|
||||
}
|
||||
captureEvent(data: any) {
|
||||
if (this.rollbar) {
|
||||
this.rollbar.captureEvent(data, 'info');
|
||||
}
|
||||
}
|
||||
reportError(error: any, data?: any) {
|
||||
DEVELOPMENT && console.error(error, data);
|
||||
constructor(@Inject(RollbarService) private rollbar?: Rollbar) {
|
||||
super();
|
||||
}
|
||||
configureUser(person: Person) {
|
||||
if (this.rollbar) {
|
||||
this.rollbar.configure({ payload: { person }, checkIgnore: rollbarCheckIgnore });
|
||||
}
|
||||
}
|
||||
configureData(data: any) {
|
||||
if (this.rollbar) {
|
||||
this.rollbar.configure({ payload: data, checkIgnore: rollbarCheckIgnore });
|
||||
}
|
||||
}
|
||||
captureEvent(data: any) {
|
||||
if (this.rollbar) {
|
||||
this.rollbar.captureEvent(data, 'info');
|
||||
}
|
||||
}
|
||||
reportError(error: any, data?: any) {
|
||||
DEVELOPMENT && console.error(error, data);
|
||||
|
||||
if (this.rollbar && !isIgnoredError(error)) {
|
||||
this.rollbar.error(error, data);
|
||||
}
|
||||
}
|
||||
disable() {
|
||||
if (this.rollbar) {
|
||||
this.rollbar.configure({ enabled: false });
|
||||
}
|
||||
}
|
||||
if (this.rollbar && !isIgnoredError(error)) {
|
||||
this.rollbar.error(error, data);
|
||||
}
|
||||
}
|
||||
disable() {
|
||||
if (this.rollbar) {
|
||||
this.rollbar.configure({ enabled: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,39 +5,39 @@ import { Model } from './model';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SettingsService {
|
||||
browser: BrowserSettings;
|
||||
private save: (settings: AccountSettings) => boolean = () => false;
|
||||
constructor(private storage: StorageService, private model: Model) {
|
||||
this.browser = this.storage.getJSON('browser-settings', {});
|
||||
}
|
||||
get account(): AccountSettings {
|
||||
return this.model.account ? this.model.account.settings : {};
|
||||
}
|
||||
set account(value) {
|
||||
if (this.model.account) {
|
||||
this.model.account.settings = value;
|
||||
}
|
||||
}
|
||||
saving(save: (settings: AccountSettings) => boolean) {
|
||||
this.save = save;
|
||||
}
|
||||
saveAccountSettings(settings: AccountSettings) {
|
||||
if (this.model.account) {
|
||||
this.model.account.settings = settings;
|
||||
}
|
||||
browser: BrowserSettings;
|
||||
private save: (settings: AccountSettings) => boolean = () => false;
|
||||
constructor(private storage: StorageService, private model: Model) {
|
||||
this.browser = this.storage.getJSON('browser-settings', {});
|
||||
}
|
||||
get account(): AccountSettings {
|
||||
return this.model.account ? this.model.account.settings : {};
|
||||
}
|
||||
set account(value) {
|
||||
if (this.model.account) {
|
||||
this.model.account.settings = value;
|
||||
}
|
||||
}
|
||||
saving(save: (settings: AccountSettings) => boolean) {
|
||||
this.save = save;
|
||||
}
|
||||
saveAccountSettings(settings: AccountSettings) {
|
||||
if (this.model.account) {
|
||||
this.model.account.settings = settings;
|
||||
}
|
||||
|
||||
if (settings.filterWords) {
|
||||
settings.filterWords = settings.filterWords.trim();
|
||||
}
|
||||
if (settings.filterWords) {
|
||||
settings.filterWords = settings.filterWords.trim();
|
||||
}
|
||||
|
||||
if (this.save(settings)) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return this.model.saveSettings(settings);
|
||||
}
|
||||
}
|
||||
saveBrowserSettings(settings?: BrowserSettings) {
|
||||
this.browser = settings || this.browser;
|
||||
this.storage.setJSON('browser-settings', this.browser);
|
||||
}
|
||||
if (this.save(settings)) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return this.model.saveSettings(settings);
|
||||
}
|
||||
}
|
||||
saveBrowserSettings(settings?: BrowserSettings) {
|
||||
this.browser = settings || this.browser;
|
||||
this.storage.setJSON('browser-settings', this.browser);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,82 +3,82 @@ import { Injectable } from '@angular/core';
|
||||
/* istanbul ignore next */
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class StorageService {
|
||||
private data?: Map<string, string> = undefined;
|
||||
constructor() {
|
||||
try {
|
||||
if (typeof localStorage === 'undefined') {
|
||||
this.data = new Map();
|
||||
}
|
||||
} catch {
|
||||
this.data = new Map();
|
||||
}
|
||||
}
|
||||
getItem(key: string) {
|
||||
if (this.data) {
|
||||
return this.data.get(key);
|
||||
} else {
|
||||
try {
|
||||
const value = localStorage.getItem(key);
|
||||
return value == null ? undefined : value;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
setItem(key: string, data: string) {
|
||||
try {
|
||||
localStorage.setItem(key, data);
|
||||
this.data = undefined;
|
||||
} catch {
|
||||
if (!this.data) {
|
||||
this.data = new Map();
|
||||
}
|
||||
private data?: Map<string, string> = undefined;
|
||||
constructor() {
|
||||
try {
|
||||
if (typeof localStorage === 'undefined') {
|
||||
this.data = new Map();
|
||||
}
|
||||
} catch {
|
||||
this.data = new Map();
|
||||
}
|
||||
}
|
||||
getItem(key: string) {
|
||||
if (this.data) {
|
||||
return this.data.get(key);
|
||||
} else {
|
||||
try {
|
||||
const value = localStorage.getItem(key);
|
||||
return value == null ? undefined : value;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
setItem(key: string, data: string) {
|
||||
try {
|
||||
localStorage.setItem(key, data);
|
||||
this.data = undefined;
|
||||
} catch {
|
||||
if (!this.data) {
|
||||
this.data = new Map();
|
||||
}
|
||||
|
||||
this.data.set(key, data);
|
||||
}
|
||||
}
|
||||
removeItem(key: string) {
|
||||
if (this.data) {
|
||||
this.data.delete(key);
|
||||
} else {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
clear() {
|
||||
if (this.data) {
|
||||
this.data.clear();
|
||||
} else {
|
||||
try {
|
||||
localStorage.clear();
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
getJSON<T>(key: string, defaultValue: T): T {
|
||||
try {
|
||||
return JSON.parse(this.getItem(key) || '');
|
||||
} catch {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
setJSON(key: string, value: any) {
|
||||
this.setItem(key, JSON.stringify(value));
|
||||
}
|
||||
getInt(key: string) {
|
||||
return parseInt(this.getItem(key) || '0', 10) | 0;
|
||||
}
|
||||
setInt(key: string, value: number) {
|
||||
this.setItem(key, value.toString(10));
|
||||
}
|
||||
getBoolean(key: string) {
|
||||
return this.getItem(key) === 'true';
|
||||
}
|
||||
setBoolean(key: string, value: boolean) {
|
||||
if (value) {
|
||||
this.setItem(key, 'true');
|
||||
} else {
|
||||
this.removeItem(key);
|
||||
}
|
||||
}
|
||||
this.data.set(key, data);
|
||||
}
|
||||
}
|
||||
removeItem(key: string) {
|
||||
if (this.data) {
|
||||
this.data.delete(key);
|
||||
} else {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
clear() {
|
||||
if (this.data) {
|
||||
this.data.clear();
|
||||
} else {
|
||||
try {
|
||||
localStorage.clear();
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
getJSON<T>(key: string, defaultValue: T): T {
|
||||
try {
|
||||
return JSON.parse(this.getItem(key) || '');
|
||||
} catch {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
setJSON(key: string, value: any) {
|
||||
this.setItem(key, JSON.stringify(value));
|
||||
}
|
||||
getInt(key: string) {
|
||||
return parseInt(this.getItem(key) || '0', 10) | 0;
|
||||
}
|
||||
setInt(key: string, value: number) {
|
||||
this.setItem(key, value.toString(10));
|
||||
}
|
||||
getBoolean(key: string) {
|
||||
return this.getItem(key) === 'true';
|
||||
}
|
||||
setBoolean(key: string, value: boolean) {
|
||||
if (value) {
|
||||
this.setItem(key, 'true');
|
||||
} else {
|
||||
this.removeItem(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user