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()) }); } } }