tslint changes

This commit is contained in:
Jeremy Simpson
2019-08-30 01:52:40 -07:00
parent 7603879d35
commit 7f7efbb94b
446 changed files with 70650 additions and 70661 deletions
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 {
}
+100 -100
View File
@@ -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);
}
}
+75 -75
View File
@@ -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 {
}
+56 -56
View File
@@ -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 = '/';
}
}
+92 -92
View File
@@ -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;
}
}
}
+4 -4
View File
@@ -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;
}
}
+4 -4
View File
@@ -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 });
}
}
+27 -27
View File
@@ -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;
}
}
+16 -16
View File
@@ -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;
}
+115 -115
View File
@@ -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));
}
}
+34 -34
View File
@@ -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
View File
@@ -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');
});
}
}
+426 -426
View File
@@ -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();
}
+190 -190
View File
@@ -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];
}
}
+10 -10
View File
@@ -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;
}
+45 -45
View File
@@ -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
View File
@@ -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);
}
}
}
}
+14 -14
View File
@@ -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;
}
}
+33 -33
View File
@@ -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 };
}
}
+27 -27
View File
@@ -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;
}
};
}
}
+257 -257
View File
@@ -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);
});
}
}
+29 -29
View File
@@ -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;
}
};
}
}
+119 -119
View File
@@ -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
View File
@@ -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 }));
}
}
+78 -78
View File
@@ -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 });
}
}
}
+33 -33
View File
@@ -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);
}
}
+77 -77
View File
@@ -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);
}
}
+309 -309
View File
@@ -18,8 +18,8 @@ const chatTypeNames: string[] = [];
const chatTypeClasses: string[] = [];
function setupChatType(type: ChatType, name: string) {
chatTypeNames[type] = name;
chatTypeClasses[type] = `chat-${name.replace(/ /, '-')}`;
chatTypeNames[type] = name;
chatTypeClasses[type] = `chat-${name.replace(/ /, '-')}`;
}
setupChatType(ChatType.Say, 'say');
@@ -33,356 +33,356 @@ setupChatType(ChatType.Think, 'think');
setupChatType(ChatType.PartyThink, 'party think');
function isActionCommand(message: string) {
return /^\/(yawn|sneeze|achoo|laugh|lol|haha|хаха|jaja)/i.test(message);
return /^\/(yawn|sneeze|achoo|laugh|lol|haha|хаха|jaja)/i.test(message);
}
@Component({
selector: 'chat-box',
templateUrl: 'chat-box.pug',
styleUrls: ['chat-box.scss'],
selector: 'chat-box',
templateUrl: 'chat-box.pug',
styleUrls: ['chat-box.scss'],
})
export class ChatBox implements AfterViewInit, OnDestroy {
readonly maxSayLength = SAY_MAX_LENGTH;
readonly commentIcon = faComment;
readonly sendIcon = faAngleDoubleRight;
@ViewChild('inputElement', { static: true }) inputElement!: ElementRef;
@ViewChild('typeBox', { static: true }) typeBox!: ElementRef;
@ViewChild('typePrefix', { static: true }) typePrefix!: ElementRef;
@ViewChild('typeName', { static: true }) typeName!: ElementRef;
@ViewChild('chatBox', { static: true }) chatBox!: ElementRef;
@ViewChild('chatBoxInput', { static: true }) chatBoxInput!: ElementRef;
isOpen = false;
message: string | undefined = '';
chatType = ChatType.Say;
private pasted = false;
private lastMessages: string[] = [];
private state: AutocompleteState = {};
private subscriptions: Subscription[];
private _disabled = false;
constructor(private game: PonyTownGame, zone: NgZone) {
this.subscriptions = [
this.game.onChat.subscribe(() => zone.run(() => this.chat(undefined))),
this.game.onToggleChat.subscribe(() => zone.run(() => this.toggle())),
this.game.onCommand.subscribe(() => zone.run(() => this.command())),
this.game.onLeft.subscribe(() => {
this.chatType = ChatType.Say;
this.close();
}),
];
readonly maxSayLength = SAY_MAX_LENGTH;
readonly commentIcon = faComment;
readonly sendIcon = faAngleDoubleRight;
@ViewChild('inputElement', { static: true }) inputElement!: ElementRef;
@ViewChild('typeBox', { static: true }) typeBox!: ElementRef;
@ViewChild('typePrefix', { static: true }) typePrefix!: ElementRef;
@ViewChild('typeName', { static: true }) typeName!: ElementRef;
@ViewChild('chatBox', { static: true }) chatBox!: ElementRef;
@ViewChild('chatBoxInput', { static: true }) chatBoxInput!: ElementRef;
isOpen = false;
message: string | undefined = '';
chatType = ChatType.Say;
private pasted = false;
private lastMessages: string[] = [];
private state: AutocompleteState = {};
private subscriptions: Subscription[];
private _disabled = false;
constructor(private game: PonyTownGame, zone: NgZone) {
this.subscriptions = [
this.game.onChat.subscribe(() => zone.run(() => this.chat(undefined))),
this.game.onToggleChat.subscribe(() => zone.run(() => this.toggle())),
this.game.onCommand.subscribe(() => zone.run(() => this.command())),
this.game.onLeft.subscribe(() => {
this.chatType = ChatType.Say;
this.close();
}),
];
this.game.onCancel = () => this.isOpen ? (zone.run(() => this.close()), true) : false;
}
@Input() get disabled() {
return this._disabled;
}
set disabled(value) {
this._disabled = value;
this.game.onCancel = () => this.isOpen ? (zone.run(() => this.close()), true) : false;
}
@Input() get disabled() {
return this._disabled;
}
set disabled(value) {
this._disabled = value;
if (value) {
this.close();
}
}
get input() {
return this.inputElement.nativeElement as HTMLInputElement;
}
ngAfterViewInit() {
this.chatBox.nativeElement.hidden = true;
this.input.addEventListener('paste', () => this.pasted = true);
}
ngOnDestroy() {
this.subscriptions.forEach(s => s.unsubscribe());
}
send(_event: Event | undefined) {
let chatType = this.chatType;
let message = replaceEmojis(cleanMessage(this.message || '')).substr(0, SAY_MAX_LENGTH);
const handled = handleActionCommand(message, this.game);
const spam = this.pasted && chatType !== ChatType.Party && isSpamMessage(message, this.lastMessages);
const empty = !this.game.player || !message;
const ignoreAction = isActionCommand(message) && this.game.player && hasHeadAnimation(this.game.player);
const whisperTo = this.game.whisperTo;
let entityId = whisperTo && whisperTo.id || 0;
if (value) {
this.close();
}
}
get input() {
return this.inputElement.nativeElement as HTMLInputElement;
}
ngAfterViewInit() {
this.chatBox.nativeElement.hidden = true;
this.input.addEventListener('paste', () => this.pasted = true);
}
ngOnDestroy() {
this.subscriptions.forEach(s => s.unsubscribe());
}
send(_event: Event | undefined) {
let chatType = this.chatType;
let message = replaceEmojis(cleanMessage(this.message || '')).substr(0, SAY_MAX_LENGTH);
const handled = handleActionCommand(message, this.game);
const spam = this.pasted && chatType !== ChatType.Party && isSpamMessage(message, this.lastMessages);
const empty = !this.game.player || !message;
const ignoreAction = isActionCommand(message) && this.game.player && hasHeadAnimation(this.game.player);
const whisperTo = this.game.whisperTo;
let entityId = whisperTo && whisperTo.id || 0;
if (/^\/(w|whisper) .+$/i.test(message)) {
chatType = ChatType.Whisper;
message = message.substr(/^\/w /i.test(message) ? 3 : 9);
if (/^\/(w|whisper) .+$/i.test(message)) {
chatType = ChatType.Whisper;
message = message.substr(/^\/w /i.test(message) ? 3 : 9);
let offset = 0;
let entity: Entity | FakeEntity | undefined = undefined;
let offset = 0;
let entity: Entity | FakeEntity | undefined = undefined;
do {
offset = message.indexOf(' ', offset);
do {
offset = message.indexOf(' ', offset);
if (offset === -1)
break;
if (offset === -1)
break;
const name = message.substr(0, offset);
entity = findBestEntityByName(this.game, name);
offset++;
} while (!entity);
const name = message.substr(0, offset);
entity = findBestEntityByName(this.game, name);
offset++;
} while (!entity);
if (entity) {
message = message.substr(offset);
entityId = entity.id;
} else {
entityId = 0;
}
}
if (entity) {
message = message.substr(offset);
entityId = entity.id;
} else {
entityId = 0;
}
}
if (handled || spam || empty || ignoreAction || this.say(message, chatType, entityId)) {
if (message) {
this.lastMessages.push(message);
if (handled || spam || empty || ignoreAction || this.say(message, chatType, entityId)) {
if (message) {
this.lastMessages.push(message);
while (this.lastMessages.length > 5) {
this.lastMessages.shift();
}
}
while (this.lastMessages.length > 5) {
this.lastMessages.shift();
}
}
this.close();
}
}
keydown(e: KeyboardEvent) {
if (e.keyCode !== Key.TAB && e.keyCode !== Key.SHIFT) {
this.state.lastEmoji = undefined;
}
this.close();
}
}
keydown(e: KeyboardEvent) {
if (e.keyCode !== Key.TAB && e.keyCode !== Key.SHIFT) {
this.state.lastEmoji = undefined;
}
if (e.keyCode === Key.TAB) {
if (this.message) {
if (/^\/(w|whisper) .+$/i.test(this.message)) {
const space = this.message.indexOf(' ');
const names = findMatchingEntityNames(this.game, this.message.substr(space + 1));
if (e.keyCode === Key.TAB) {
if (this.message) {
if (/^\/(w|whisper) .+$/i.test(this.message)) {
const space = this.message.indexOf(' ');
const names = findMatchingEntityNames(this.game, this.message.substr(space + 1));
if (names.length === 1) {
this.message = `${this.message.substring(0, space)} ${names[0]}`;
}
} else {
this.message = autocompleteMesssage(this.message, e.shiftKey, this.state);
}
}
if (names.length === 1) {
this.message = `${this.message.substring(0, space)} ${names[0]}`;
}
} else {
this.message = autocompleteMesssage(this.message, e.shiftKey, this.state);
}
}
e.preventDefault();
} else if (e.keyCode === Key.ENTER && this.isOpen) {
this.send(e);
} else if (e.keyCode === Key.ESCAPE) {
this.close();
e.preventDefault();
} else if (e.keyCode === Key.SPACE) {
if (!this.message)
return;
e.preventDefault();
} else if (e.keyCode === Key.ENTER && this.isOpen) {
this.send(e);
} else if (e.keyCode === Key.ESCAPE) {
this.close();
e.preventDefault();
} else if (e.keyCode === Key.SPACE) {
if (!this.message)
return;
const isParty = /^\/(p|party)$/i.test(this.message);
const isSay = /^\/(s|say)$/i.test(this.message);
const isSup = /^\/(ss)$/i.test(this.message);
const isSup1 = /^\/(s1)$/i.test(this.message);
const isSup2 = /^\/(s2)$/i.test(this.message);
const isSup3 = /^\/(s3)$/i.test(this.message);
const isParty = /^\/(p|party)$/i.test(this.message);
const isSay = /^\/(s|say)$/i.test(this.message);
const isSup = /^\/(ss)$/i.test(this.message);
const isSup1 = /^\/(s1)$/i.test(this.message);
const isSup2 = /^\/(s2)$/i.test(this.message);
const isSup3 = /^\/(s3)$/i.test(this.message);
const supporter = this.game.model.supporter;
const isSayOrInvalid = isSay
|| (isParty && !isInParty(this.game))
|| (isSup && supporter === 0)
|| (isSup1 && supporter < 1)
|| (isSup2 && supporter < 2)
|| (isSup3 && supporter < 3);
const supporter = this.game.model.supporter;
const isSayOrInvalid = isSay
|| (isParty && !isInParty(this.game))
|| (isSup && supporter === 0)
|| (isSup1 && supporter < 1)
|| (isSup2 && supporter < 2)
|| (isSup3 && supporter < 3);
if (isSayOrInvalid) {
this.changeChatType(e, ChatType.Say);
} else if (isParty) {
this.changeChatType(e, ChatType.Party);
} else if (isSup) {
this.changeChatType(e, ChatType.Supporter);
} else if (isSup1) {
this.changeChatType(e, ChatType.Supporter1);
} else if (isSup2) {
this.changeChatType(e, ChatType.Supporter2);
} else if (isSup3) {
this.changeChatType(e, ChatType.Supporter3);
} else if (/^\/(t|think)$/i.test(this.message)) {
if (isPartyChat(this.chatType)) {
this.changeChatType(e, ChatType.PartyThink);
} else {
this.changeChatType(e, ChatType.Think);
}
} else if (/^\/(r|reply)$/i.test(this.message)) {
const lastWhisperFrom = this.game.lastWhisperFrom;
const entity = lastWhisperFrom && findEntityOrMockByAnyMeans(this.game, lastWhisperFrom.entityId);
if (isSayOrInvalid) {
this.changeChatType(e, ChatType.Say);
} else if (isParty) {
this.changeChatType(e, ChatType.Party);
} else if (isSup) {
this.changeChatType(e, ChatType.Supporter);
} else if (isSup1) {
this.changeChatType(e, ChatType.Supporter1);
} else if (isSup2) {
this.changeChatType(e, ChatType.Supporter2);
} else if (isSup3) {
this.changeChatType(e, ChatType.Supporter3);
} else if (/^\/(t|think)$/i.test(this.message)) {
if (isPartyChat(this.chatType)) {
this.changeChatType(e, ChatType.PartyThink);
} else {
this.changeChatType(e, ChatType.Think);
}
} else if (/^\/(r|reply)$/i.test(this.message)) {
const lastWhisperFrom = this.game.lastWhisperFrom;
const entity = lastWhisperFrom && findEntityOrMockByAnyMeans(this.game, lastWhisperFrom.entityId);
if (entity) {
this.game.whisperTo = entity;
this.changeChatType(e, ChatType.Whisper);
} else {
this.changeChatType(e, ChatType.Say);
}
} else if (/^\/(w|whisper) .+$/i.test(this.message) && !e.shiftKey) {
const name = this.message.substr(/^\/w /i.test(this.message) ? 3 : 9);
const entity = findBestEntityByName(this.game, name);
if (entity) {
this.game.whisperTo = entity;
this.changeChatType(e, ChatType.Whisper);
} else {
this.changeChatType(e, ChatType.Say);
}
} else if (/^\/(w|whisper) .+$/i.test(this.message) && !e.shiftKey) {
const name = this.message.substr(/^\/w /i.test(this.message) ? 3 : 9);
const entity = findBestEntityByName(this.game, name);
if (entity) {
this.game.whisperTo = entity;
this.changeChatType(e, ChatType.Whisper);
}
}
}
}
private say(message: string, chatType: ChatType, entityId: number): boolean {
this.game.lastChatMessageType = chatType;
return !!this.game.send(server => server.say(entityId, message, chatType));
}
private changeChatType(e: KeyboardEvent, chatType: ChatType) {
this.chatType = chatType;
this.message = '';
this.updateChatType();
e.preventDefault();
}
private chat(event: Event | undefined) {
if (this.isOpen) {
this.send(event);
} else {
this.open();
}
}
private command() {
if (!this.isOpen) {
this.chat(undefined);
this.message = '/';
this.input.selectionStart = this.input.selectionEnd = 10000;
}
}
private open() {
if (!this.isOpen) {
this.isOpen = true;
this.chatBox.nativeElement.hidden = false;
}
if (entity) {
this.game.whisperTo = entity;
this.changeChatType(e, ChatType.Whisper);
}
}
}
}
private say(message: string, chatType: ChatType, entityId: number): boolean {
this.game.lastChatMessageType = chatType;
return !!this.game.send(server => server.say(entityId, message, chatType));
}
private changeChatType(e: KeyboardEvent, chatType: ChatType) {
this.chatType = chatType;
this.message = '';
this.updateChatType();
e.preventDefault();
}
private chat(event: Event | undefined) {
if (this.isOpen) {
this.send(event);
} else {
this.open();
}
}
private command() {
if (!this.isOpen) {
this.chat(undefined);
this.message = '/';
this.input.selectionStart = this.input.selectionEnd = 10000;
}
}
private open() {
if (!this.isOpen) {
this.isOpen = true;
this.chatBox.nativeElement.hidden = false;
}
this.chatType = isValidChatType(this.chatType, this.game) ? this.chatType : ChatType.Say;
this.updateChatType();
this.input.focus();
}
private close() {
if (this.isOpen) {
this.input.blur();
this.isOpen = false;
this.chatBox.nativeElement.hidden = true;
this.message = '';
this.pasted = false;
}
}
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
toggleChatType() {
const chatTypes = getChatTypes(this.game);
this.chatType = chatTypes[(chatTypes.indexOf(this.chatType) + 1) % chatTypes.length];
this.updateChatType();
this.input.focus();
}
setChatType(type: 'say' | 'party' | 'whisper') {
if (type === 'say') {
this.chatType = ChatType.Say;
this.open();
} else if (type === 'party' && isInParty(this.game)) {
this.chatType = ChatType.Party;
this.open();
} else if (type === 'whisper') {
this.chatType = ChatType.Whisper;
this.open();
}
}
private currentTypeClass = '';
private currentTypePrefix = '';
private currentTypeName = '';
private updateChatType() {
let typeName: string;
let typePrefix: string;
let changed = false;
this.chatType = isValidChatType(this.chatType, this.game) ? this.chatType : ChatType.Say;
this.updateChatType();
this.input.focus();
}
private close() {
if (this.isOpen) {
this.input.blur();
this.isOpen = false;
this.chatBox.nativeElement.hidden = true;
this.message = '';
this.pasted = false;
}
}
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
toggleChatType() {
const chatTypes = getChatTypes(this.game);
this.chatType = chatTypes[(chatTypes.indexOf(this.chatType) + 1) % chatTypes.length];
this.updateChatType();
this.input.focus();
}
setChatType(type: 'say' | 'party' | 'whisper') {
if (type === 'say') {
this.chatType = ChatType.Say;
this.open();
} else if (type === 'party' && isInParty(this.game)) {
this.chatType = ChatType.Party;
this.open();
} else if (type === 'whisper') {
this.chatType = ChatType.Whisper;
this.open();
}
}
private currentTypeClass = '';
private currentTypePrefix = '';
private currentTypeName = '';
private updateChatType() {
let typeName: string;
let typePrefix: string;
let changed = false;
const typeClass = chatTypeClass(this.chatType, this.game.model.supporter);
const typeClass = chatTypeClass(this.chatType, this.game.model.supporter);
if (this.currentTypeClass !== typeClass) {
this.currentTypeClass = typeClass;
(this.chatBoxInput.nativeElement as HTMLElement).className = typeClass;
}
if (this.currentTypeClass !== typeClass) {
this.currentTypeClass = typeClass;
(this.chatBoxInput.nativeElement as HTMLElement).className = typeClass;
}
if (this.chatType === ChatType.Whisper) {
typePrefix = 'To ';
typeName = this.game.whisperTo && this.game.whisperTo.name || 'unknown';
} else {
typePrefix = '';
typeName = chatTypeNames[this.chatType];
}
if (this.chatType === ChatType.Whisper) {
typePrefix = 'To ';
typeName = this.game.whisperTo && this.game.whisperTo.name || 'unknown';
} else {
typePrefix = '';
typeName = chatTypeNames[this.chatType];
}
if (this.currentTypePrefix !== typePrefix) {
changed = true;
this.currentTypePrefix = typePrefix;
(this.typePrefix.nativeElement as HTMLElement).textContent = typePrefix;
}
if (this.currentTypePrefix !== typePrefix) {
changed = true;
this.currentTypePrefix = typePrefix;
(this.typePrefix.nativeElement as HTMLElement).textContent = typePrefix;
}
if (this.currentTypeName !== typeName) {
changed = true;
this.currentTypeName = typeName;
replaceNodes(this.typeName.nativeElement, typeName);
}
if (this.currentTypeName !== typeName) {
changed = true;
this.currentTypeName = typeName;
replaceNodes(this.typeName.nativeElement, typeName);
}
if (changed) {
const { width } = (this.typeBox.nativeElement as HTMLElement).getBoundingClientRect();
const padding = 35 + 13 + Math.ceil(width);
(this.inputElement.nativeElement as HTMLElement).style.paddingLeft = `${padding}px`;
}
}
if (changed) {
const { width } = (this.typeBox.nativeElement as HTMLElement).getBoundingClientRect();
const padding = 35 + 13 + Math.ceil(width);
(this.inputElement.nativeElement as HTMLElement).style.paddingLeft = `${padding}px`;
}
}
}
function chatTypeClass(chatType: ChatType, supporter: number) {
if (chatType === ChatType.Supporter) {
switch (supporter) {
case 1: return 'chat-sup chat-sup1';
case 2: return 'chat-sup chat-sup2';
case 3: return 'chat-sup chat-sup3';
}
}
if (chatType === ChatType.Supporter) {
switch (supporter) {
case 1: return 'chat-sup chat-sup1';
case 2: return 'chat-sup chat-sup2';
case 3: return 'chat-sup chat-sup3';
}
}
return chatTypeClasses[chatType];
return chatTypeClasses[chatType];
}
function isValidChatType(type: ChatType, game: PonyTownGame) {
const supporter = game.model.supporter;
const supporter = game.model.supporter;
switch (type) {
case ChatType.Say:
case ChatType.Think:
case ChatType.Whisper:
return true;
case ChatType.Party:
case ChatType.PartyThink:
return isInParty(game);
case ChatType.Supporter:
return supporter > 0;
case ChatType.Supporter1:
return supporter >= 1;
case ChatType.Supporter2:
return supporter >= 2;
case ChatType.Supporter3:
return supporter >= 3;
case ChatType.Dismiss:
return false;
default:
return invalidEnumReturn(type, false);
}
switch (type) {
case ChatType.Say:
case ChatType.Think:
case ChatType.Whisper:
return true;
case ChatType.Party:
case ChatType.PartyThink:
return isInParty(game);
case ChatType.Supporter:
return supporter > 0;
case ChatType.Supporter1:
return supporter >= 1;
case ChatType.Supporter2:
return supporter >= 2;
case ChatType.Supporter3:
return supporter >= 3;
case ChatType.Dismiss:
return false;
default:
return invalidEnumReturn(type, false);
}
}
function getChatTypes(game: PonyTownGame) {
const chatTypes = [ChatType.Say];
const supporter = game.model.supporter;
const chatTypes = [ChatType.Say];
const supporter = game.model.supporter;
if (isInParty(game)) {
chatTypes.push(ChatType.Party);
}
if (isInParty(game)) {
chatTypes.push(ChatType.Party);
}
if (supporter) {
chatTypes.push(ChatType.Supporter);
}
if (supporter) {
chatTypes.push(ChatType.Supporter);
}
return chatTypes;
return chatTypes;
}
File diff suppressed because it is too large Load Diff
+15 -15
View File
@@ -2,21 +2,21 @@ import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from
import { faCheck } from '../../../client/icons';
@Component({
selector: 'check-box',
templateUrl: 'check-box.pug',
styleUrls: ['check-box.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'check-box',
templateUrl: 'check-box.pug',
styleUrls: ['check-box.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CheckBox {
@Input() icon = faCheck;
@Input() label?: string;
@Input() disabled = false;
@Input() checked = false;
@Output() checkedChange = new EventEmitter<boolean>();
toggle() {
if (!this.disabled) {
this.checked = !this.checked;
this.checkedChange.emit(this.checked);
}
}
@Input() icon = faCheck;
@Input() label?: string;
@Input() disabled = false;
@Input() checked = false;
@Output() checkedChange = new EventEmitter<boolean>();
toggle() {
if (!this.disabled) {
this.checked = !this.checked;
this.checkedChange.emit(this.checked);
}
}
}
@@ -7,119 +7,119 @@ import { faChevronDown } from '../../../client/icons';
const SIZE = 175;
@Component({
selector: 'color-picker',
templateUrl: 'color-picker.pug',
styleUrls: ['color-picker.scss'],
selector: 'color-picker',
templateUrl: 'color-picker.pug',
styleUrls: ['color-picker.scss'],
})
export class ColorPicker {
readonly chevronIcon = faChevronDown;
@Input() isOpen = false;
@Input() isDisabled = false;
@Input() disabledColor = '';
@Input() color = '';
@Input() indicatorColor = '';
@Input() label?: string = undefined;
@Input() labelledBy?: string = undefined;
@Output() colorChange = new EventEmitter<string>();
s = 0;
v = 0;
h = 0;
private lastColor = '';
private closeHandler = () => this.close();
get inputColor() {
return this.isDisabled && this.disabledColor ? this.disabledColor : this.color;
}
set inputColor(value) {
if (!this.isDisabled) {
this.color = value;
}
}
get bg() {
return colorToCSS(parseColorFast(this.inputColor));
}
get svLeft() {
this.updateHsv();
return this.s * 100;
}
get svTop() {
this.updateHsv();
return (1 - this.v) * 100;
}
get hueTop() {
this.updateHsv();
return this.h * 100 / 360;
}
get hue() {
this.updateHsv();
return colorToCSS(colorFromHSVA(this.h, 1, 1, 1));
}
focus(e: Event) {
this.isOpen = true;
(e.target as HTMLInputElement).select();
}
dragSV({ event, x, y }: AgDragEvent) {
event.preventDefault();
readonly chevronIcon = faChevronDown;
@Input() isOpen = false;
@Input() isDisabled = false;
@Input() disabledColor = '';
@Input() color = '';
@Input() indicatorColor = '';
@Input() label?: string = undefined;
@Input() labelledBy?: string = undefined;
@Output() colorChange = new EventEmitter<string>();
s = 0;
v = 0;
h = 0;
private lastColor = '';
private closeHandler = () => this.close();
get inputColor() {
return this.isDisabled && this.disabledColor ? this.disabledColor : this.color;
}
set inputColor(value) {
if (!this.isDisabled) {
this.color = value;
}
}
get bg() {
return colorToCSS(parseColorFast(this.inputColor));
}
get svLeft() {
this.updateHsv();
return this.s * 100;
}
get svTop() {
this.updateHsv();
return (1 - this.v) * 100;
}
get hueTop() {
this.updateHsv();
return this.h * 100 / 360;
}
get hue() {
this.updateHsv();
return colorToCSS(colorFromHSVA(this.h, 1, 1, 1));
}
focus(e: Event) {
this.isOpen = true;
(e.target as HTMLInputElement).select();
}
dragSV({ event, x, y }: AgDragEvent) {
event.preventDefault();
this.updateHsv();
this.s = clamp(x / SIZE, 0, 1);
this.v = 1 - clamp(y / SIZE, 0, 1);
this.updateColor();
}
dragHue({ event, y }: AgDragEvent) {
event.preventDefault();
this.updateHsv();
this.s = clamp(x / SIZE, 0, 1);
this.v = 1 - clamp(y / SIZE, 0, 1);
this.updateColor();
}
dragHue({ event, y }: AgDragEvent) {
event.preventDefault();
this.updateHsv();
this.h = clamp(360 * y / SIZE, 0, 360);
this.updateColor();
}
updateHsv() {
if (this.lastColor !== this.color) {
const { h, s, v } = colorToHSVA(parseColorFast(this.color), this.h);
this.h = h;
this.s = s;
this.v = v;
this.lastColor = this.color;
}
}
updateColor() {
const color = colorToHexRGB(colorFromHSVA(this.h, this.s, this.v, 1));
const changed = this.color !== color;
this.lastColor = this.color = color;
this.updateHsv();
this.h = clamp(360 * y / SIZE, 0, 360);
this.updateColor();
}
updateHsv() {
if (this.lastColor !== this.color) {
const { h, s, v } = colorToHSVA(parseColorFast(this.color), this.h);
this.h = h;
this.s = s;
this.v = v;
this.lastColor = this.color;
}
}
updateColor() {
const color = colorToHexRGB(colorFromHSVA(this.h, this.s, this.v, 1));
const changed = this.color !== color;
this.lastColor = this.color = color;
if (changed) {
this.colorChange.emit(color);
}
}
inputChanged(value: string) {
this.color = value;
this.colorChange.emit(this.color);
}
stopEvent(e: Event) {
e.stopPropagation();
e.preventDefault();
}
open() {
if (!this.isOpen) {
this.isOpen = true;
if (changed) {
this.colorChange.emit(color);
}
}
inputChanged(value: string) {
this.color = value;
this.colorChange.emit(this.color);
}
stopEvent(e: Event) {
e.stopPropagation();
e.preventDefault();
}
open() {
if (!this.isOpen) {
this.isOpen = true;
setTimeout(() => {
document.addEventListener('mousedown', this.closeHandler);
document.addEventListener('touchstart', this.closeHandler);
});
}
}
close() {
this.isOpen = false;
document.removeEventListener('mousedown', this.closeHandler);
document.removeEventListener('touchstart', this.closeHandler);
}
toggleOpen() {
if (!this.isDisabled) {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
}
setTimeout(() => {
document.addEventListener('mousedown', this.closeHandler);
document.addEventListener('touchstart', this.closeHandler);
});
}
}
close() {
this.isOpen = false;
document.removeEventListener('mousedown', this.closeHandler);
document.removeEventListener('touchstart', this.closeHandler);
}
toggleOpen() {
if (!this.isDisabled) {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
}
}
@@ -2,15 +2,15 @@ import { Component, ChangeDetectionStrategy, Output, Input, EventEmitter } from
import { uniqueId } from 'lodash';
@Component({
selector: 'custom-checkbox',
templateUrl: 'custom-checkbox.pug',
styleUrls: ['custom-checkbox.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'custom-checkbox',
templateUrl: 'custom-checkbox.pug',
styleUrls: ['custom-checkbox.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomCheckbox {
@Input() disabled = false;
@Input() help = '';
@Input() checked = false;
@Output() checkedChange = new EventEmitter<boolean>();
helpId = uniqueId('custom-checkbox-help-');
@Input() disabled = false;
@Input() help = '';
@Input() checked = false;
@Output() checkedChange = new EventEmitter<boolean>();
helpId = uniqueId('custom-checkbox-help-');
}
@@ -4,52 +4,52 @@ import { MONTH_NAMES_EN } from '../../../common/constants';
import { getLocale } from '../../../client/clientUtils';
@Component({
selector: 'date-picker',
templateUrl: 'date-picker.pug',
selector: 'date-picker',
templateUrl: 'date-picker.pug',
})
export class DatePicker {
readonly days = times(31, i => i + 1);
readonly years: number[] = [];
readonly months = getMonthNames();
day = 0;
month = 0;
year = 0;
@Output() dateChange = new EventEmitter<string | undefined>();
constructor() {
const minYear = 1914;
const maxYear = (new Date()).getFullYear() - 6;
readonly days = times(31, i => i + 1);
readonly years: number[] = [];
readonly months = getMonthNames();
day = 0;
month = 0;
year = 0;
@Output() dateChange = new EventEmitter<string | undefined>();
constructor() {
const minYear = 1914;
const maxYear = (new Date()).getFullYear() - 6;
for (let year = maxYear; year >= minYear; year--) {
this.years.push(year);
}
}
@Input() get date() {
const date = createValidBirthDate(this.day, this.month, this.year);
return date && formatISODate(date);
}
set date(value) {
if (value) {
const { day, month, year } = parseISODate(value);
this.day = day;
this.month = month;
this.year = year;
}
}
change() {
this.dateChange.emit(this.date);
}
for (let year = maxYear; year >= minYear; year--) {
this.years.push(year);
}
}
@Input() get date() {
const date = createValidBirthDate(this.day, this.month, this.year);
return date && formatISODate(date);
}
set date(value) {
if (value) {
const { day, month, year } = parseISODate(value);
this.day = day;
this.month = month;
this.year = year;
}
}
change() {
this.dateChange.emit(this.date);
}
}
function getMonthNames() {
try {
const format = new Intl.DateTimeFormat(getLocale(), { month: 'long' });
try {
const format = new Intl.DateTimeFormat(getLocale(), { month: 'long' });
return times(12, i => {
const date = new Date(523456789);
date.setMonth(i);
return format.format(date);
});
} catch {
return MONTH_NAMES_EN;
}
return times(12, i => {
const date = new Date(523456789);
date.setMonth(i);
return format.format(date);
});
} catch {
return MONTH_NAMES_EN;
}
}
@@ -1,12 +1,12 @@
import { Directive, AfterViewInit, ElementRef } from '@angular/core';
@Directive({
selector: '[agAutoFocus]'
selector: '[agAutoFocus]'
})
export class AgAutoFocus implements AfterViewInit {
constructor(private element: ElementRef) {
}
ngAfterViewInit() {
setTimeout(() => this.element.nativeElement.focus(), 100);
}
constructor(private element: ElementRef) {
}
ngAfterViewInit() {
setTimeout(() => this.element.nativeElement.focus(), 100);
}
}
+113 -113
View File
@@ -3,138 +3,138 @@ import { noop } from 'lodash';
import { getButton, getX, getY, AnyEvent } from '../../../common/utils';
export interface AgDragEvent {
event: AnyEvent;
type: 'start' | 'drag' | 'end';
x: number;
y: number;
dx: number;
dy: number;
event: AnyEvent;
type: 'start' | 'drag' | 'end';
x: number;
y: number;
dx: number;
dy: number;
}
export interface AgDragOptions {
relative?: 'self' | 'parent';
prevent?: boolean;
relative?: 'self' | 'parent';
prevent?: boolean;
}
export function handleDrag(element: HTMLElement, emit: (event: AgDragEvent) => void, options: AgDragOptions = {}) {
// typeof PointerEvent !== 'undefined'
const eventSets = window.navigator.pointerEnabled ? [
{ down: 'pointerdown', move: 'pointermove', up: 'pointerup' }, // , up2: 'pointercancel' },
] : [
{ down: 'mousedown', move: 'mousemove', up: 'mouseup' },
{ down: 'touchstart', move: 'touchmove', up: 'touchend', up2: 'touchcancel' },
];
const emptyRect = { left: 0, top: 0 };
let rect = emptyRect;
let scrollLeft = 0;
let scrollTop = 0;
let startX = 0;
let startY = 0;
let button = 0;
let dragging = false;
let lastEvent: any;
// typeof PointerEvent !== 'undefined'
const eventSets = window.navigator.pointerEnabled ? [
{ down: 'pointerdown', move: 'pointermove', up: 'pointerup' }, // , up2: 'pointercancel' },
] : [
{ down: 'mousedown', move: 'mousemove', up: 'mouseup' },
{ down: 'touchstart', move: 'touchmove', up: 'touchend', up2: 'touchcancel' },
];
const emptyRect = { left: 0, top: 0 };
let rect = emptyRect;
let scrollLeft = 0;
let scrollTop = 0;
let startX = 0;
let startY = 0;
let button = 0;
let dragging = false;
let lastEvent: any;
function setupScrollAndRect() {
// TODO: fix issue with scroll
switch (options.relative) {
case 'self':
rect = element.getBoundingClientRect();
scrollLeft = -(window.scrollX || window.pageXOffset || 0);
scrollTop = -(window.scrollY || window.pageYOffset || 0);
break;
case 'parent':
rect = element.parentElement!.getBoundingClientRect();
scrollLeft = element.parentElement!.scrollLeft;
scrollTop = element.parentElement!.scrollTop;
break;
default:
rect = emptyRect;
scrollLeft = 0;
scrollTop = 0;
}
}
function setupScrollAndRect() {
// TODO: fix issue with scroll
switch (options.relative) {
case 'self':
rect = element.getBoundingClientRect();
scrollLeft = -(window.scrollX || window.pageXOffset || 0);
scrollTop = -(window.scrollY || window.pageYOffset || 0);
break;
case 'parent':
rect = element.parentElement!.getBoundingClientRect();
scrollLeft = element.parentElement!.scrollLeft;
scrollTop = element.parentElement!.scrollTop;
break;
default:
rect = emptyRect;
scrollLeft = 0;
scrollTop = 0;
}
}
function send(event: AnyEvent, type: 'start' | 'drag' | 'end') {
const x = getX(event);
const y = getY(event);
function send(event: AnyEvent, type: 'start' | 'drag' | 'end') {
const x = getX(event);
const y = getY(event);
emit({
event,
type,
x: x - rect.left + scrollLeft,
y: y - rect.top + scrollTop,
dx: x - startX,
dy: y - startY,
});
}
emit({
event,
type,
x: x - rect.left + scrollLeft,
y: y - rect.top + scrollTop,
dx: x - startX,
dy: y - startY,
});
}
const handlers = eventSets.map(events => {
function move(e: any) {
lastEvent = e;
e.preventDefault();
send(e, 'drag');
}
const handlers = eventSets.map(events => {
function move(e: any) {
lastEvent = e;
e.preventDefault();
send(e, 'drag');
}
function up(e: any) {
if (getButton(e) === button) {
// touchend event does not have x, y coordinates, use last touchmove event instead
if (e.type !== 'touchend' && e.type !== 'touchcancel') {
lastEvent = e;
}
end();
}
}
function up(e: any) {
if (getButton(e) === button) {
// touchend event does not have x, y coordinates, use last touchmove event instead
if (e.type !== 'touchend' && e.type !== 'touchcancel') {
lastEvent = e;
}
end();
}
}
function end() {
send(lastEvent, 'end');
window.removeEventListener(events.move, move);
window.removeEventListener(events.up, up);
events.up2 && window.removeEventListener(events.up2, up);
window.removeEventListener('blur', end);
dragging = false;
}
function end() {
send(lastEvent, 'end');
window.removeEventListener(events.move, move);
window.removeEventListener(events.up, up);
events.up2 && window.removeEventListener(events.up2, up);
window.removeEventListener('blur', end);
dragging = false;
}
function handler(e: any) {
if (!dragging) {
setupScrollAndRect();
dragging = true;
button = getButton(e);
startX = getX(e);
startY = getY(e);
send(e, 'start');
lastEvent = e;
function handler(e: any) {
if (!dragging) {
setupScrollAndRect();
dragging = true;
button = getButton(e);
startX = getX(e);
startY = getY(e);
send(e, 'start');
lastEvent = e;
window.addEventListener(events.move, move);
window.addEventListener(events.up, up);
events.up2 && window.addEventListener(events.up2, up);
window.addEventListener('blur', end);
e.stopPropagation();
window.addEventListener(events.move, move);
window.addEventListener(events.up, up);
events.up2 && window.addEventListener(events.up2, up);
window.addEventListener('blur', end);
e.stopPropagation();
if (options.prevent) {
e.preventDefault();
}
}
}
if (options.prevent) {
e.preventDefault();
}
}
}
element.addEventListener(events.down, handler);
return () => element.removeEventListener(events.down, handler);
});
element.addEventListener(events.down, handler);
return () => element.removeEventListener(events.down, handler);
});
return () => handlers.forEach(f => f());
return () => handlers.forEach(f => f());
}
@Directive({ selector: '[agDrag]' })
export class AgDrag implements OnInit, OnDestroy {
@Input('agDragRelative') relative: 'self' | 'parent' | undefined = undefined;
@Input('agDragPrevent') prevent = false;
@Output('agDrag') drag = new EventEmitter<AgDragEvent>();
private unsubscribe = noop;
constructor(private element: ElementRef) {
}
ngOnInit() {
this.unsubscribe = handleDrag(this.element.nativeElement, e => this.drag.emit(e), this);
}
ngOnDestroy() {
this.unsubscribe();
}
@Input('agDragRelative') relative: 'self' | 'parent' | undefined = undefined;
@Input('agDragPrevent') prevent = false;
@Output('agDrag') drag = new EventEmitter<AgDragEvent>();
private unsubscribe = noop;
constructor(private element: ElementRef) {
}
ngOnInit() {
this.unsubscribe = handleDrag(this.element.nativeElement, e => this.drag.emit(e), this);
}
ngOnDestroy() {
this.unsubscribe();
}
}
+10 -10
View File
@@ -1,17 +1,17 @@
import { Directive, OnInit, ElementRef } from '@angular/core';
@Directive({
selector: 'a[href]'
selector: 'a[href]'
})
export class Anchor implements OnInit {
constructor(private element: ElementRef) {
}
ngOnInit() {
const a = this.element.nativeElement as HTMLAnchorElement;
constructor(private element: ElementRef) {
}
ngOnInit() {
const a = this.element.nativeElement as HTMLAnchorElement;
if (/^(https?|mailto):/.test(a.href) && !a.target) {
a.setAttribute('target', '_blank');
a.setAttribute('rel', 'noopener noreferrer');
}
}
if (/^(https?|mailto):/.test(a.href) && !a.target) {
a.setAttribute('target', '_blank');
a.setAttribute('rel', 'noopener noreferrer');
}
}
}
@@ -2,29 +2,29 @@ import { Directive, Input, Optional } from '@angular/core';
import { NgModel } from '@angular/forms';
@Directive({
selector: '[btnHighlight]',
host: {
'[class.btn-default]': '!on',
'[class.btn-primary]': 'on',
},
selector: '[btnHighlight]',
host: {
'[class.btn-default]': '!on',
'[class.btn-primary]': 'on',
},
})
export class BtnHighlight {
@Input() btnHighlight?: boolean = undefined;
constructor(@Optional() private model?: NgModel) {
}
get on() {
const value = this.btnHighlight;
return (value === true || value === false || !this.model) ? value : !!this.model.value;
}
@Input() btnHighlight?: boolean = undefined;
constructor(@Optional() private model?: NgModel) {
}
get on() {
const value = this.btnHighlight;
return (value === true || value === false || !this.model) ? value : !!this.model.value;
}
}
@Directive({
selector: '[btnHighlightDanger]',
host: {
'[class.btn-default]': '!btnHighlightDanger',
'[class.btn-danger]': 'btnHighlightDanger',
},
selector: '[btnHighlightDanger]',
host: {
'[class.btn-default]': '!btnHighlightDanger',
'[class.btn-danger]': 'btnHighlightDanger',
},
})
export class BtnHighlightDanger {
@Input() btnHighlightDanger = false;
@Input() btnHighlightDanger = false;
}
+169 -169
View File
@@ -6,202 +6,202 @@ import { rect } from '../../../common/rect';
@Injectable({ providedIn: 'root' })
export class DraggableService {
root?: ElementRef;
draggedItem?: any;
activeDropZone?: DraggableDrop<any>;
dropZones: DraggableDrop<any>[] = [];
get rootElement(): HTMLElement {
return this.root ? this.root.nativeElement : document.body;
}
setActiveDropZone(dropZone: DraggableDrop<any> | undefined) {
if (this.activeDropZone !== dropZone) {
if (this.activeDropZone) {
this.activeDropZone.setActive(false);
}
root?: ElementRef;
draggedItem?: any;
activeDropZone?: DraggableDrop<any>;
dropZones: DraggableDrop<any>[] = [];
get rootElement(): HTMLElement {
return this.root ? this.root.nativeElement : document.body;
}
setActiveDropZone(dropZone: DraggableDrop<any> | undefined) {
if (this.activeDropZone !== dropZone) {
if (this.activeDropZone) {
this.activeDropZone.setActive(false);
}
this.activeDropZone = dropZone;
this.activeDropZone = dropZone;
if (this.activeDropZone) {
this.activeDropZone.setActive(true);
}
}
}
startMove(element: HTMLElement, item: any) {
this.setActiveDropZone(undefined);
this.rootElement.appendChild(element);
this.draggedItem = item;
this.initRects();
}
endMove() {
if (this.activeDropZone) {
this.activeDropZone.drop.emit(this.draggedItem);
this.setActiveDropZone(undefined);
}
if (this.activeDropZone) {
this.activeDropZone.setActive(true);
}
}
}
startMove(element: HTMLElement, item: any) {
this.setActiveDropZone(undefined);
this.rootElement.appendChild(element);
this.draggedItem = item;
this.initRects();
}
endMove() {
if (this.activeDropZone) {
this.activeDropZone.drop.emit(this.draggedItem);
this.setActiveDropZone(undefined);
}
this.draggedItem = undefined;
}
addDropZone(dropZone: DraggableDrop<any>) {
this.dropZones.push(dropZone);
this.draggedItem = undefined;
}
addDropZone(dropZone: DraggableDrop<any>) {
this.dropZones.push(dropZone);
if (this.draggedItem) {
this.initRects();
}
}
removeDropZone(dropZone: DraggableDrop<any>) {
removeItem(this.dropZones, dropZone);
if (this.draggedItem) {
this.initRects();
}
}
removeDropZone(dropZone: DraggableDrop<any>) {
removeItem(this.dropZones, dropZone);
if (this.draggedItem) {
this.initRects();
}
if (this.draggedItem) {
this.initRects();
}
if (this.activeDropZone === dropZone) {
this.setActiveDropZone(undefined);
}
}
updateHover(x: number, y: number) {
if (this.draggedItem) {
for (const zone of this.dropZones) {
if (pointInRect(x, y, zone.rect)) {
this.setActiveDropZone(zone);
return;
}
}
if (this.activeDropZone === dropZone) {
this.setActiveDropZone(undefined);
}
}
updateHover(x: number, y: number) {
if (this.draggedItem) {
for (const zone of this.dropZones) {
if (pointInRect(x, y, zone.rect)) {
this.setActiveDropZone(zone);
return;
}
}
this.setActiveDropZone(undefined);
}
}
private initRects() {
this.dropZones.forEach(i => i.initRect());
}
this.setActiveDropZone(undefined);
}
}
private initRects() {
this.dropZones.forEach(i => i.initRect());
}
}
@Component({
selector: 'draggable-outlet',
template: `<div></div>`,
styles: [`:host { position: fixed; top: 0; left: 0; z-index: 10000; }`],
selector: 'draggable-outlet',
template: `<div></div>`,
styles: [`:host { position: fixed; top: 0; left: 0; z-index: 10000; }`],
})
export class DraggableOutlet {
constructor(element: ElementRef, service: DraggableService) {
service.root = element;
}
constructor(element: ElementRef, service: DraggableService) {
service.root = element;
}
}
@Directive({ selector: '[draggableDrop]' })
export class DraggableDrop<T> implements OnInit, OnDestroy {
@Input('draggablePad') pad = 0;
@Output('draggableDrop') drop = new EventEmitter<T>();
rect = rect(0, 0, 0, 0);
constructor(private element: ElementRef, private service: DraggableService) {
}
ngOnInit() {
this.service.addDropZone(this);
}
ngOnDestroy() {
this.service.removeDropZone(this);
}
setActive(active: boolean) {
const element = this.element.nativeElement as HTMLElement;
@Input('draggablePad') pad = 0;
@Output('draggableDrop') drop = new EventEmitter<T>();
rect = rect(0, 0, 0, 0);
constructor(private element: ElementRef, private service: DraggableService) {
}
ngOnInit() {
this.service.addDropZone(this);
}
ngOnDestroy() {
this.service.removeDropZone(this);
}
setActive(active: boolean) {
const element = this.element.nativeElement as HTMLElement;
if (active) {
element.classList.add('draggable-hover');
} else {
element.classList.remove('draggable-hover');
}
}
initRect() {
const element = this.element.nativeElement as HTMLElement;
const clientBounds = element.getBoundingClientRect();
this.rect.x = clientBounds.left - this.pad;
this.rect.y = clientBounds.top - this.pad;
this.rect.w = clientBounds.width + 2 * this.pad;
this.rect.h = clientBounds.height + 2 * this.pad;
}
if (active) {
element.classList.add('draggable-hover');
} else {
element.classList.remove('draggable-hover');
}
}
initRect() {
const element = this.element.nativeElement as HTMLElement;
const clientBounds = element.getBoundingClientRect();
this.rect.x = clientBounds.left - this.pad;
this.rect.y = clientBounds.top - this.pad;
this.rect.w = clientBounds.width + 2 * this.pad;
this.rect.h = clientBounds.height + 2 * this.pad;
}
}
@Directive({
selector: '[draggableItem]',
host: {
'[style.touch-action]': `touchAction`,
}
selector: '[draggableItem]',
host: {
'[style.touch-action]': `touchAction`,
}
})
export class DraggableItem<T> implements OnInit, OnDestroy {
@Input('draggableItem') item: T | undefined;
@Output('draggableDrag') dragStarted = new EventEmitter<void>();
private startX = 0;
private startY = 0;
private draggable?: HTMLElement;
private width = 0;
private height = 0;
private unsubscribeDrag = noop;
private _disabled = false;
constructor(private element: ElementRef, private service: DraggableService) {
}
ngOnInit() {
this.setupDragEvents();
}
ngOnDestroy() {
this.unsubscribeDrag();
}
get touchAction() {
return this.disabled ? 'inherit' : 'none';
}
@Input('draggableDisabled') get disabled() {
return this._disabled;
}
set disabled(value) {
if (this._disabled !== value) {
this._disabled = value;
this.setupDragEvents();
}
}
private setupDragEvents() {
this.unsubscribeDrag();
this.unsubscribeDrag = noop;
@Input('draggableItem') item: T | undefined;
@Output('draggableDrag') dragStarted = new EventEmitter<void>();
private startX = 0;
private startY = 0;
private draggable?: HTMLElement;
private width = 0;
private height = 0;
private unsubscribeDrag = noop;
private _disabled = false;
constructor(private element: ElementRef, private service: DraggableService) {
}
ngOnInit() {
this.setupDragEvents();
}
ngOnDestroy() {
this.unsubscribeDrag();
}
get touchAction() {
return this.disabled ? 'inherit' : 'none';
}
@Input('draggableDisabled') get disabled() {
return this._disabled;
}
set disabled(value) {
if (this._disabled !== value) {
this._disabled = value;
this.setupDragEvents();
}
}
private setupDragEvents() {
this.unsubscribeDrag();
this.unsubscribeDrag = noop;
if (!this.disabled) {
this.unsubscribeDrag = handleDrag(this.element.nativeElement, e => this.drag(e), { prevent: true });
}
}
drag(e: AgDragEvent) {
if (this.item && !this.disabled && !this.draggable && (Math.abs(e.dx) > 5 || Math.abs(e.dy) > 5)) {
const element = this.element.nativeElement as HTMLElement;
const rect = element.getBoundingClientRect();
this.startX = rect.left;
this.startY = rect.top;
this.draggable = element.cloneNode(true) as HTMLElement;
this.draggable.style.position = 'absolute';
this.draggable.style.width = `${rect.width}px`;
this.draggable.style.height = `${rect.height}px`;
this.draggable.style.margin = '0';
this.draggable.classList.add('draggable-dragging');
this.width = rect.width;
this.height = rect.height;
if (!this.disabled) {
this.unsubscribeDrag = handleDrag(this.element.nativeElement, e => this.drag(e), { prevent: true });
}
}
drag(e: AgDragEvent) {
if (this.item && !this.disabled && !this.draggable && (Math.abs(e.dx) > 5 || Math.abs(e.dy) > 5)) {
const element = this.element.nativeElement as HTMLElement;
const rect = element.getBoundingClientRect();
this.startX = rect.left;
this.startY = rect.top;
this.draggable = element.cloneNode(true) as HTMLElement;
this.draggable.style.position = 'absolute';
this.draggable.style.width = `${rect.width}px`;
this.draggable.style.height = `${rect.height}px`;
this.draggable.style.margin = '0';
this.draggable.classList.add('draggable-dragging');
this.width = rect.width;
this.height = rect.height;
const src = element.querySelectorAll('canvas') as NodeListOf<HTMLCanvasElement>;
const dst = this.draggable.querySelectorAll('canvas') as NodeListOf<HTMLCanvasElement>;
const src = element.querySelectorAll('canvas') as NodeListOf<HTMLCanvasElement>;
const dst = this.draggable.querySelectorAll('canvas') as NodeListOf<HTMLCanvasElement>;
for (let i = 0; i < src.length; i++) {
const context = dst.item(i).getContext('2d');
context && context.drawImage(src.item(i), 0, 0);
}
for (let i = 0; i < src.length; i++) {
const context = dst.item(i).getContext('2d');
context && context.drawImage(src.item(i), 0, 0);
}
this.service.startMove(this.draggable, this.item!);
this.dragStarted.emit();
}
this.service.startMove(this.draggable, this.item!);
this.dragStarted.emit();
}
if (this.draggable) {
if (e.type === 'end') {
this.draggable!.parentNode!.removeChild(this.draggable!);
this.draggable = undefined;
this.service.endMove();
} else {
const x = clamp(this.startX + e.dx, 0, window.innerWidth - this.width);
const y = clamp(this.startY + e.dy, 0, window.innerHeight - this.height);
setTransform(this.draggable, `translate3d(${x}px, ${y}px, 0px)`);
this.service.updateHover(e.x, e.y);
}
}
}
if (this.draggable) {
if (e.type === 'end') {
this.draggable!.parentNode!.removeChild(this.draggable!);
this.draggable = undefined;
this.service.endMove();
} else {
const x = clamp(this.startX + e.dx, 0, window.innerWidth - this.width);
const y = clamp(this.startY + e.dy, 0, window.innerHeight - this.height);
setTransform(this.draggable, `translate3d(${x}px, ${y}px, 0px)`);
this.service.updateHover(e.x, e.y);
}
}
}
}
export const draggableComponents = [DraggableOutlet, DraggableItem, DraggableDrop];
+179 -179
View File
@@ -1,227 +1,227 @@
import {
Directive, HostListener, Input, Output, EventEmitter, TemplateRef, ViewContainerRef, ContentChild,
Renderer2, ElementRef, EmbeddedViewRef, Component, Injectable
Directive, HostListener, Input, Output, EventEmitter, TemplateRef, ViewContainerRef, ContentChild,
Renderer2, ElementRef, EmbeddedViewRef, Component, Injectable
} from '@angular/core';
import { uniqueId } from 'lodash';
import { focusFirstElement } from '../../../client/htmlUtils';
@Injectable({ providedIn: 'root' })
export class DropdownOutletService {
viewContainer?: ViewContainerRef;
rootElement?: HTMLElement;
viewContainer?: ViewContainerRef;
rootElement?: HTMLElement;
}
@Component({
selector: 'dropdown-outlet',
template: `<ng-template></ng-template>`,
selector: 'dropdown-outlet',
template: `<ng-template></ng-template>`,
})
export class DropdownOutlet {
constructor(service: DropdownOutletService, viewContainer: ViewContainerRef, element: ElementRef) {
service.viewContainer = viewContainer;
service.rootElement = element.nativeElement.parentElement;
}
constructor(service: DropdownOutletService, viewContainer: ViewContainerRef, element: ElementRef) {
service.viewContainer = viewContainer;
service.rootElement = element.nativeElement.parentElement;
}
}
@Directive({
selector: '[dropdownMenu]',
selector: '[dropdownMenu]',
})
export class DropdownMenu {
ref?: EmbeddedViewRef<any>;
id = uniqueId('dropdown-menu-');
private onClose?: () => void;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private renderer: Renderer2,
private service: DropdownOutletService,
) {
}
private get root(): HTMLElement {
return this.ref && this.ref.rootNodes[0];
}
open(useOutlet: boolean, rootElement: HTMLElement) {
if (!this.ref) {
if (useOutlet) {
this.ref = this.service.viewContainer!.createEmbeddedView(this.templateRef);
} else {
this.ref = this.viewContainer.createEmbeddedView(this.templateRef);
}
ref?: EmbeddedViewRef<any>;
id = uniqueId('dropdown-menu-');
private onClose?: () => void;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private renderer: Renderer2,
private service: DropdownOutletService,
) {
}
private get root(): HTMLElement {
return this.ref && this.ref.rootNodes[0];
}
open(useOutlet: boolean, rootElement: HTMLElement) {
if (!this.ref) {
if (useOutlet) {
this.ref = this.service.viewContainer!.createEmbeddedView(this.templateRef);
} else {
this.ref = this.viewContainer.createEmbeddedView(this.templateRef);
}
const { renderer, root } = this;
const { renderer, root } = this;
renderer.addClass(root, 'show');
renderer.setAttribute(root, 'id', this.id);
renderer.addClass(root, 'show');
renderer.setAttribute(root, 'id', this.id);
if (useOutlet) {
const positionMenu = () => {
const rect = rootElement.getBoundingClientRect();
const menuRect = root.getBoundingClientRect();
let transform: string;
if (useOutlet) {
const positionMenu = () => {
const rect = rootElement.getBoundingClientRect();
const menuRect = root.getBoundingClientRect();
let transform: string;
if ((rect.bottom + menuRect.height) > window.innerHeight) {
transform = `translate3d(${Math.round(rect.left)}px, ${Math.round(rect.top - menuRect.height)}px, 0)`;
renderer.addClass(root, 'dropdown-menu-up');
} else {
transform = `translate3d(${Math.round(rect.left)}px, ${Math.round(rect.bottom)}px, 0)`;
renderer.removeClass(root, 'dropdown-menu-up');
}
if ((rect.bottom + menuRect.height) > window.innerHeight) {
transform = `translate3d(${Math.round(rect.left)}px, ${Math.round(rect.top - menuRect.height)}px, 0)`;
renderer.addClass(root, 'dropdown-menu-up');
} else {
transform = `translate3d(${Math.round(rect.left)}px, ${Math.round(rect.bottom)}px, 0)`;
renderer.removeClass(root, 'dropdown-menu-up');
}
renderer.setStyle(root, 'transform', transform);
};
renderer.setStyle(root, 'transform', transform);
};
renderer.addClass(root, 'dropdown-in-outlet');
positionMenu();
renderer.addClass(root, 'dropdown-in-outlet');
positionMenu();
const closeDropdown = () => {
this.close();
};
const closeDropdown = () => {
this.close();
};
document.addEventListener('scroll', closeDropdown, true);
window.addEventListener('resize', closeDropdown, true);
document.addEventListener('scroll', closeDropdown, true);
window.addEventListener('resize', closeDropdown, true);
this.onClose = () => {
document.removeEventListener('scroll', closeDropdown, true);
window.removeEventListener('resize', closeDropdown, true);
};
}
}
}
close() {
if (this.ref) {
this.ref.destroy();
this.ref = undefined;
}
this.onClose = () => {
document.removeEventListener('scroll', closeDropdown, true);
window.removeEventListener('resize', closeDropdown, true);
};
}
}
}
close() {
if (this.ref) {
this.ref.destroy();
this.ref = undefined;
}
if (this.onClose) {
this.onClose();
this.onClose = undefined;
}
}
checkTarget(e: Event) {
return this.root && this.root.contains(e.target as any);
}
focusFirstElement() {
if (this.root) {
focusFirstElement(this.root);
}
}
if (this.onClose) {
this.onClose();
this.onClose = undefined;
}
}
checkTarget(e: Event) {
return this.root && this.root.contains(e.target as any);
}
focusFirstElement() {
if (this.root) {
focusFirstElement(this.root);
}
}
}
@Directive({
selector: '[dropdown]',
exportAs: 'ag-dropdown',
host: {
'[class.show]': 'isOpen',
},
selector: '[dropdown]',
exportAs: 'ag-dropdown',
host: {
'[class.show]': 'isOpen',
},
})
export class Dropdown {
dropdownToggle?: DropdownToggle;
@ContentChild(DropdownMenu, { static: false }) menu!: DropdownMenu;
@Input() autoClose: boolean | 'outsideClick' = true;
@Input() preventAutoCloseOnOutlet = false;
@Input() hookToCanvas = false;
@Input() focusOnOpen = true;
@Input() focusOnClose = true;
@Input() useOutlet = false;
@Input() isOpen = false;
@Output() isOpenChange = new EventEmitter<boolean>();
get menuId() {
return this.isOpen ? this.menu.id : '';
}
constructor(private element: ElementRef, private service: DropdownOutletService) {
}
open() {
if (!this.isOpen) {
this.isOpen = true;
this.isOpenChange.emit(true);
this.menu.open(this.useOutlet, this.element.nativeElement);
dropdownToggle?: DropdownToggle;
@ContentChild(DropdownMenu, { static: false }) menu!: DropdownMenu;
@Input() autoClose: boolean | 'outsideClick' = true;
@Input() preventAutoCloseOnOutlet = false;
@Input() hookToCanvas = false;
@Input() focusOnOpen = true;
@Input() focusOnClose = true;
@Input() useOutlet = false;
@Input() isOpen = false;
@Output() isOpenChange = new EventEmitter<boolean>();
get menuId() {
return this.isOpen ? this.menu.id : '';
}
constructor(private element: ElementRef, private service: DropdownOutletService) {
}
open() {
if (!this.isOpen) {
this.isOpen = true;
this.isOpenChange.emit(true);
this.menu.open(this.useOutlet, this.element.nativeElement);
setTimeout(() => {
document.addEventListener('click', this.closeHandler);
document.addEventListener('keydown', this.closeHandler);
setTimeout(() => {
document.addEventListener('click', this.closeHandler);
document.addEventListener('keydown', this.closeHandler);
if (this.focusOnOpen) {
this.menu.focusFirstElement();
}
if (this.focusOnOpen) {
this.menu.focusFirstElement();
}
if (this.hookToCanvas) {
const canvas = document.getElementById('canvas');
if (this.hookToCanvas) {
const canvas = document.getElementById('canvas');
if (canvas) {
canvas.addEventListener('touchstart', this.canvasCloseHandler);
canvas.addEventListener('mousedown', this.canvasCloseHandler);
}
}
});
}
}
close() {
if (this.isOpen) {
this.isOpen = false;
this.isOpenChange.emit(false);
this.menu.close();
if (canvas) {
canvas.addEventListener('touchstart', this.canvasCloseHandler);
canvas.addEventListener('mousedown', this.canvasCloseHandler);
}
}
});
}
}
close() {
if (this.isOpen) {
this.isOpen = false;
this.isOpenChange.emit(false);
this.menu.close();
if (this.focusOnClose && this.dropdownToggle) {
this.dropdownToggle.focus();
}
if (this.focusOnClose && this.dropdownToggle) {
this.dropdownToggle.focus();
}
document.removeEventListener('click', this.closeHandler);
document.removeEventListener('keydown', this.closeHandler);
document.removeEventListener('click', this.closeHandler);
document.removeEventListener('keydown', this.closeHandler);
if (this.hookToCanvas) {
const canvas = document.getElementById('canvas');
if (this.hookToCanvas) {
const canvas = document.getElementById('canvas');
if (canvas) {
canvas.removeEventListener('touchstart', this.canvasCloseHandler);
canvas.removeEventListener('mousedown', this.canvasCloseHandler);
}
}
}
}
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
private closeHandler: any = (e: KeyboardEvent) => {
if (
!e.keyCode
&& (this.autoClose || (this.dropdownToggle && this.dropdownToggle.checkTarget(e)))
&& !(this.preventAutoCloseOnOutlet && this.service.rootElement && this.service.rootElement.contains(e.target as any))
&& !(this.autoClose === 'outsideClick' && this.menu.checkTarget(e))
) {
this.close();
} else if (this.autoClose && e.keyCode === 27) { // esc
this.close();
}
}
private canvasCloseHandler: any = () => this.close();
if (canvas) {
canvas.removeEventListener('touchstart', this.canvasCloseHandler);
canvas.removeEventListener('mousedown', this.canvasCloseHandler);
}
}
}
}
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
private closeHandler: any = (e: KeyboardEvent) => {
if (
!e.keyCode
&& (this.autoClose || (this.dropdownToggle && this.dropdownToggle.checkTarget(e)))
&& !(this.preventAutoCloseOnOutlet && this.service.rootElement && this.service.rootElement.contains(e.target as any))
&& !(this.autoClose === 'outsideClick' && this.menu.checkTarget(e))
) {
this.close();
} else if (this.autoClose && e.keyCode === 27) { // esc
this.close();
}
}
private canvasCloseHandler: any = () => this.close();
}
@Directive({
selector: '[dropdownToggle]',
host: {
'aria-haspopup': 'true',
'[attr.aria-expanded]': 'dropdown.isOpen',
'[attr.aria-controls]': 'dropdown.isOpen ? dropdown.menuId : undefined',
},
selector: '[dropdownToggle]',
host: {
'aria-haspopup': 'true',
'[attr.aria-expanded]': 'dropdown.isOpen',
'[attr.aria-controls]': 'dropdown.isOpen ? dropdown.menuId : undefined',
},
})
export class DropdownToggle {
constructor(private element: ElementRef, public dropdown: Dropdown) {
dropdown.dropdownToggle = this;
}
@HostListener('click')
click() {
this.dropdown.toggle();
}
checkTarget(e: Event) {
return this.element.nativeElement.contains(e.target);
}
focus() {
this.element.nativeElement.focus();
}
constructor(private element: ElementRef, public dropdown: Dropdown) {
dropdown.dropdownToggle = this;
}
@HostListener('click')
click() {
this.dropdown.toggle();
}
checkTarget(e: Event) {
return this.element.nativeElement.contains(e.target);
}
focus() {
this.element.nativeElement.focus();
}
}
export const dropdownDirectives = [Dropdown, DropdownToggle, DropdownMenu, DropdownOutlet];
+15 -15
View File
@@ -1,22 +1,22 @@
import { Directive, ElementRef, Input, HostListener, HostBinding, Output, EventEmitter } from '@angular/core';
@Directive({
selector: '[fixToTop]',
selector: '[fixToTop]',
})
export class FixToTop {
@Input() fixToTopOffset = 0;
@Output() fixToTop = new EventEmitter<boolean>();
@HostBinding('class.fixed-to-top') fixed = false;
constructor(private element: ElementRef) {
}
@HostListener('window:scroll')
scroll() {
const element = this.element.nativeElement as HTMLElement;
const { top } = element.getBoundingClientRect();
@Input() fixToTopOffset = 0;
@Output() fixToTop = new EventEmitter<boolean>();
@HostBinding('class.fixed-to-top') fixed = false;
constructor(private element: ElementRef) {
}
@HostListener('window:scroll')
scroll() {
const element = this.element.nativeElement as HTMLElement;
const { top } = element.getBoundingClientRect();
if (this.fixed !== top < this.fixToTopOffset) {
this.fixed = top < this.fixToTopOffset;
this.fixToTop.emit(this.fixed);
}
}
if (this.fixed !== top < this.fixToTopOffset) {
this.fixed = top < this.fixToTopOffset;
this.fixToTop.emit(this.fixed);
}
}
}
@@ -1,15 +1,15 @@
import { Directive, AfterViewInit, ElementRef } from '@angular/core';
@Directive({
selector: '[focusTitle]',
host: {
'tabindex': '-1',
},
selector: '[focusTitle]',
host: {
'tabindex': '-1',
},
})
export class FocusTitle implements AfterViewInit {
constructor(private element: ElementRef) {
}
ngAfterViewInit() {
setTimeout(() => this.element.nativeElement.focus());
}
constructor(private element: ElementRef) {
}
ngAfterViewInit() {
setTimeout(() => this.element.nativeElement.focus());
}
}
@@ -3,55 +3,55 @@ import { isParentOf, focusFirstElement, findFocusableElements } from '../../../c
import { isMobile } from '../../../client/data';
@Directive({
selector: '[focusTrap]',
selector: '[focusTrap]',
})
export class FocusTrap implements OnInit, OnDestroy {
private on = true;
private lastActiveElement?: HTMLElement;
@Input() set focusTrap(value: boolean) {
if (this.on !== value) {
this.on = value;
this.update();
}
}
constructor(private element: ElementRef) {
}
ngOnInit() {
this.update();
}
ngOnDestroy() {
this.focusTrap = false;
}
private update() {
if (!isMobile) {
if (this.on) {
this.lastActiveElement = document.activeElement as HTMLElement;
document.addEventListener('focusin', this.focus);
private on = true;
private lastActiveElement?: HTMLElement;
@Input() set focusTrap(value: boolean) {
if (this.on !== value) {
this.on = value;
this.update();
}
}
constructor(private element: ElementRef) {
}
ngOnInit() {
this.update();
}
ngOnDestroy() {
this.focusTrap = false;
}
private update() {
if (!isMobile) {
if (this.on) {
this.lastActiveElement = document.activeElement as HTMLElement;
document.addEventListener('focusin', this.focus);
if (!isParentOf(this.element.nativeElement, this.lastActiveElement)) {
setTimeout(() => this.lastActiveElement = focusFirstElement(this.element.nativeElement));
}
} else {
this.lastActiveElement = undefined;
document.removeEventListener('focusin', this.focus);
}
}
}
private focus = (e: Event) => {
if (isParentOf(this.element.nativeElement, e.target as any)) {
this.lastActiveElement = e.target as any;
} else {
const focusable = findFocusableElements(this.element.nativeElement);
if (!isParentOf(this.element.nativeElement, this.lastActiveElement)) {
setTimeout(() => this.lastActiveElement = focusFirstElement(this.element.nativeElement));
}
} else {
this.lastActiveElement = undefined;
document.removeEventListener('focusin', this.focus);
}
}
}
private focus = (e: Event) => {
if (isParentOf(this.element.nativeElement, e.target as any)) {
this.lastActiveElement = e.target as any;
} else {
const focusable = findFocusableElements(this.element.nativeElement);
if (focusable.length) {
if (this.lastActiveElement === focusable[0]) {
this.lastActiveElement = focusable[focusable.length - 1];
} else {
this.lastActiveElement = focusable[0];
}
if (focusable.length) {
if (this.lastActiveElement === focusable[0]) {
this.lastActiveElement = focusable[focusable.length - 1];
} else {
this.lastActiveElement = focusable[0];
}
this.lastActiveElement.focus();
}
}
}
this.lastActiveElement.focus();
}
}
}
}
@@ -4,57 +4,57 @@ import { hasFeatureFlag, featureFlagsChanged } from '../../../client/clientUtils
import { Model } from '../../services/model';
@Directive({
selector: '[hasFeature]',
selector: '[hasFeature]',
})
export class HasFeature implements AfterViewInit, OnDestroy {
private subscriptions: Subscription[] = [];
private showing = false;
private _flag: string | undefined = undefined;
private _orMod = false;
private _alsoIf = true;
private ref?: EmbeddedViewRef<any>;
constructor(private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef, private model: Model) {
}
ngAfterViewInit() {
this.subscriptions.push(featureFlagsChanged.subscribe(() => this.update()));
this.subscriptions.push(this.model.accountChanged.subscribe(() => this.update()));
}
ngOnDestroy() {
this.subscriptions.forEach(s => s.unsubscribe());
}
@Input()
set hasFeature(value: string | undefined) {
if (this._flag !== value) {
this._flag = value;
this.update();
}
}
@Input()
set hasFeatureOrMod(value: boolean) {
if (this._orMod !== value) {
this._orMod = value;
this.update();
}
}
@Input()
set hasFeatureAlso(value: boolean) {
if (this._alsoIf !== value) {
this._alsoIf = value;
this.update();
}
}
private update() {
const show = this._alsoIf && (hasFeatureFlag(this._flag as any) || (this._orMod && this.model.isMod));
private subscriptions: Subscription[] = [];
private showing = false;
private _flag: string | undefined = undefined;
private _orMod = false;
private _alsoIf = true;
private ref?: EmbeddedViewRef<any>;
constructor(private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef, private model: Model) {
}
ngAfterViewInit() {
this.subscriptions.push(featureFlagsChanged.subscribe(() => this.update()));
this.subscriptions.push(this.model.accountChanged.subscribe(() => this.update()));
}
ngOnDestroy() {
this.subscriptions.forEach(s => s.unsubscribe());
}
@Input()
set hasFeature(value: string | undefined) {
if (this._flag !== value) {
this._flag = value;
this.update();
}
}
@Input()
set hasFeatureOrMod(value: boolean) {
if (this._orMod !== value) {
this._orMod = value;
this.update();
}
}
@Input()
set hasFeatureAlso(value: boolean) {
if (this._alsoIf !== value) {
this._alsoIf = value;
this.update();
}
}
private update() {
const show = this._alsoIf && (hasFeatureFlag(this._flag as any) || (this._orMod && this.model.isMod));
if (this.showing !== show) {
this.showing = show;
if (this.showing !== show) {
this.showing = show;
if (show) {
this.ref = this.ref || this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
this.ref = undefined;
}
}
}
if (show) {
this.ref = this.ref || this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
this.ref = undefined;
}
}
}
}
@@ -3,19 +3,19 @@ import { uniqueId } from 'lodash';
import { findParentElement } from '../../../client/htmlUtils';
@Directive({
selector: '[labelledBy]',
selector: '[labelledBy]',
})
export class LabelledBy implements OnInit {
@Input('labelledBy') selector!: string;
constructor(private element: ElementRef) {
}
ngOnInit() {
const element = this.element.nativeElement as HTMLElement;
const target = findParentElement(element, this.selector);
const id = element.id = element.id || uniqueId('labelled-by-');
@Input('labelledBy') selector!: string;
constructor(private element: ElementRef) {
}
ngOnInit() {
const element = this.element.nativeElement as HTMLElement;
const target = findParentElement(element, this.selector);
const id = element.id = element.id || uniqueId('labelled-by-');
if (target) {
target.setAttribute('aria-labelledby', id);
}
}
if (target) {
target.setAttribute('aria-labelledby', id);
}
}
}
@@ -2,13 +2,13 @@ import { Directive, HostBinding } from '@angular/core';
import { RouterLinkActive } from '@angular/router';
@Directive({
selector: '[linkCurrent]',
selector: '[linkCurrent]',
})
export class LinkCurrent {
constructor(private routerLinkActive: RouterLinkActive) {
}
@HostBinding('attr.aria-current')
get current() {
return this.routerLinkActive.isActive ? 'true' : undefined;
}
constructor(private routerLinkActive: RouterLinkActive) {
}
@HostBinding('attr.aria-current')
get current() {
return this.routerLinkActive.isActive ? 'true' : undefined;
}
}
@@ -2,11 +2,11 @@ import { Directive, Input, HostBinding } from '@angular/core';
import { getUrl } from '../../../client/rev';
@Directive({
selector: '[revSrc]',
selector: '[revSrc]',
})
export class RevSrc {
@HostBinding() get src() {
return this.revSrc && getUrl(this.revSrc);
}
@Input() revSrc?: string;
@HostBinding() get src() {
return this.revSrc && getUrl(this.revSrc);
}
@Input() revSrc?: string;
}
@@ -4,23 +4,23 @@ import { Tabset } from '../tabset/tabset';
import { StorageService } from '../../services/storageService';
@Directive({
selector: '[saveActiveTab]',
selector: '[saveActiveTab]',
})
export class SaveActiveTab implements OnInit, OnDestroy {
@Input('saveActiveTab') key!: string;
private subscription?: Subscription;
constructor(@Host() private tabset: Tabset, private storage: StorageService) {
}
ngOnInit() {
// this.tabset.activeIndex = parseInt(this.storage.getItem(this.key) || '0', 10);
this.tabset.select(parseInt(this.storage.getItem(this.key) || '0', 10));
this.subscription = this.tabset.activeIndexChange.subscribe((i: number) => {
this.storage.setItem(this.key, i.toString());
});
}
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
@Input('saveActiveTab') key!: string;
private subscription?: Subscription;
constructor(@Host() private tabset: Tabset, private storage: StorageService) {
}
ngOnInit() {
// this.tabset.activeIndex = parseInt(this.storage.getItem(this.key) || '0', 10);
this.tabset.select(parseInt(this.storage.getItem(this.key) || '0', 10));
this.subscription = this.tabset.activeIndexChange.subscribe((i: number) => {
this.storage.setItem(this.key, i.toString());
});
}
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
}
+66 -66
View File
@@ -5,76 +5,76 @@ import { font } from '../../../client/fonts';
import { getCharacterSprite } from '../../../graphics/spriteFont';
@Component({
selector: 'emote-box',
template: '<img #image class="emote-box pixelart" />',
styles: ['.emote-box { pointer-events: none; }'],
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'emote-box',
template: '<img #image class="emote-box pixelart" />',
styles: ['.emote-box { pointer-events: none; }'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class EmoteBox implements AfterViewInit {
@ViewChild('image', { static: true }) image!: ElementRef;
private emoteValue = '';
private scaleValue = 2;
private initialized = false;
constructor(private zone: NgZone) {
}
ngAfterViewInit() {
loadAndInitSpriteSheets()
.then(() => {
this.initialized = true;
this.zone.runOutsideAngular(() => this.redraw());
});
}
get emote() {
return this.emoteValue;
}
@Input()
set emote(value: string) {
if (this.emoteValue !== value) {
this.emoteValue = value;
this.zone.runOutsideAngular(() => this.redraw());
}
}
get scale() {
return this.scaleValue;
}
@Input()
set scale(value: number) {
if (this.scaleValue !== value) {
this.scaleValue = value;
this.zone.runOutsideAngular(() => this.redraw());
}
}
redraw() {
if (this.initialized) {
const emote = findEmoji(this.emote);
const sprite = font && emote && getCharacterSprite(emote.symbol, font);
const image = this.image.nativeElement as HTMLImageElement;
@ViewChild('image', { static: true }) image!: ElementRef;
private emoteValue = '';
private scaleValue = 2;
private initialized = false;
constructor(private zone: NgZone) {
}
ngAfterViewInit() {
loadAndInitSpriteSheets()
.then(() => {
this.initialized = true;
this.zone.runOutsideAngular(() => this.redraw());
});
}
get emote() {
return this.emoteValue;
}
@Input()
set emote(value: string) {
if (this.emoteValue !== value) {
this.emoteValue = value;
this.zone.runOutsideAngular(() => this.redraw());
}
}
get scale() {
return this.scaleValue;
}
@Input()
set scale(value: number) {
if (this.scaleValue !== value) {
this.scaleValue = value;
this.zone.runOutsideAngular(() => this.redraw());
}
}
redraw() {
if (this.initialized) {
const emote = findEmoji(this.emote);
const sprite = font && emote && getCharacterSprite(emote.symbol, font);
const image = this.image.nativeElement as HTMLImageElement;
if (sprite) {
const width = sprite.w + sprite.ox;
const height = 10; // sprite.h + sprite.oy;
if (sprite) {
const width = sprite.w + sprite.ox;
const height = 10; // sprite.h + sprite.oy;
image.style.width = `${width * this.scale}px`;
image.style.height = `${height * this.scale}px`;
image.style.marginTop = `${-this.scale}px`;
image.style.display = 'inline-block';
image.style.visibility = 'hidden';
image.style.width = `${width * this.scale}px`;
image.style.height = `${height * this.scale}px`;
image.style.marginTop = `${-this.scale}px`;
image.style.display = 'inline-block';
image.style.visibility = 'hidden';
if (emote) {
image.setAttribute('aria-label', emote.names[0]);
}
if (emote) {
image.setAttribute('aria-label', emote.names[0]);
}
getEmojiImageAsync(sprite, src => {
image.src = src;
image.alt = emote ? emote.symbol : '';
image.style.visibility = 'visible';
});
} else {
image.style.width = `0px`;
image.style.height = `0px`;
image.src = '';
image.alt = '';
}
}
}
getEmojiImageAsync(sprite, src => {
image.src = src;
image.alt = emote ? emote.symbol : '';
image.style.visibility = 'visible';
});
} else {
image.style.width = `0px`;
image.style.height = `0px`;
image.src = '';
image.alt = '';
}
}
}
}
@@ -2,46 +2,46 @@ import { Component, Input, Output, EventEmitter } from '@angular/core';
import { faLock } from '../../../client/icons';
@Component({
selector: 'fill-outline',
templateUrl: 'fill-outline.pug',
styleUrls: ['fill-outline.scss'],
selector: 'fill-outline',
templateUrl: 'fill-outline.pug',
styleUrls: ['fill-outline.scss'],
})
export class FillOutline {
readonly lockIcon = faLock;
@Input() label = 'Color';
@Input() indicatorColor = '';
@Input() base?: string;
@Input() fill?: string;
@Output() fillChange = new EventEmitter<string>();
@Input() outline?: string;
@Output() outlineChange = new EventEmitter<string>();
@Input() locked?: boolean;
@Output() lockedChange = new EventEmitter<boolean>();
@Input() nonLockable = false;
@Input() outlineLocked = false;
@Output() outlineLockedChange = new EventEmitter<boolean>();
@Input() outlineHidden = false;
@Output() change = new EventEmitter<void>();
get hasLock() {
return this.locked !== undefined;
}
onChange() {
this.change.emit();
}
onFillChange(value: string) {
this.fillChange.emit(value);
this.onChange();
}
onOutlineChange(value: string) {
this.outlineChange.emit(value);
this.onChange();
}
onLockedChange(value: boolean) {
this.lockedChange.emit(value);
this.onChange();
}
onOutlineLockedChange(value: boolean) {
this.outlineLockedChange.emit(value);
this.onChange();
}
readonly lockIcon = faLock;
@Input() label = 'Color';
@Input() indicatorColor = '';
@Input() base?: string;
@Input() fill?: string;
@Output() fillChange = new EventEmitter<string>();
@Input() outline?: string;
@Output() outlineChange = new EventEmitter<string>();
@Input() locked?: boolean;
@Output() lockedChange = new EventEmitter<boolean>();
@Input() nonLockable = false;
@Input() outlineLocked = false;
@Output() outlineLockedChange = new EventEmitter<boolean>();
@Input() outlineHidden = false;
@Output() change = new EventEmitter<void>();
get hasLock() {
return this.locked !== undefined;
}
onChange() {
this.change.emit();
}
onFillChange(value: string) {
this.fillChange.emit(value);
this.onChange();
}
onOutlineChange(value: string) {
this.outlineChange.emit(value);
this.onChange();
}
onLockedChange(value: boolean) {
this.lockedChange.emit(value);
this.onChange();
}
onOutlineLockedChange(value: boolean) {
this.outlineLockedChange.emit(value);
this.onChange();
}
}
@@ -7,55 +7,55 @@ import { removeItem } from '../../../common/utils';
import { SettingsService } from '../../services/settingsService';
@Component({
selector: 'friends-box',
templateUrl: 'friends-box.pug',
styleUrls: ['friends-box.scss'],
selector: 'friends-box',
templateUrl: 'friends-box.pug',
styleUrls: ['friends-box.scss'],
})
export class FriendsBox {
readonly friendsIcon = faUserFriends;
readonly cogIcon = faCog;
readonly addToPartyIcon = faUserPlus;
readonly userOptionsIcon = faUserCog;
readonly statusIcon = faCircle;
@Output() sendMessage = new EventEmitter<Friend>();
removing?: Friend;
constructor(private settings: SettingsService, private model: Model, private game: PonyTownGame) {
}
get friends() {
return this.model.friends;
}
get hidden() {
return !!this.settings.account.hidden;
}
toggleHidden() {
this.settings.account.hidden = !this.settings.account.hidden;
this.settings.saveAccountSettings(this.settings.account);
}
toggle() {
this.removing = undefined;
}
sendMessageTo(friend: Friend) {
this.sendMessage.emit(friend);
}
inviteToParty(friend: Friend) {
this.game.send(server => server.playerAction(friend.entityId, PlayerAction.InviteToParty, undefined));
}
remove(friend: Friend) {
this.removing = friend;
}
cancelRemove() {
this.removing = undefined;
}
confirmRemove() {
if (this.removing && this.model.friends) {
const { accountId } = this.removing;
this.game.send(server => server.actionParam(Action.RemoveFriend, accountId));
removeItem(this.model.friends, this.removing);
this.removing = undefined;
}
}
setStatus(status: string) {
this.settings.account.hidden = status === 'invisible';
this.settings.saveAccountSettings(this.settings.account);
}
readonly friendsIcon = faUserFriends;
readonly cogIcon = faCog;
readonly addToPartyIcon = faUserPlus;
readonly userOptionsIcon = faUserCog;
readonly statusIcon = faCircle;
@Output() sendMessage = new EventEmitter<Friend>();
removing?: Friend;
constructor(private settings: SettingsService, private model: Model, private game: PonyTownGame) {
}
get friends() {
return this.model.friends;
}
get hidden() {
return !!this.settings.account.hidden;
}
toggleHidden() {
this.settings.account.hidden = !this.settings.account.hidden;
this.settings.saveAccountSettings(this.settings.account);
}
toggle() {
this.removing = undefined;
}
sendMessageTo(friend: Friend) {
this.sendMessage.emit(friend);
}
inviteToParty(friend: Friend) {
this.game.send(server => server.playerAction(friend.entityId, PlayerAction.InviteToParty, undefined));
}
remove(friend: Friend) {
this.removing = friend;
}
cancelRemove() {
this.removing = undefined;
}
confirmRemove() {
if (this.removing && this.model.friends) {
const { accountId } = this.removing;
this.game.send(server => server.actionParam(Action.RemoveFriend, accountId));
removeItem(this.model.friends, this.removing);
this.removing = undefined;
}
}
setStatus(status: string) {
this.settings.account.hidden = status === 'invisible';
this.settings.saveAccountSettings(this.settings.account);
}
}
@@ -4,24 +4,24 @@ import { InstallService } from '../../services/installService';
import { isMobile } from '../../../client/data';
@Component({
selector: 'install-button',
templateUrl: 'install-button.pug',
styleUrls: ['install-button.scss'],
selector: 'install-button',
templateUrl: 'install-button.pug',
styleUrls: ['install-button.scss'],
})
export class InstallButton {
readonly closeIcon = faTimes;
constructor(private installService: InstallService) {
}
get canInstall() {
return this.installService.canInstall;
}
get isMobile() {
return isMobile;
}
install() {
this.installService.install();
}
dismiss() {
this.installService.dismiss();
}
readonly closeIcon = faTimes;
constructor(private installService: InstallService) {
}
get canInstall() {
return this.installService.canInstall;
}
get isMobile() {
return isMobile;
}
install() {
this.installService.install();
}
dismiss() {
this.installService.dismiss();
}
}
@@ -7,26 +7,26 @@ import { removeItem } from '../../../common/utils';
import { Model } from '../../services/model';
@Component({
selector: 'invites-modal',
templateUrl: 'invites-modal.pug',
selector: 'invites-modal',
templateUrl: 'invites-modal.pug',
})
export class InvitesModal implements OnInit {
@Output() close = new EventEmitter();
invites: (SupporterInvite & { pony: PalettePonyInfo; })[] = [];
error?: string;
constructor(private model: Model, private game: PonyTownGame) {
}
get inviteLimit() {
return this.model.supporterInviteLimit;
}
ngOnInit() {
this.game.send(server => server.getInvites())!
.then(invites => invites.map(i => ({ ...i, pony: toPalette(decompressPonyString(i.info)) })))
.then(invites => this.invites = invites);
}
remove(invite: SupporterInvite) {
this.error = undefined;
this.game.send(server => server.actionParam(Action.CancelSupporterInvite, invite.id));
removeItem(this.invites, invite);
}
@Output() close = new EventEmitter();
invites: (SupporterInvite & { pony: PalettePonyInfo; })[] = [];
error?: string;
constructor(private model: Model, private game: PonyTownGame) {
}
get inviteLimit() {
return this.model.supporterInviteLimit;
}
ngOnInit() {
this.game.send(server => server.getInvites())!
.then(invites => invites.map(i => ({ ...i, pony: toPalette(decompressPonyString(i.info)) })))
.then(invites => this.invites = invites);
}
remove(invite: SupporterInvite) {
this.error = undefined;
this.game.send(server => server.actionParam(Action.CancelSupporterInvite, invite.id));
removeItem(this.invites, invite);
}
}
+3 -3
View File
@@ -1,9 +1,9 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'kbd-key',
templateUrl: 'kbd-key.pug',
selector: 'kbd-key',
templateUrl: 'kbd-key.pug',
})
export class KbdKey {
@Input() title?: string;
@Input() title?: string;
}
+47 -47
View File
@@ -9,53 +9,53 @@ import { SettingsService } from '../../services/settingsService';
import { REQUEST_DATE_OF_BIRTH } from '../../../common/constants';
@Component({
selector: 'menu-bar',
templateUrl: 'menu-bar.pug',
styleUrls: ['menu-bar.scss'],
selector: 'menu-bar',
templateUrl: 'menu-bar.pug',
styleUrls: ['menu-bar.scss'],
})
export class MenuBar {
readonly signUpProviders = signUpProviders;
readonly signInProviders = signInProviders;
readonly starIcon = faStar;
readonly spinnerIcon = faSpinner;
readonly userIcon = faUser;
readonly alertIcon = faExclamationCircle;
readonly cogIcon = faCog;
readonly statusIcon = faCircle;
@Input() logo = false;
@Input() loading = false;
@Input() loadingError = false;
@Input() account?: AccountData;
@Output() signOut = new EventEmitter();
@Output() signIn = new EventEmitter<OAuthProvider>();
constructor(private model: Model, private settings: SettingsService) {
}
get hasSupporterIcon() {
return isSupporterOrPastSupporter(this.account);
}
get supporterTitle() {
return supporterTitle(this.account);
}
get supporterClass() {
return supporterClass(this.account);
}
get showAccountAlert() {
return this.model.missingBirthdate && REQUEST_DATE_OF_BIRTH;
}
get hidden() {
return !!this.settings.account.hidden;
}
icon(id: string) {
return getProviderIcon(id);
}
signInTo(provider: OAuthProvider) {
this.signIn.emit(provider);
}
@HostListener('window:resize')
resize() {
}
setStatus(status: string) {
this.settings.account.hidden = status === 'invisible';
this.settings.saveAccountSettings(this.settings.account);
}
readonly signUpProviders = signUpProviders;
readonly signInProviders = signInProviders;
readonly starIcon = faStar;
readonly spinnerIcon = faSpinner;
readonly userIcon = faUser;
readonly alertIcon = faExclamationCircle;
readonly cogIcon = faCog;
readonly statusIcon = faCircle;
@Input() logo = false;
@Input() loading = false;
@Input() loadingError = false;
@Input() account?: AccountData;
@Output() signOut = new EventEmitter();
@Output() signIn = new EventEmitter<OAuthProvider>();
constructor(private model: Model, private settings: SettingsService) {
}
get hasSupporterIcon() {
return isSupporterOrPastSupporter(this.account);
}
get supporterTitle() {
return supporterTitle(this.account);
}
get supporterClass() {
return supporterClass(this.account);
}
get showAccountAlert() {
return this.model.missingBirthdate && REQUEST_DATE_OF_BIRTH;
}
get hidden() {
return !!this.settings.account.hidden;
}
icon(id: string) {
return getProviderIcon(id);
}
signInTo(provider: OAuthProvider) {
this.signIn.emit(provider);
}
@HostListener('window:resize')
resize() {
}
setStatus(status: string) {
this.settings.account.hidden = status === 'invisible';
this.settings.saveAccountSettings(this.settings.account);
}
}
@@ -2,12 +2,12 @@ import { Component, Input } from '@angular/core';
import { emptyIcon } from '../../../client/icons';
@Component({
selector: 'menu-item',
templateUrl: 'menu-item.pug',
styleUrls: ['menu-item.scss'],
selector: 'menu-item',
templateUrl: 'menu-item.pug',
styleUrls: ['menu-item.scss'],
})
export class MenuItem {
@Input() route: any;
@Input() name?: string;
@Input() icon = emptyIcon;
@Input() route: any;
@Input() name?: string;
@Input() icon = emptyIcon;
}
+83 -83
View File
@@ -9,89 +9,89 @@ const ageLabels = ['', 'M', 'A', '', '', '[M]', '[A]'];
const ageTitles = ['Not set', 'Minor', 'Adult', '', '', 'Minor (locked)', 'Adult (locked)'];
@Component({
selector: 'mod-box',
templateUrl: 'mod-box.pug',
styleUrls: ['mod-box.scss'],
selector: 'mod-box',
templateUrl: 'mod-box.pug',
styleUrls: ['mod-box.scss'],
})
export class ModBox implements OnDestroy {
readonly flagIcon = faFlag;
readonly noteIcon = faStickyNote;
readonly muteIcon = faMicrophoneSlash;
readonly hideIcon = faEyeSlash;
readonly moreIcon = faUserCog;
readonly dangerIcon = faExclamationCircle;
readonly timeouts = TIMEOUTS;
@Input() pony!: Pony;
isNoteOpen = false;
constructor(private model: Model, private game: PonyTownGame) {
}
get ageLabel() {
return ageLabels[this.modInfo && this.modInfo.age || 0];
}
get ageTitle() {
return ageTitles[this.modInfo && this.modInfo.age || 0];
}
get modInfo() {
return this.pony.modInfo;
}
get account() {
return this.modInfo && this.modInfo.account;
}
get country() {
return this.modInfo && this.modInfo.country;
}
get mute() {
return this.modInfo && this.modInfo.mute;
}
get muteTooltip() {
return this.mute ? (this.mute === 'perma' ? 'Permanently Muted' : `Muted for ${this.mute}`) : 'Mute';
}
get shadow() {
return this.modInfo && this.modInfo.shadow;
}
get shadowTooltip() {
return this.shadow ? (this.shadow === 'perma' ? 'Permanently Shadowed' : `Shadowed for ${this.shadow}`) : 'Shadow';
}
get counters() {
return this.modInfo && this.modInfo.counters;
}
get hasCounters() {
const counters = this.counters;
return counters && (counters.spam || counters.swears || counters.timeouts);
}
get check() {
return this.model.modCheck;
}
get note() {
return this.modInfo && this.modInfo.note;
}
set note(value) {
if (this.modInfo) {
this.modInfo.note = value;
}
}
ngOnDestroy() {
if (this.isNoteOpen) {
this.blur();
}
}
className(value: string) {
return value ? (value === 'perma' ? 'btn-danger' : 'btn-warning') : 'btn-default';
}
report() {
this.modAction(ModAction.Report);
}
setMute(value: number) {
this.modAction(ModAction.Mute, value);
}
setShadow(value: number) {
this.modAction(ModAction.Shadow, value);
}
blur() {
this.game.send(server => server.setNote(this.pony.id, this.modInfo && this.modInfo.note || ''));
this.isNoteOpen = false;
}
modAction(type: ModAction, param = 0) {
return this.game.send(server => server.otherAction(this.pony.id, type, param));
}
readonly flagIcon = faFlag;
readonly noteIcon = faStickyNote;
readonly muteIcon = faMicrophoneSlash;
readonly hideIcon = faEyeSlash;
readonly moreIcon = faUserCog;
readonly dangerIcon = faExclamationCircle;
readonly timeouts = TIMEOUTS;
@Input() pony!: Pony;
isNoteOpen = false;
constructor(private model: Model, private game: PonyTownGame) {
}
get ageLabel() {
return ageLabels[this.modInfo && this.modInfo.age || 0];
}
get ageTitle() {
return ageTitles[this.modInfo && this.modInfo.age || 0];
}
get modInfo() {
return this.pony.modInfo;
}
get account() {
return this.modInfo && this.modInfo.account;
}
get country() {
return this.modInfo && this.modInfo.country;
}
get mute() {
return this.modInfo && this.modInfo.mute;
}
get muteTooltip() {
return this.mute ? (this.mute === 'perma' ? 'Permanently Muted' : `Muted for ${this.mute}`) : 'Mute';
}
get shadow() {
return this.modInfo && this.modInfo.shadow;
}
get shadowTooltip() {
return this.shadow ? (this.shadow === 'perma' ? 'Permanently Shadowed' : `Shadowed for ${this.shadow}`) : 'Shadow';
}
get counters() {
return this.modInfo && this.modInfo.counters;
}
get hasCounters() {
const counters = this.counters;
return counters && (counters.spam || counters.swears || counters.timeouts);
}
get check() {
return this.model.modCheck;
}
get note() {
return this.modInfo && this.modInfo.note;
}
set note(value) {
if (this.modInfo) {
this.modInfo.note = value;
}
}
ngOnDestroy() {
if (this.isNoteOpen) {
this.blur();
}
}
className(value: string) {
return value ? (value === 'perma' ? 'btn-danger' : 'btn-warning') : 'btn-default';
}
report() {
this.modAction(ModAction.Report);
}
setMute(value: number) {
this.modAction(ModAction.Mute, value);
}
setShadow(value: number) {
this.modAction(ModAction.Shadow, value);
}
blur() {
this.game.send(server => server.setNote(this.pony.id, this.modInfo && this.modInfo.note || ''));
this.isNoteOpen = false;
}
modAction(type: ModAction, param = 0) {
return this.game.send(server => server.otherAction(this.pony.id, type, param));
}
}

Some files were not shown because too many files have changed in this diff Show More