diff --git a/package-lock.json b/package-lock.json
index f434d8b..484115c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3187,7 +3187,8 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"jsonfile": {
"version": "4.0.0",
@@ -3285,6 +3286,7 @@
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
"dev": true,
+ "optional": true,
"requires": {
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^4.0.0"
@@ -7531,7 +7533,8 @@
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz",
"integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==",
- "dev": true
+ "dev": true,
+ "optional": true
},
"natives": {
"version": "1.1.6",
@@ -8886,6 +8889,39 @@
}
}
},
+ "react-countdown": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/react-countdown/-/react-countdown-2.2.1.tgz",
+ "integrity": "sha512-e8dUUhlysDqgci32VOOe0uDfeDMaiyyFNrWHdmMky5fithYDt4iOJa22EF96VbkU64R4D+Bww4AbLpqA/J4dww==",
+ "requires": {
+ "prop-types": "^15.7.2"
+ },
+ "dependencies": {
+ "loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "requires": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ }
+ },
+ "prop-types": {
+ "version": "15.7.2",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
+ "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
+ "requires": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.8.1"
+ }
+ },
+ "react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ }
+ }
+ },
"react-display-name": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/react-display-name/-/react-display-name-0.2.4.tgz",
diff --git a/package.json b/package.json
index b22d9d4..d2beb09 100644
--- a/package.json
+++ b/package.json
@@ -58,6 +58,7 @@
"electron-store": "^1.3.0",
"material-ui": "^0.20.2",
"react": "^16.7.0",
+ "react-countdown": "^2.2.1",
"react-dnd": "^7.0.2",
"react-dom": "^16.7.0",
"react-router-dom": "^4.3.1",
diff --git a/src/components/skillbrowser/CharacterSelector.jsx b/src/components/skillbrowser/CharacterSelector.jsx
index b0e14a0..68ee9da 100644
--- a/src/components/skillbrowser/CharacterSelector.jsx
+++ b/src/components/skillbrowser/CharacterSelector.jsx
@@ -19,7 +19,10 @@ export default class CharacterSelector extends React.Component {
Object.keys(allChars).forEach((char) => {
this.chars.push();
});
- this.chars.sort((a, b) => a.props.primaryText.localeCompare(b.props.primaryText));
+
+ // Make sure characters with numbers in names get sorted correctly ([1, 2, 11], not [1, 11, 2])
+ var collator = new Intl.Collator(undefined, {numeric: true, sensitivity: "base"});
+ this.chars.sort((a, b) => collator.compare(a.props.primaryText, b.props.primaryText));
}
handleCharacterChange(event, index, value) {
diff --git a/src/components/tables/CharactersOverviewTable.jsx b/src/components/tables/CharactersOverviewTable.jsx
index cb06eca..1f63abd 100644
--- a/src/components/tables/CharactersOverviewTable.jsx
+++ b/src/components/tables/CharactersOverviewTable.jsx
@@ -4,7 +4,7 @@ import React from 'react';
import {Redirect} from 'react-router';
import Character from '../../models/Character';
-import DateTimeHelper from '../../helpers/DateTimeHelper';
+import EveCountdownTimer from '../widgets/EveCountdownTimer';
import Avatar from 'material-ui/Avatar';
import {Table, TableBody, TableRow, TableRowColumn} from 'material-ui/Table';
@@ -24,32 +24,18 @@ export default class CharactersOverviewTable extends React.Component {
super(props);
this.state = {
characters: Object.values(Character.getAll()).sort((a, b) => b.getTotalSp() - a.getTotalSp()),
- ticking: true,
redirectPath: undefined
};
}
componentDidMount() {
- this.timerId = setInterval(
- () => this.tick(),
- 1000
- );
-
this.subscriberId = Character.subscribe(this);
}
componentWillUnmount() {
- clearInterval(this.timerId);
-
Character.unsubscribe(this.subscriberId);
}
- tick() {
- if (this.state.ticking) {
- this.forceUpdate();
- }
- }
-
handleClick(e, characterId) {
let path = '/characters/' + characterId;
@@ -116,7 +102,7 @@ export default class CharactersOverviewTable extends React.Component {
{currentSkill !== undefined ? `${currentSkill.skill_name} ${currentSkill.finished_level}` : "Not Training"}
- {currentSkill !== undefined ? DateTimeHelper.timeUntil(new Date(currentSkill.finish_date)) : ""}
+ {currentSkill !== undefined ? : ""}
);
diff --git a/src/components/tables/SpFarmingTable.jsx b/src/components/tables/SpFarmingTable.jsx
index f9e7808..e477889 100644
--- a/src/components/tables/SpFarmingTable.jsx
+++ b/src/components/tables/SpFarmingTable.jsx
@@ -6,7 +6,7 @@ import {Redirect} from 'react-router';
import Character from '../../models/Character';
import FarmCharacter from '../../models/FarmCharacter';
import FarmHelper from '../../helpers/FarmHelper';
-import DateTimeHelper from '../../helpers/DateTimeHelper';
+import EveCountdownTimer from '../widgets/EveCountdownTimer';
import Avatar from 'material-ui/Avatar';
import {Table, TableHeader, TableHeaderColumn, TableBody, TableRow, TableRowColumn} from 'material-ui/Table';
@@ -28,33 +28,34 @@ export default class SpFarmingTable extends React.Component {
constructor(props) {
super(props);
this.state = {
- characters: FarmCharacter.getAll(),
- ticking: true,
+ characters: Object.values(FarmCharacter.getAll()).sort((a, b) => {
+ var charA = Character.get(a.id);
+ var charB = Character.get(b.id);
+
+ var countSort = charB.getInjectorsReady(b.baseSp) - charA.getInjectorsReady(a.baseSp); // Descending
+
+ const MAX_DATE = new Date(8640000000000000);
+ var nextCharB = charB.getNextInjectorDate(b.baseSp) || MAX_DATE;
+ var nextCharA = charA.getNextInjectorDate(a.baseSp) || MAX_DATE;
+ var timeSort = nextCharA - nextCharB; // Ascending
+
+ return countSort || timeSort || MAX_DATE;
+ }),
+ injectorsReady: Object.values(FarmCharacter.getAll()).reduce((count, char) => {
+ return count + (Character.get(char.id).getInjectorsReady(char.baseSp));
+ }, 0),
redirectPath: undefined
};
}
componentDidMount() {
- this.timerId = setInterval(
- () => this.tick(),
- 1000
- );
-
this.subscriberId = FarmCharacter.subscribe(this);
}
componentWillUnmount() {
- clearInterval(this.timerId);
-
FarmCharacter.unsubscribe(this.subscriberId);
}
- tick() {
- if (this.state.ticking) {
- this.forceUpdate();
- }
- }
-
handleClick(e, characterId) {
let path = '/characters/' + characterId;
@@ -67,7 +68,16 @@ export default class SpFarmingTable extends React.Component {
FarmHelper.deleteFarm(characterId);
this.forceUpdate();
- };
+ }
+
+ renderSpHour(char)
+ {
+ const spHour = char.getCurrentSpPerHour();
+ const maxSpHour = char.getMaxSpPerHour();
+ const spWarning = `Not training at max speed! Could be ${maxSpHour.toLocaleString()} SP/hour`;
+
+ return ({spHour < maxSpHour && ⚠ }{spHour.toLocaleString()});
+ }
render() {
if (this.state.redirectPath !== undefined) {
@@ -89,7 +99,7 @@ export default class SpFarmingTable extends React.Component {
Total SP
- Injectors Ready
+ {this.state.injectorsReady} Injectors Ready
Time Until Next Injector
@@ -139,12 +149,12 @@ export default class SpFarmingTable extends React.Component {
{char.getInjectorsReady(farmChar.baseSp)}
- {DateTimeHelper.timeUntil(char.getNextInjectorDate(farmChar.baseSp))}
+ {}
- {currentSkill !== undefined ? char.getCurrentSpPerHour() : "Not Training"}
- {currentSkill !== undefined ? DateTimeHelper.timeUntil(new Date(char.getLastSkill().finish_date)) : ""}
+ {currentSkill !== undefined ? this.renderSpHour(char) : "Not Training"}
+ {currentSkill !== undefined ? : ""}
diff --git a/src/components/widgets/EveCountdownTimer.jsx b/src/components/widgets/EveCountdownTimer.jsx
new file mode 100644
index 0000000..fea1d01
--- /dev/null
+++ b/src/components/widgets/EveCountdownTimer.jsx
@@ -0,0 +1,32 @@
+'use strict';
+
+import React from 'react';
+import Countdown from "react-countdown";
+
+
+import DateTimeHelper from '../../helpers/DateTimeHelper';
+
+
+export default class EveCountdownTimer extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ endDate: props.endDate || new Date(),
+ intervalDelay: props.intervalDelay || 4500,
+ };
+ }
+
+ renderEveTimer(totalMilliseconds, completed) {
+ return completed ? null : DateTimeHelper.niceCountdown(totalMilliseconds);
+ }
+
+ render() {
+ return (
+ this.renderEveTimer(total, completed)}
+ />
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/models/Character.js b/src/models/Character.js
index 9710978..ccd2bf3 100644
--- a/src/models/Character.js
+++ b/src/models/Character.js
@@ -44,7 +44,7 @@ class Character {
getCurrentSkill() {
const currentDate = new Date();
- for(let o of this.skillQueue) {
+ for (let o of this.skillQueue) {
if ((o.hasOwnProperty('finish_date')) && (new Date(o.finish_date) > currentDate)) {
return o;
}
@@ -73,7 +73,7 @@ class Character {
let lastDate = new Date();
let lastSkill = undefined;
- for(let o of this.skillQueue) {
+ for (let o of this.skillQueue) {
if ((o.hasOwnProperty('finish_date')) && (new Date(o.finish_date) > lastDate)) {
lastSkill = o;
lastDate = new Date(o.finish_date);
@@ -100,6 +100,11 @@ class Character {
return Math.round(this.getCurrentSpPerMillisecond() * 1000 * 3600);
}
+ getMaxSpPerHour() {
+ // Assuming best remap and Omega with +5s or Alpha with +3s
+ return this.isOmega() ? 2700 : 1260;
+ }
+
getInjectorsReady(baseSp) {
if (baseSp === undefined) {
baseSp = 5000000;
@@ -119,7 +124,7 @@ class Character {
const spNeeded = nextInjectorTotalSp - currentSp;
const millisecondsToTrainSp = spNeeded / this.getCurrentSpPerMillisecond();
- return new Date(new Date().getTime() + millisecondsToTrainSp);
+ return isFinite(millisecondsToTrainSp) ? new Date(new Date().getTime() + millisecondsToTrainSp) : undefined;
}
/**
@@ -149,7 +154,7 @@ class Character {
const finishedSkills = this.getFinishedSkillsInQueue();
const finishedSkillIds = finishedSkills.map(o => o.skill_id);
- for(let skill of this.skills) {
+ for (let skill of this.skills) {
if (skill.skill_id === currentSkill.skill_id) {
let startingMilliseconds = new Date(currentSkill.start_date).getTime();
let millisecondsPassed = new Date().getTime() - startingMilliseconds;
@@ -158,7 +163,7 @@ class Character {
} else if (finishedSkillIds.includes(skill.skill_id)) {
const queueEntries = finishedSkills.filter(o => o.skill_id === skill.skill_id);
- for(let queueEntry of queueEntries) {
+ for (let queueEntry of queueEntries) {
totalSp += (queueEntry.level_end_sp - queueEntry.training_start_sp);
}
@@ -185,8 +190,7 @@ class Character {
// if they have any skills with a higher active level than the maximum alpha level, must be omega
if (this.skills.find(o => o.active_skill_level > 0 &&
- (
- !alphaSkillSet.hasOwnProperty(o.skill_name) ||
+ (!alphaSkillSet.hasOwnProperty(o.skill_name) ||
o.active_skill_level > alphaSkillSet[o.skill_name]
)
) !== undefined) {
@@ -195,7 +199,7 @@ class Character {
// if they have any skills starting >24 hours in the future with a scheduled finish date, must be omega
if (this.skills.find(o =>
- (new Date(o.start_date).getTime()) > (new Date().getTime() + 24*60*60) &&
+ (new Date(o.start_date).getTime()) > (new Date().getTime() + 24 * 60 * 60) &&
o.hasOwnProperty('finish_date')
) !== undefined) {
return true;
@@ -272,7 +276,7 @@ class Character {
buildSkillTree() {
let groups = {};
- for(const skill of this.skills) {
+ for (const skill of this.skills) {
if (!groups.hasOwnProperty(skill.skill_group_name)) {
groups[skill.skill_group_name] = [];
}
@@ -281,7 +285,7 @@ class Character {
}
let groupsArray = [];
- for(const groupName in groups) {
+ for (const groupName in groups) {
if (groups.hasOwnProperty(groupName)) {
let skills = groups[groupName];
skills.sort((a, b) => a.skill_name.localeCompare(b.skill_name));
@@ -389,9 +393,9 @@ class Character {
const spRequirements = {};
spRequirements[0] = [0];
- for(const a of [1, 2, 3, 4, 5]) {
+ for (const a of[1, 2, 3, 4, 5]) {
spRequirements[a] = [];
- for(const b of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]) {
+ for (const b of[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]) {
const x = Math.round(250 * b * Math.pow(Math.sqrt(32), a - 1));
spRequirements[a].push(x);
spRequirements[a].push(x + 1);
@@ -456,7 +460,7 @@ class Character {
let implantIds = await client.get('characters/' + this.id + '/implants', 'v1');
this.implants = [];
for (let id of implantIds) {
- this.implants.push({id: id});
+ this.implants.push({ id: id });
}
let promises = this.implants.map((o) => {
@@ -506,7 +510,7 @@ class Character {
jumpClone.implants = [];
for (let id of jumpCloneData.implants) {
- jumpClone.implants.push({id: id});
+ jumpClone.implants.push({ id: id });
}
Array.prototype.push.apply(promises, jumpClone.implants.map((o) => {
@@ -635,7 +639,7 @@ class Character {
);
this.loyalty_points = [];
- for(let o of data) {
+ for (let o of data) {
if (o.loyalty_points > 0) {
o.corporation = await client.get('corporations/' + o.corporation_id, 'v4');
this.loyalty_points.push(o);
@@ -667,7 +671,7 @@ class Character {
let resolver = new BulkIdResolver();
this.contractSlotsUsed = 0;
- for(let contract of this.contracts) {
+ for (let contract of this.contracts) {
resolver.addId(contract.issuer_id);
resolver.addId(contract.issuer_corporation_id);
resolver.addId(contract.assignee_id);
@@ -686,7 +690,7 @@ class Character {
await resolver.resolve();
- for(let contract of this.contracts) {
+ for (let contract of this.contracts) {
contract.issuer = resolver.get(contract.issuer_id);
contract.issuer_corporation = resolver.get(contract.issuer_corporation_id);
contract.assignee = resolver.get(contract.assignee_id);
@@ -741,7 +745,7 @@ class Character {
this.save();
}
}
-
+
async refreshMailingLists() {
if (this.shouldRefresh('mailinglists')) {
const client = new EsiClient();
@@ -780,7 +784,7 @@ class Character {
const resolver = new BulkIdResolver();
for (const mail of this.mails) {
// "Welcome" mail from a mailing list, mailing lists will not resolve
- if (mail.recipients[0].recipient_type === 'mailing_list' && mail.recipients[0].recipient_id == mail.from){
+ if (mail.recipients[0].recipient_type === 'mailing_list' && mail.recipients[0].recipient_id == mail.from) {
mail.from_name = 'Welcome ML';
} else {
resolver.addId(mail.from);
@@ -852,7 +856,7 @@ class Character {
}
}
- mail.body = await MailBodyHelper.retrieveMailBody(mail.mail_id,client,this.id)
+ mail.body = await MailBodyHelper.retrieveMailBody(mail.mail_id, client, this.id)
}
this.markRefreshed('mails');
@@ -884,7 +888,7 @@ class Character {
* @param {string} reason - failure reason: 'scope', 'token', 'client', 'error'
* @param {boolean} temporary - if true, failure is expected to only be temporary
*/
- markTypeFailed(type, reason, temporary=false) {
+ markTypeFailed(type, reason, temporary = false) {
this.nextRefreshes[type] = {
last: new Date(),
do: temporary ? new Date(new Date().getTime() + (300 * 1000)) : undefined,
@@ -898,8 +902,8 @@ class Character {
* @param {string} reason - failure reason: 'scope', 'token', 'client', 'error'
* @param {boolean} temporary - if true, failure is expected to only be temporary
*/
- markFailed(reason, temporary=false) {
- for(const type in this.nextRefreshes) {
+ markFailed(reason, temporary = false) {
+ for (const type in this.nextRefreshes) {
if (this.nextRefreshes.hasOwnProperty(type)) {
this.markTypeFailed(type, reason, temporary);
}
@@ -935,7 +939,7 @@ class Character {
};
// TODO: clean this up jfc
- for(const key in translations) {
+ for (const key in translations) {
if ((this.nextRefreshes.hasOwnProperty(key)) && (translations.hasOwnProperty(key))) {
let las;
if (this.nextRefreshes[key].error === undefined) {
@@ -943,7 +947,7 @@ class Character {
las = (lastDate.getTime() + 5000 < new Date().getTime()) ?
DateTimeHelper.timeSince(lastDate) + " ago" : "Just now";
} else {
- switch(this.nextRefreshes[key].error) {
+ switch (this.nextRefreshes[key].error) {
case 'scope':
las = 'No Scope';
break;
@@ -1009,7 +1013,7 @@ class Character {
try {
await Promise.all(promises);
- } catch(err) {}
+ } catch (err) {}
Character.pushToSubscribers();
}
@@ -1022,10 +1026,10 @@ class Character {
let contracts = [];
let contractIds = [];
- for(const id in characters) {
+ for (const id in characters) {
if (characters.hasOwnProperty(id)) {
if (characters[id].hasOwnProperty('contracts') && characters[id].contracts !== undefined) {
- for(const contract of characters[id].contracts) {
+ for (const contract of characters[id].contracts) {
if (complete !== undefined) {
if ((complete === true) && (!appProperties.contract_completed_statuses.includes(contract.status))) {
continue;
@@ -1102,18 +1106,15 @@ class Character {
}
static suspendSubscribers() {
- for(let component of subscribedComponents) {
- if (component !== null) {
- component.setState({'ticking': false});
- }
+ for (let component of subscribedComponents) {
+ if (component !== null) {}
}
}
static pushToSubscribers() {
- for(let component of subscribedComponents) {
+ for (let component of subscribedComponents) {
if (component !== null) {
- component.setState({'characters': Object.values(Character.getAll()).sort((a, b) => b.getTotalSp() - a.getTotalSp())});
- component.setState({'ticking': true});
+ component.setState({ 'characters': Object.values(Character.getAll()).sort((a, b) => b.getTotalSp() - a.getTotalSp()) });
}
}
}