Cancel, Retry, and Warning download UI

This commit is contained in:
Geomitron
2020-02-11 20:50:51 -05:00
parent a98b03dcd4
commit db083d573a
13 changed files with 291 additions and 179 deletions

247
package-lock.json generated
View File

@@ -10,33 +10,33 @@
"integrity": "sha512-GLyWIFBbGvpKPGo55JyRZAo4lVbnBiD52cKlw/0Vt+wnmKvWJkpZvsjVoaIolyBXDeAQKSicRtqFNPem9w0WYA==" "integrity": "sha512-GLyWIFBbGvpKPGo55JyRZAo4lVbnBiD52cKlw/0Vt+wnmKvWJkpZvsjVoaIolyBXDeAQKSicRtqFNPem9w0WYA=="
}, },
"@angular-devkit/architect": { "@angular-devkit/architect": {
"version": "0.803.24", "version": "0.803.25",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.803.24.tgz", "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.803.25.tgz",
"integrity": "sha512-ONY/Ppzyvtb0tqgwnzQvlGlexb5nTyy58ljgL1aQLTO3cNTkpl4IQYUCTdvn61gGA+FWPAXMCCbNqOPZMsOZCQ==", "integrity": "sha512-usV/zEncKCKQuF6AD3pRU6N5i5fbaAux/qZb+nbOz9/2G5jrXwe5sH+y3vxbgqB83e3LqusEQCTu7/tfg6LwZg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@angular-devkit/core": "8.3.24", "@angular-devkit/core": "8.3.25",
"rxjs": "6.4.0" "rxjs": "6.4.0"
} }
}, },
"@angular-devkit/build-angular": { "@angular-devkit/build-angular": {
"version": "0.803.24", "version": "0.803.25",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-0.803.24.tgz", "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-0.803.25.tgz",
"integrity": "sha512-uA789spMVghXehwAhl5zK0loY/wfxblUiL+y21T24LMCJc15a9QX5dwbXH72ioHz7qdzb/agXk7AK+foc2/0Hw==", "integrity": "sha512-WY0E7NgXuog3phhz5ZdutZPWQ9nbOr+omGN5KI1e8MZs1sJO4xkyaGRT8zOulkogkqJ2NboTBq3j9uSbZkcYeg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@angular-devkit/architect": "0.803.24", "@angular-devkit/architect": "0.803.25",
"@angular-devkit/build-optimizer": "0.803.24", "@angular-devkit/build-optimizer": "0.803.25",
"@angular-devkit/build-webpack": "0.803.24", "@angular-devkit/build-webpack": "0.803.25",
"@angular-devkit/core": "8.3.24", "@angular-devkit/core": "8.3.25",
"@babel/core": "7.8.3", "@babel/core": "7.8.3",
"@babel/preset-env": "7.8.3", "@babel/preset-env": "7.8.3",
"@ngtools/webpack": "8.3.24", "@ngtools/webpack": "8.3.25",
"ajv": "6.10.2", "ajv": "6.10.2",
"autoprefixer": "9.6.1", "autoprefixer": "9.6.1",
"browserslist": "4.8.3", "browserslist": "4.8.6",
"cacache": "12.0.2", "cacache": "12.0.2",
"caniuse-lite": "1.0.30001019", "caniuse-lite": "1.0.30001024",
"circular-dependency-plugin": "5.2.0", "circular-dependency-plugin": "5.2.0",
"clean-css": "4.2.1", "clean-css": "4.2.1",
"copy-webpack-plugin": "5.1.1", "copy-webpack-plugin": "5.1.1",
@@ -81,12 +81,31 @@
"webpack-sources": "1.4.3", "webpack-sources": "1.4.3",
"webpack-subresource-integrity": "1.1.0-rc.6", "webpack-subresource-integrity": "1.1.0-rc.6",
"worker-plugin": "3.2.0" "worker-plugin": "3.2.0"
},
"dependencies": {
"browserslist": {
"version": "4.8.6",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.8.6.tgz",
"integrity": "sha512-ZHao85gf0eZ0ESxLfCp73GG9O/VTytYDIkIiZDlURppLTI9wErSM/5yAKEq6rcUdxBLjMELmrYUJGg5sxGKMHg==",
"dev": true,
"requires": {
"caniuse-lite": "^1.0.30001023",
"electron-to-chromium": "^1.3.341",
"node-releases": "^1.1.47"
}
},
"caniuse-lite": {
"version": "1.0.30001024",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001024.tgz",
"integrity": "sha512-LubRSEPpOlKlhZw9wGlLHo8ZVj6ugGU3xGUfLPneNBledSd9lIM5cCGZ9Mz/mMCJUhEt4jZpYteZNVRdJw5FRA==",
"dev": true
}
} }
}, },
"@angular-devkit/build-optimizer": { "@angular-devkit/build-optimizer": {
"version": "0.803.24", "version": "0.803.25",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.803.24.tgz", "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.803.25.tgz",
"integrity": "sha512-Z+d7M+WpBq7AWWRwbxzb1l9O9qkylxnDRKxXvq3Tzjn43g+2WyspE91dMyrg1ISc+p8jgX6xKSblRLvtWqpA8w==", "integrity": "sha512-MiQimuEs8QeM3xo7bR3Yk1OWHHlp2pGCc2GLUMIcWhKqM+QjoRky0HoGoBazbznx292l+xjFjANvPEKbqJ2v7Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"loader-utils": "1.2.3", "loader-utils": "1.2.3",
@@ -94,31 +113,23 @@
"tslib": "1.10.0", "tslib": "1.10.0",
"typescript": "3.5.3", "typescript": "3.5.3",
"webpack-sources": "1.4.3" "webpack-sources": "1.4.3"
},
"dependencies": {
"typescript": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz",
"integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==",
"dev": true
}
} }
}, },
"@angular-devkit/build-webpack": { "@angular-devkit/build-webpack": {
"version": "0.803.24", "version": "0.803.25",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.803.24.tgz", "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.803.25.tgz",
"integrity": "sha512-Bbd5KUGaE+edN0sp8K3azuqS/JTBmeWXIumdBEtqWyL6VsohX7fL+toJlSvRkj8lg02LVyozAFetXKnyaBkfCQ==", "integrity": "sha512-WR7HWJIWL6TB3WHG7ZFn8s0z3WlojeQlod75UIKl5i+f4OU90kp8kxcoH5G6OCXu56x5w40oIi1ve5ljjWSJkw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@angular-devkit/architect": "0.803.24", "@angular-devkit/architect": "0.803.25",
"@angular-devkit/core": "8.3.24", "@angular-devkit/core": "8.3.25",
"rxjs": "6.4.0" "rxjs": "6.4.0"
} }
}, },
"@angular-devkit/core": { "@angular-devkit/core": {
"version": "8.3.24", "version": "8.3.25",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-8.3.24.tgz", "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-8.3.25.tgz",
"integrity": "sha512-xpT5yg+ddGDnifryBv2sRSYtq5F3iZIS+lN/K2AhhEa50B7Z+QaCVlEzoV/IfrGd6sLArdnKYwjLHFZ0LElUuw==", "integrity": "sha512-l7Gqy1tMrTpRmPVlovcFX8UA3mtXRlgO8kcSsbJ9MKRKNTCcxlfsWEYY5igyDBUVh6ADkgSIu0nuk31ZGTe0lw==",
"dev": true, "dev": true,
"requires": { "requires": {
"ajv": "6.10.2", "ajv": "6.10.2",
@@ -129,12 +140,12 @@
} }
}, },
"@angular-devkit/schematics": { "@angular-devkit/schematics": {
"version": "8.3.24", "version": "8.3.25",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-8.3.24.tgz", "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-8.3.25.tgz",
"integrity": "sha512-HrwDCgw7i3GrNns0Ce5zStWkxBqlcLuDkMcLY6981jpvVzgXMIQ+YqDrJ2kD46xHh979ev7hhw1d6jwPXh85Xw==", "integrity": "sha512-/p1MkfursfLy+JRGXlJGPEmX55lrFCsR/2khWAVXZcMaFR3QlR/b6/zvB8I2pHFfr0/XWnYTT/BsF7rJjO3RmA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@angular-devkit/core": "8.3.24", "@angular-devkit/core": "8.3.25",
"rxjs": "6.4.0" "rxjs": "6.4.0"
} }
}, },
@@ -153,16 +164,16 @@
} }
}, },
"@angular/cli": { "@angular/cli": {
"version": "8.3.24", "version": "8.3.25",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-8.3.24.tgz", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-8.3.25.tgz",
"integrity": "sha512-cUB6H+BAISMdaFsstcyvC+17hOV3ET4MaVgcmgT2cL7A4vMBRBxJ0cW4r3D9c6e7m4wipyJzOUESYoIHu0cp4A==", "integrity": "sha512-CPJI5nnbBvvyBUFwOHfRXy/KVwsiYlcbDAeIk1klcjQjbVFYZbnY0iAhNupy9j7rPQhb7jle5oslU3TLfbqOTQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@angular-devkit/architect": "0.803.24", "@angular-devkit/architect": "0.803.25",
"@angular-devkit/core": "8.3.24", "@angular-devkit/core": "8.3.25",
"@angular-devkit/schematics": "8.3.24", "@angular-devkit/schematics": "8.3.25",
"@schematics/angular": "8.3.24", "@schematics/angular": "8.3.25",
"@schematics/update": "0.803.24", "@schematics/update": "0.803.25",
"@yarnpkg/lockfile": "1.1.0", "@yarnpkg/lockfile": "1.1.0",
"ansi-colors": "4.1.1", "ansi-colors": "4.1.1",
"debug": "^4.1.1", "debug": "^4.1.1",
@@ -1082,9 +1093,9 @@
} }
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001023", "version": "1.0.30001027",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001023.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001027.tgz",
"integrity": "sha512-C5TDMiYG11EOhVOA62W1p3UsJ2z4DsHtMBQtjzp3ZsUglcQn62WOUgW0y795c7A5uZ+GCEIvzkMatLIlAsbNTA==", "integrity": "sha512-7xvKeErvXZFtUItTHgNtLgS9RJpVnwBlWX8jSo/BO8VsF6deszemZSkJJJA1KOKrXuzZH4WALpAJdq5EyfgMLg==",
"dev": true "dev": true
}, },
"semver": { "semver": {
@@ -1216,9 +1227,9 @@
} }
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001023", "version": "1.0.30001027",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001023.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001027.tgz",
"integrity": "sha512-C5TDMiYG11EOhVOA62W1p3UsJ2z4DsHtMBQtjzp3ZsUglcQn62WOUgW0y795c7A5uZ+GCEIvzkMatLIlAsbNTA==", "integrity": "sha512-7xvKeErvXZFtUItTHgNtLgS9RJpVnwBlWX8jSo/BO8VsF6deszemZSkJJJA1KOKrXuzZH4WALpAJdq5EyfgMLg==",
"dev": true "dev": true
}, },
"semver": { "semver": {
@@ -2012,9 +2023,9 @@
} }
}, },
"@electron/get": { "@electron/get": {
"version": "1.7.2", "version": "1.7.6",
"resolved": "https://registry.npmjs.org/@electron/get/-/get-1.7.2.tgz", "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.7.6.tgz",
"integrity": "sha512-LSE4LZGMjGS9TloDx0yO44D2UTbaeKRk+QjlhWLiQlikV6J4spgDCjb6z4YIcqmPAwNzlNCnWF4dubytwI+ATA==", "integrity": "sha512-zlNikt6ziVLNcm4lly1L4y62fJd/eYpEBjF5DiV/VAQq2vdPjH4sbUphXt9upmHz86lAhAj8g9lTnWrxJ/KBZw==",
"dev": true, "dev": true,
"requires": { "requires": {
"debug": "^4.1.1", "debug": "^4.1.1",
@@ -2080,12 +2091,12 @@
"dev": true "dev": true
}, },
"@ngtools/webpack": { "@ngtools/webpack": {
"version": "8.3.24", "version": "8.3.25",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-8.3.24.tgz", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-8.3.25.tgz",
"integrity": "sha512-OpR7t/99qNOpADayCuM67agBVdYkdbFyEEcOLaDFYh3LsefHOSSxtAGv8M77e7dguvtaljHTiVkMxgcXFsZM0Q==", "integrity": "sha512-yHvgxXUXlgdWijtzcRjTaUqzK+6TVK/8p7PreBR00GsLxhl4U1jQSC6yDaZUCjOaEkiczFWl4hEuC4wTU/hLdg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@angular-devkit/core": "8.3.24", "@angular-devkit/core": "8.3.25",
"enhanced-resolve": "4.1.0", "enhanced-resolve": "4.1.0",
"rxjs": "6.4.0", "rxjs": "6.4.0",
"tree-kill": "1.2.2", "tree-kill": "1.2.2",
@@ -2219,23 +2230,23 @@
} }
}, },
"@schematics/angular": { "@schematics/angular": {
"version": "8.3.24", "version": "8.3.25",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-8.3.24.tgz", "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-8.3.25.tgz",
"integrity": "sha512-0nf/LgMHAvhjWS97Pl3JGMqS9/4PI+C0+vJoAo6D7ax8Fb+wuY5uD6Pb7ZqaZALlEnqTgE+FBQ1K8VBVbuwKbA==", "integrity": "sha512-/vEPtE+fvgsWPml/MVqzmlGPBujadPPNwaTuuj5Uz1aVcKeEYzLkbN8YQOpml4vxZHCF8RDwNdGiU4SZg63Jfg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@angular-devkit/core": "8.3.24", "@angular-devkit/core": "8.3.25",
"@angular-devkit/schematics": "8.3.24" "@angular-devkit/schematics": "8.3.25"
} }
}, },
"@schematics/update": { "@schematics/update": {
"version": "0.803.24", "version": "0.803.25",
"resolved": "https://registry.npmjs.org/@schematics/update/-/update-0.803.24.tgz", "resolved": "https://registry.npmjs.org/@schematics/update/-/update-0.803.25.tgz",
"integrity": "sha512-NvCKn3QfpRjx1EzL56q9IC9fRtDXZP4bMGs/2tj+wtdBNHgm6ZJMJ9qc4mGeztKGbDFLmnX3Xz0XawAl+KeYzQ==", "integrity": "sha512-VIlqhJsCStA3aO4llxZ7lAOvQUqppyZdrEO7f/ApIJmuofPQTkO5Hx21tnv0dyExwoqPCSIHzEu4Tmc0/TWM1A==",
"dev": true, "dev": true,
"requires": { "requires": {
"@angular-devkit/core": "8.3.24", "@angular-devkit/core": "8.3.25",
"@angular-devkit/schematics": "8.3.24", "@angular-devkit/schematics": "8.3.25",
"@yarnpkg/lockfile": "1.1.0", "@yarnpkg/lockfile": "1.1.0",
"ini": "1.3.5", "ini": "1.3.5",
"pacote": "9.5.5", "pacote": "9.5.5",
@@ -2328,9 +2339,9 @@
} }
}, },
"@types/node": { "@types/node": {
"version": "8.9.5", "version": "11.15.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.5.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-11.15.0.tgz",
"integrity": "sha512-jRHfWsvyMtXdbhnz5CVHxaBgnV6duZnPlQuRSo/dm/GnmikNcmZhxIES4E9OZjUmQ8C+HCl4KJux+cXN/ErGDQ==" "integrity": "sha512-JRq4kw1GQZrF90YRrp3C1kIoioAEj9PweNF2Qgp/6XZYVgXPl7OWKdggFNtRxlBPyl40Fz/bOhCnXuKMFaJ06w=="
}, },
"@types/source-list-map": { "@types/source-list-map": {
"version": "0.1.2", "version": "0.1.2",
@@ -2363,12 +2374,12 @@
} }
}, },
"@typescript-eslint/eslint-plugin": { "@typescript-eslint/eslint-plugin": {
"version": "2.19.0", "version": "2.19.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.19.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.19.2.tgz",
"integrity": "sha512-u7IcQ9qwsB6U806LupZmINRnQjC+RJyv36sV/ugaFWMHTbFm/hlLTRx3gGYJgHisxcGSTnf+I/fPDieRMhPSQQ==", "integrity": "sha512-HX2qOq2GOV04HNrmKnTpSIpHjfl7iwdXe3u/Nvt+/cpmdvzYvY0NHSiTkYN257jHnq4OM/yo+OsFgati+7LqJA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/experimental-utils": "2.19.0", "@typescript-eslint/experimental-utils": "2.19.2",
"eslint-utils": "^1.4.3", "eslint-utils": "^1.4.3",
"functional-red-black-tree": "^1.0.1", "functional-red-black-tree": "^1.0.1",
"regexpp": "^3.0.0", "regexpp": "^3.0.0",
@@ -2387,13 +2398,13 @@
} }
}, },
"@typescript-eslint/experimental-utils": { "@typescript-eslint/experimental-utils": {
"version": "2.19.0", "version": "2.19.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.19.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.19.2.tgz",
"integrity": "sha512-zwpg6zEOPbhB3+GaQfufzlMUOO6GXCNZq6skk+b2ZkZAIoBhVoanWK255BS1g5x9bMwHpLhX0Rpn5Fc3NdCZdg==", "integrity": "sha512-B88QuwT1wMJR750YvTJBNjMZwmiPpbmKYLm1yI7PCc3x0NariqPwqaPsoJRwU9DmUi0cd9dkhz1IqEnwfD+P1A==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/json-schema": "^7.0.3", "@types/json-schema": "^7.0.3",
"@typescript-eslint/typescript-estree": "2.19.0", "@typescript-eslint/typescript-estree": "2.19.2",
"eslint-scope": "^5.0.0" "eslint-scope": "^5.0.0"
}, },
"dependencies": { "dependencies": {
@@ -2410,21 +2421,21 @@
} }
}, },
"@typescript-eslint/parser": { "@typescript-eslint/parser": {
"version": "2.19.0", "version": "2.19.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.19.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.19.2.tgz",
"integrity": "sha512-s0jZoxAWjHnuidbbN7aA+BFVXn4TCcxEVGPV8lWMxZglSs3NRnFFAlL+aIENNmzB2/1jUJuySi6GiM6uACPmpg==", "integrity": "sha512-8uwnYGKqX9wWHGPGdLB9sk9+12sjcdqEEYKGgbS8A0IvYX59h01o8os5qXUHMq2na8vpDRaV0suTLM7S8wraTA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/eslint-visitor-keys": "^1.0.0", "@types/eslint-visitor-keys": "^1.0.0",
"@typescript-eslint/experimental-utils": "2.19.0", "@typescript-eslint/experimental-utils": "2.19.2",
"@typescript-eslint/typescript-estree": "2.19.0", "@typescript-eslint/typescript-estree": "2.19.2",
"eslint-visitor-keys": "^1.1.0" "eslint-visitor-keys": "^1.1.0"
} }
}, },
"@typescript-eslint/typescript-estree": { "@typescript-eslint/typescript-estree": {
"version": "2.19.0", "version": "2.19.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.19.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.19.2.tgz",
"integrity": "sha512-n6/Xa37k0jQdwpUszffi19AlNbVCR0sdvCs3DmSKMD7wBttKY31lhD2fug5kMD91B2qW4mQldaTEc1PEzvGu8w==", "integrity": "sha512-Xu/qa0MDk6upQWqE4Qy2X16Xg8Vi32tQS2PR0AvnT/ZYS4YGDvtn2MStOh5y8Zy2mg4NuL06KUHlvCh95j9C6Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"debug": "^4.1.1", "debug": "^4.1.1",
@@ -3572,9 +3583,9 @@
} }
}, },
"boolean": { "boolean": {
"version": "3.0.0", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.0.0.tgz", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.0.1.tgz",
"integrity": "sha512-OElxJ1lUSinuoUnkpOgLmxp0DC4ytEhODEL6QJU0NpxE/mI4rUSh8h1P1Wkvfi3xQEBcxXR2gBIPNYNuaFcAbQ==", "integrity": "sha512-HRZPIjPcbwAVQvOTxR4YE3o8Xs98NqbbL1iEZDCz7CL8ql0Lt5iOyJFxfnAB0oFs8Oh02F/lLlg30Mexv46LjA==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
@@ -4645,17 +4656,6 @@
"ssri": "^6.0.1", "ssri": "^6.0.1",
"unique-filename": "^1.1.1", "unique-filename": "^1.1.1",
"y18n": "^4.0.0" "y18n": "^4.0.0"
},
"dependencies": {
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
}
} }
}, },
"find-cache-dir": { "find-cache-dir": {
@@ -4668,6 +4668,15 @@
"make-dir": "^2.0.0", "make-dir": "^2.0.0",
"pkg-dir": "^3.0.0" "pkg-dir": "^3.0.0"
} }
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
} }
} }
}, },
@@ -5370,9 +5379,9 @@
"dev": true "dev": true
}, },
"electron": { "electron": {
"version": "7.1.11", "version": "7.1.12",
"resolved": "https://registry.npmjs.org/electron/-/electron-7.1.11.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-7.1.12.tgz",
"integrity": "sha512-YDXfnovKY+8iZ5ISQh1kRqYIRKbpOSxGXCx2WVxPFPutEQ7Q/Xzr3h4GePEY25/NXMytMfhKaAZAYjtWUm3r9Q==", "integrity": "sha512-gOJxAlJX2UyCRRncKzKzHSZStDI6MdoDzsustTCzudoZx3vlst1kkIP0n5t3TWTNoKNY/ihRsYIpeu63ar1m/g==",
"dev": true, "dev": true,
"requires": { "requires": {
"@electron/get": "^1.0.1", "@electron/get": "^1.0.1",
@@ -5381,9 +5390,9 @@
}, },
"dependencies": { "dependencies": {
"@types/node": { "@types/node": {
"version": "12.12.26", "version": "12.12.27",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.26.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.27.tgz",
"integrity": "sha512-UmUm94/QZvU5xLcUlNR8hA7Ac+fGpO1EG/a8bcWVz0P0LqtxFmun9Y2bbtuckwGboWJIT70DoWq1r3hb56n3DA==", "integrity": "sha512-odQFl/+B9idbdS0e8IxDl2ia/LP8KZLXhV3BUeI98TrZp0uoIzQPhGd+5EtzHmT0SMOIaPd7jfz6pOHLWTtl7A==",
"dev": true "dev": true
} }
} }
@@ -7782,9 +7791,9 @@
}, },
"dependencies": { "dependencies": {
"semver": { "semver": {
"version": "7.1.2", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.1.2.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.1.3.tgz",
"integrity": "sha512-BJs9T/H8sEVHbeigqzIEo57Iu/3DG6c4QoqTfbQB3BPA4zgzAomh/Fk9E7QtjWQ8mx2dgA9YCfSF4y9k9bHNpQ==", "integrity": "sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA==",
"dev": true, "dev": true,
"optional": true "optional": true
} }
@@ -9832,9 +9841,9 @@
"dev": true "dev": true
}, },
"istanbul-lib-instrument": { "istanbul-lib-instrument": {
"version": "4.0.0", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.0.tgz", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.1.tgz",
"integrity": "sha512-Nm4wVHdo7ZXSG30KjZ2Wl5SU/Bw7bDx1PdaiIFzEStdjs0H12mOTncn1GVYuqQSaZxpg87VGBRsVRPGD2cD1AQ==", "integrity": "sha512-imIchxnodll7pvQBYOqUu88EufLCU56LMeFPZZM/fJZ1irYcYdqroaV+ACK1Ila8ls09iEYArp+nqyC6lW1Vfg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/core": "^7.7.5", "@babel/core": "^7.7.5",
@@ -10438,9 +10447,9 @@
"integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M="
}, },
"loglevel": { "loglevel": {
"version": "1.6.6", "version": "1.6.7",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.6.tgz", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.7.tgz",
"integrity": "sha512-Sgr5lbboAUBo3eXCSPL4/KoVz3ROKquOjcctxmHIt+vol2DrqTQe3SwkKKuYhEiWB5kYa13YyopJ69deJ1irzQ==", "integrity": "sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A==",
"dev": true "dev": true
}, },
"longest": { "longest": {
@@ -13127,9 +13136,9 @@
} }
}, },
"rimraf": { "rimraf": {
"version": "3.0.1", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.1.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-IQ4ikL8SjBiEDZfk+DFVwqRK8md24RWMEJkdSlgNLkyyAImcjf8SWvU1qFMDOb4igBClbTQ/ugPqXcRwdFTxZw==", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dev": true, "dev": true,
"requires": { "requires": {
"glob": "^7.1.3" "glob": "^7.1.3"

View File

@@ -16,13 +16,13 @@
"private": true, "private": true,
"scripts": { "scripts": {
"start": "run-p serve:angular serve:electron", "start": "run-p serve:angular serve:electron",
"serve:electron": "wait-on http-get://localhost:4200/ && tsc -p tsconfig.electron.json && electron . --dev",
"lint": "ng lint", "lint": "ng lint",
"clean": "rimraf dist release", "clean": "rimraf dist release",
"build:windows": "ng build -c production && tsc -p tsconfig.electron.json && electron-builder build --windows", "build:windows": "ng build -c production && tsc -p tsconfig.electron.json && electron-builder build --windows",
"build:mac": "ng build -c production && tsc -p tsconfig.electron.json && electron-builder build --mac", "build:mac": "ng build -c production && tsc -p tsconfig.electron.json && electron-builder build --mac",
"build:linux": "ng build -c production && tsc -p tsconfig.electron.json && electron-builder build --linux", "build:linux": "ng build -c production && tsc -p tsconfig.electron.json && electron-builder build --linux",
"serve:angular": "ng serve", "serve:angular": "ng serve"
"serve:electron": "wait-on http-get://localhost:4200/ && tsc -p tsconfig.electron.json && electron . --dev"
}, },
"dependencies": { "dependencies": {
"7zip-bin": "^5.0.3", "7zip-bin": "^5.0.3",
@@ -52,20 +52,20 @@
"zone.js": "~0.9.1" "zone.js": "~0.9.1"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "~0.803.24", "@angular-devkit/build-angular": "^0.803.25",
"@angular-eslint/builder": "0.0.1-alpha.18", "@angular-eslint/builder": "0.0.1-alpha.18",
"@angular/cli": "~8.3.24", "@angular/cli": "^8.3.25",
"@angular/compiler-cli": "~8.2.14", "@angular/compiler-cli": "~8.2.14",
"@angular/language-service": "~8.2.14", "@angular/language-service": "~8.2.14",
"@types/node": "~8.9.4", "@types/node": "11.15.0",
"@typescript-eslint/eslint-plugin": "^2.19.0", "@typescript-eslint/eslint-plugin": "^2.19.2",
"@typescript-eslint/parser": "^2.19.0", "@typescript-eslint/parser": "^2.19.2",
"electron": "^7.1.11", "electron": "^7.1.12",
"electron-builder": "^22.3.2", "electron-builder": "^22.3.2",
"electron-reload": "^1.5.0", "electron-reload": "^1.5.0",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"rimraf": "^3.0.1", "rimraf": "^3.0.2",
"ts-node": "~7.0.0", "ts-node": "~7.0.0",
"tslint": "~5.15.0", "tslint": "~5.15.0",
"typescript": "~3.5.3", "typescript": "~3.5.3",

View File

@@ -128,8 +128,8 @@ export class ChartSidebarComponent {
} }
onDownloadClicked() { onDownloadClicked() {
this.downloadService.addDownload({ this.downloadService.addDownload(
versionID: this.selectedVersion.versionID, this.selectedVersion.versionID, {
avTagName: this.selectedVersion.avTagName, avTagName: this.selectedVersion.avTagName,
artist: this.songResult.artist, artist: this.songResult.artist,
charter: this.selectedVersion.charters, charter: this.selectedVersion.charters,

View File

@@ -1,9 +1,24 @@
<div class="ui cards"> <div class="ui cards">
<div *ngFor="let download of downloads; trackBy:trackByVersionID" class="card"> <div *ngFor="let download of downloads; trackBy:trackByVersionID" class="card" [style.background-color]="getBackgroundColor(download)">
<div class="content"> <div class="content">
<i class="inside right floated close icon" (click)="cancelDownload(download.versionID)"></i>
<div class="header">{{download.title}}</div> <div class="header">{{download.title}}</div>
</div> </div>
<div class="content"> <div class="content">
<button
*ngIf="download.type == 'error'"
class="ui right floated labeled icon button"
(click)="retryDownload(download.versionID)">
<i class="redo icon"></i>
Retry
</button>
<button
*ngIf="download.type == 'warning'"
class="ui right floated labeled icon button"
(click)="continueDownload(download.versionID)">
<i class="exclamation triangle icon"></i>
Download Anyway
</button>
<div class="header">{{download.header}}</div> <div class="header">{{download.header}}</div>
<div class="description">{{download.description}}</div> <div class="description">{{download.description}}</div>
<div appProgressBar [percent]="download.percent" class="ui progress"> <div appProgressBar [percent]="download.percent" class="ui progress">

View File

@@ -4,4 +4,8 @@
.ui.progress { .ui.progress {
margin: 0; margin: 0;
}
i.close.icon {
cursor: pointer;
} }

View File

@@ -1,5 +1,5 @@
import { Component, ChangeDetectorRef } from '@angular/core' import { Component, ChangeDetectorRef } from '@angular/core'
import { Download } from '../../../../../electron/shared/interfaces/download.interface' import { DownloadProgress } from '../../../../../electron/shared/interfaces/download.interface'
import { DownloadService } from '../../../../core/services/download.service' import { DownloadService } from '../../../../core/services/download.service'
@Component({ @Component({
@@ -9,13 +9,15 @@ import { DownloadService } from '../../../../core/services/download.service'
}) })
export class DownloadsModalComponent { export class DownloadsModalComponent {
downloads: Download[] = [] downloads: DownloadProgress[] = []
constructor(downloadService: DownloadService, ref: ChangeDetectorRef) { constructor(private downloadService: DownloadService, ref: ChangeDetectorRef) {
downloadService.onDownloadUpdated(download => { downloadService.onDownloadUpdated(download => {
const index = this.downloads.findIndex(thisDownload => thisDownload.versionID == download.versionID) const index = this.downloads.findIndex(thisDownload => thisDownload.versionID == download.versionID)
if (index == -1) { if (index == -1) {
this.downloads.push(download) this.downloads.push(download)
} else if (download.type == 'cancel') {
this.downloads.splice(index, 1)
} else { } else {
this.downloads[index] = download this.downloads[index] = download
} }
@@ -23,7 +25,28 @@ export class DownloadsModalComponent {
}) })
} }
trackByVersionID(_index: number, item: Download) { trackByVersionID(_index: number, item: DownloadProgress) {
return item.versionID return item.versionID
} }
cancelDownload(versionID: number) {
this.downloadService.cancelDownload(versionID)
}
retryDownload(versionID: number) {
this.downloadService.retryDownload(versionID)
}
continueDownload(versionID: number) {
// TODO: test this
this.downloadService.continueDownload(versionID)
}
getBackgroundColor(download: DownloadProgress) {
switch(download.type) {
case 'good': return 'unset'
case 'warning': return 'yellow'
case 'error': return 'indianred'
}
}
} }

View File

@@ -1,6 +1,6 @@
import { Injectable, EventEmitter } from '@angular/core' import { Injectable, EventEmitter } from '@angular/core'
import { ElectronService } from './electron.service' import { ElectronService } from './electron.service'
import { Download, NewDownload } from '../../../electron/shared/interfaces/download.interface' import { NewDownload, DownloadProgress } from '../../../electron/shared/interfaces/download.interface'
import * as _ from 'underscore' import * as _ from 'underscore'
@Injectable({ @Injectable({
@@ -8,12 +8,24 @@ import * as _ from 'underscore'
}) })
export class DownloadService { export class DownloadService {
private downloadUpdatedEmitter = new EventEmitter<Download>() private downloadUpdatedEmitter = new EventEmitter<DownloadProgress>()
private downloads: Download[] = [] private downloads: DownloadProgress[] = []
constructor(private electronService: ElectronService) { } constructor(private electronService: ElectronService) { }
addDownload(newDownload: NewDownload) { get downloadCount() {
return this.downloads.length
}
get totalPercent() {
let total = 0
for (const download of this.downloads) {
total += download.percent
}
return total / this.downloads.length
}
addDownload(versionID: number, newDownload: NewDownload) {
this.electronService.receiveIPC('download-updated', result => { this.electronService.receiveIPC('download-updated', result => {
this.downloadUpdatedEmitter.emit(result) this.downloadUpdatedEmitter.emit(result)
@@ -25,28 +37,26 @@ export class DownloadService {
this.downloads[thisDownloadIndex] = result this.downloads[thisDownloadIndex] = result
} }
}) })
this.electronService.sendIPC('add-download', newDownload) this.electronService.sendIPC('download', { action: 'add', versionID, data: newDownload })
} }
onDownloadUpdated(callback: (download: Download) => void) { onDownloadUpdated(callback: (download: DownloadProgress) => void) {
this.downloadUpdatedEmitter.subscribe(_.throttle(callback, 30)) this.downloadUpdatedEmitter.subscribe(_.throttle(callback, 30))
} }
get downloadCount() { cancelDownload(versionID: number) {
return this.downloads.length
}
removeDownload(versionID: number) {
const removedDownload = this.downloads.find(download => download.versionID == versionID) const removedDownload = this.downloads.find(download => download.versionID == versionID)
this.downloads = this.downloads.filter(download => download.versionID != versionID) this.downloads = this.downloads.filter(download => download.versionID != versionID)
removedDownload.type = 'cancel'
this.downloadUpdatedEmitter.emit(removedDownload) this.downloadUpdatedEmitter.emit(removedDownload)
this.electronService.sendIPC('download', { action: 'cancel', versionID })
} }
get totalPercent() { retryDownload(versionID: number) {
let total = 0 this.electronService.sendIPC('download', { action: 'retry', versionID })
for (const download of this.downloads) { }
total += download.percent
} continueDownload(versionID: number) {
return total / this.downloads.length this.electronService.sendIPC('download', { action: 'continue', versionID })
} }
} }

View File

@@ -4,7 +4,7 @@ import { createHash, randomBytes as _randomBytes } from 'crypto'
import { tempPath } from '../../shared/Paths' import { tempPath } from '../../shared/Paths'
import { promisify } from 'util' import { promisify } from 'util'
import { join } from 'path' import { join } from 'path'
import { Download, NewDownload } from '../../shared/interfaces/download.interface' import { Download, NewDownload, DownloadProgress } from '../../shared/interfaces/download.interface'
import { emitIPCEvent } from '../../main' import { emitIPCEvent } from '../../main'
import { mkdir as _mkdir } from 'fs' import { mkdir as _mkdir } from 'fs'
import { FileExtractor } from './FileExtractor' import { FileExtractor } from './FileExtractor'
@@ -13,16 +13,29 @@ import { sanitizeFilename, interpolate } from '../../shared/UtilFunctions'
const randomBytes = promisify(_randomBytes) const randomBytes = promisify(_randomBytes)
const mkdir = promisify(_mkdir) const mkdir = promisify(_mkdir)
export class AddDownloadHandler implements IPCEmitHandler<'add-download'> { export class DownloadHandler implements IPCEmitHandler<'download'> {
event: 'add-download' = 'add-download' event: 'download' = 'download'
async handler(data: NewDownload) { // TODO: replace needle with got (for cancel() method) (if before-headers event is possible?)
const download: Download = {
downloadCallbacks: { [versionID: number]: { cancel: () => void, retry: () => void, continue: () => void } } = {}
async handler(data: Download) {
switch (data.action) {
case 'cancel': this.downloadCallbacks[data.versionID].cancel(); return
case 'retry': this.downloadCallbacks[data.versionID].retry(); return
case 'continue': this.downloadCallbacks[data.versionID].continue(); return
case 'add': this.downloadCallbacks[data.versionID] = { cancel: () => { }, retry: () => { }, continue: () => { } }
}
// data.action == add; data.data should be defined
const download: DownloadProgress = {
versionID: data.versionID, versionID: data.versionID,
title: `${data.avTagName} - ${data.artist}`, title: `${data.data.avTagName} - ${data.data.artist}`,
header: '', header: '',
description: '', description: '',
percent: 0 percent: 0,
type: 'good'
} }
const randomString = (await randomBytes(5)).toString('hex') const randomString = (await randomBytes(5)).toString('hex')
const chartPath = join(tempPath, `chart_${randomString}`) const chartPath = join(tempPath, `chart_${randomString}`)
@@ -30,17 +43,19 @@ export class AddDownloadHandler implements IPCEmitHandler<'add-download'> {
let allFilesProgress = 0 let allFilesProgress = 0
// Only iterate over the keys in data.links that have link values (not hashes) // Only iterate over the keys in data.links that have link values (not hashes)
const fileKeys = Object.keys(data.links).filter(link => data.links[link].includes('.')) const fileKeys = Object.keys(data.data.links).filter(link => data.data.links[link].includes('.'))
const individualFileProgressPortion = 80 / fileKeys.length const individualFileProgressPortion = 80 / fileKeys.length
for (let i = 0; i < fileKeys.length; i++) { for (let i = 0; i < fileKeys.length; i++) {
const typeHash = createHash('md5').update(data.links[fileKeys[i]]).digest('hex') const typeHash = createHash('md5').update(data.data.links[fileKeys[i]]).digest('hex')
const downloader = new FileDownloader(data.links[fileKeys[i]], chartPath, data.links[typeHash]) const downloader = new FileDownloader(data.data.links[fileKeys[i]], chartPath, data.data.links[typeHash])
this.downloadCallbacks[data.versionID].cancel = () => downloader.cancelDownload() // Make cancel button cancel this download
let fileProgress = 0 let fileProgress = 0
let waitTime: number let waitTime: number
downloader.on('wait', (_waitTime) => { downloader.on('wait', (_waitTime) => {
download.header = `[${fileKeys[i]}] (file ${i + 1}/${fileKeys.length})` download.header = `[${fileKeys[i]}] (file ${i + 1}/${fileKeys.length})`
download.description = `Waiting for Google rate limit... (${_waitTime}s)` download.description = `Waiting for Google rate limit... (${_waitTime}s)`
download.type = 'good'
waitTime = _waitTime waitTime = _waitTime
}) })
@@ -48,6 +63,7 @@ export class AddDownloadHandler implements IPCEmitHandler<'add-download'> {
download.description = `Waiting for Google rate limit... (${secondsRemaining}s)` download.description = `Waiting for Google rate limit... (${secondsRemaining}s)`
fileProgress = interpolate(secondsRemaining, waitTime, 0, 0, individualFileProgressPortion / 2) fileProgress = interpolate(secondsRemaining, waitTime, 0, 0, individualFileProgressPortion / 2)
download.percent = allFilesProgress + fileProgress download.percent = allFilesProgress + fileProgress
download.type = 'good'
emitIPCEvent('download-updated', download) emitIPCEvent('download-updated', download)
}) })
@@ -55,13 +71,15 @@ export class AddDownloadHandler implements IPCEmitHandler<'add-download'> {
download.description = `Sending request...` download.description = `Sending request...`
fileProgress = individualFileProgressPortion / 2 fileProgress = individualFileProgressPortion / 2
download.percent = allFilesProgress + fileProgress download.percent = allFilesProgress + fileProgress
download.type = 'good'
emitIPCEvent('download-updated', download) emitIPCEvent('download-updated', download)
}) })
downloader.on('warning', (continueAnyway) => { downloader.on('warning', (continueAnyway) => {
download.description = 'WARNING' download.description = 'WARNING'
this.downloadCallbacks[data.versionID].continue = continueAnyway
download.type = 'warning'
emitIPCEvent('download-updated', download) emitIPCEvent('download-updated', download)
//TODO: continue anyway
}) })
let filesize = -1 let filesize = -1
@@ -73,6 +91,7 @@ export class AddDownloadHandler implements IPCEmitHandler<'add-download'> {
} else { } else {
download.description = `Downloading... (0 MB)` download.description = `Downloading... (0 MB)`
} }
download.type = 'good'
emitIPCEvent('download-updated', download) emitIPCEvent('download-updated', download)
}) })
@@ -85,14 +104,16 @@ export class AddDownloadHandler implements IPCEmitHandler<'add-download'> {
download.description = `Downloading... (${Math.round(bytesDownloaded / 1e+5) / 10} MB)` download.description = `Downloading... (${Math.round(bytesDownloaded / 1e+5) / 10} MB)`
download.percent = allFilesProgress + fileProgress download.percent = allFilesProgress + fileProgress
} }
download.type = 'good'
emitIPCEvent('download-updated', download) emitIPCEvent('download-updated', download)
}) })
downloader.on('error', (error, retry) => { downloader.on('error', (error, retry) => {
download.header = error.header download.header = error.header
download.description = error.body download.description = error.body
download.type = 'error'
this.downloadCallbacks[data.versionID].retry = retry
emitIPCEvent('download-updated', download) emitIPCEvent('download-updated', download)
// TODO: retry
}) })
// Wait for the 'complete' event before moving on to another file download // Wait for the 'complete' event before moving on to another file download
@@ -107,14 +128,16 @@ export class AddDownloadHandler implements IPCEmitHandler<'add-download'> {
}) })
} }
const destinationFolderName = sanitizeFilename(`${data.artist} - ${data.avTagName} (${data.charter})`) const destinationFolderName = sanitizeFilename(`${data.data.artist} - ${data.data.avTagName} (${data.data.charter})`)
const extractor = new FileExtractor(chartPath, fileKeys.includes('archive'), destinationFolderName) const extractor = new FileExtractor(chartPath, fileKeys.includes('archive'), destinationFolderName)
this.downloadCallbacks[data.versionID].cancel = () => extractor.cancelExtract()
let archive = '' let archive = ''
extractor.on('extract', (filename) => { extractor.on('extract', (filename) => {
archive = filename archive = filename
download.header = `[${archive}]` download.header = `[${archive}]`
download.description = `Extracting...` download.description = `Extracting...`
download.type = 'good'
emitIPCEvent('download-updated', download) emitIPCEvent('download-updated', download)
}) })
@@ -122,6 +145,7 @@ export class AddDownloadHandler implements IPCEmitHandler<'add-download'> {
download.header = `[${archive}] (${filecount} file${filecount == 1 ? '' : 's'} extracted)` download.header = `[${archive}] (${filecount} file${filecount == 1 ? '' : 's'} extracted)`
download.description = `Extracting... (${percent}%)` download.description = `Extracting... (${percent}%)`
download.percent = interpolate(percent, 0, 100, 80, 95) download.percent = interpolate(percent, 0, 100, 80, 95)
download.type = 'good'
emitIPCEvent('download-updated', download) emitIPCEvent('download-updated', download)
}) })
@@ -129,6 +153,7 @@ export class AddDownloadHandler implements IPCEmitHandler<'add-download'> {
download.header = `Moving files to library folder...` download.header = `Moving files to library folder...`
download.description = filepath download.description = filepath
download.percent = 95 download.percent = 95
download.type = 'good'
emitIPCEvent('download-updated', download) emitIPCEvent('download-updated', download)
}) })
@@ -136,14 +161,16 @@ export class AddDownloadHandler implements IPCEmitHandler<'add-download'> {
download.header = `Download complete.` download.header = `Download complete.`
download.description = filepath download.description = filepath
download.percent = 100 download.percent = 100
download.type = 'good'
emitIPCEvent('download-updated', download) emitIPCEvent('download-updated', download)
}) })
extractor.on('error', (error, retry) => { extractor.on('error', (error, retry) => {
download.header = error.header download.header = error.header
download.description = error.body download.description = error.body
download.type = 'error'
this.downloadCallbacks[data.versionID].retry = retry
emitIPCEvent('download-updated', download) emitIPCEvent('download-updated', download)
// TODO: retry
}) })
extractor.beginExtract() extractor.beginExtract()

View File

@@ -19,12 +19,13 @@ export type DownloadError = { header: string, body: string }
export class FileDownloader { export class FileDownloader {
private RATE_LIMIT_DELAY: number private RATE_LIMIT_DELAY: number
private readonly RETRY_MAX = 3 private readonly RETRY_MAX = 2
private static waitTime = 0 private static waitTime = 0
private static clock: NodeJS.Timer private static clock: NodeJS.Timer
private callbacks = {} as Callbacks private callbacks = {} as Callbacks
private retryCount: number private retryCount: number
private wasCanceled = false
constructor(private url: string, private destinationFolder: string, private expectedHash?: string) { constructor(private url: string, private destinationFolder: string, private expectedHash?: string) {
if (FileDownloader.clock == undefined) { if (FileDownloader.clock == undefined) {
@@ -60,6 +61,7 @@ export class FileDownloader {
} }
this.callbacks.wait(waitTime) this.callbacks.wait(waitTime)
const clock = setInterval(() => { const clock = setInterval(() => {
if (this.wasCanceled) { clearInterval(clock); return } // CANCEL POINT
waitTime-- waitTime--
this.callbacks.waitProgress(waitTime) this.callbacks.waitProgress(waitTime)
if (waitTime <= 0) { if (waitTime <= 0) {
@@ -75,10 +77,12 @@ export class FileDownloader {
* @param cookieHeader the "cookie=" header to include this request. * @param cookieHeader the "cookie=" header to include this request.
*/ */
private requestDownload(cookieHeader?: string) { private requestDownload(cookieHeader?: string) {
if (this.wasCanceled) { return } // CANCEL POINT
this.callbacks.request() this.callbacks.request()
let uuid = generateUUID() let uuid = generateUUID()
const req = needle.get(this.url, { const req = needle.get(this.url, {
follow_max: 10, follow_max: 10,
open_timeout: 5000,
headers: Object.assign({ headers: Object.assign({
'User-Agent': 'PostmanRuntime/7.22.0', 'User-Agent': 'PostmanRuntime/7.22.0',
'Referer': this.url, 'Referer': this.url,
@@ -99,12 +103,12 @@ export class FileDownloader {
} }
}) })
req.on('err', (err) => { req.on('err', (err: Error) => {
// TODO: this is called on timeout; if there are other cases where this can fail, they should be printed correctly this.callbacks.error({ header: 'Connection Error', body: `${err.name}: ${err.message}` }, () => this.beginDownload())
// this.callbacks.error({ header: 'Error', description: `${err}` }, () => this.beginDownload())
}) })
req.on('header', (statusCode, headers: Headers) => { req.on('header', (statusCode, headers: Headers) => {
if (this.wasCanceled) { return } // CANCEL POINT
if (statusCode != 200) { if (statusCode != 200) {
this.callbacks.error({ header: 'Connection failed', body: `Server returned status code: ${statusCode}` }, () => this.beginDownload()) this.callbacks.error({ header: 'Connection failed', body: `Server returned status code: ${statusCode}` }, () => this.beginDownload())
return return
@@ -117,10 +121,10 @@ export class FileDownloader {
const fileName = this.getDownloadFileName(headers) const fileName = this.getDownloadFileName(headers)
const downloadHash = this.getDownloadHash(headers) const downloadHash = this.getDownloadHash(headers)
if (this.expectedHash !== undefined && downloadHash !== this.expectedHash) { if (this.expectedHash !== undefined && downloadHash !== this.expectedHash) {
req.pause()
this.callbacks.warning(() => { this.callbacks.warning(() => {
//TODO: check if this will actually work (or will the data get lost in the time before the button is clicked?)
// Maybe show the message at the end, and ask if they want to keep it.
this.handleDownloadResponse(req, fileName, headers['content-length']) this.handleDownloadResponse(req, fileName, headers['content-length'])
req.resume()
}) })
} else { } else {
this.handleDownloadResponse(req, fileName, headers['content-length']) this.handleDownloadResponse(req, fileName, headers['content-length'])
@@ -220,4 +224,8 @@ export class FileDownloader {
return headers['x-goog-hash'] return headers['x-goog-hash']
} }
} }
cancelDownload() {
this.wasCanceled = true
}
} }

View File

@@ -29,6 +29,7 @@ export class FileExtractor {
private callbacks = {} as Callbacks private callbacks = {} as Callbacks
private libraryFolder: string private libraryFolder: string
private wasCanceled = false
constructor(private sourceFolder: string, private isArchive: boolean, private destinationFolderName: string) { } constructor(private sourceFolder: string, private isArchive: boolean, private destinationFolderName: string) { }
/** /**
@@ -58,6 +59,7 @@ export class FileExtractor {
* @param filename The name of the archive file. * @param filename The name of the archive file.
*/ */
private extract(filename: string) { private extract(filename: string) {
if (this.wasCanceled) { return } // CANCEL POINT
this.callbacks.extract(filename) this.callbacks.extract(filename)
const source = join(this.sourceFolder, filename) const source = join(this.sourceFolder, filename)
@@ -93,6 +95,7 @@ export class FileExtractor {
* Deletes the archive at <archiveFilepath>, then transfers the extracted chart to <this.libraryFolder>. * Deletes the archive at <archiveFilepath>, then transfers the extracted chart to <this.libraryFolder>.
*/ */
private async transfer(archiveFilepath?: string) { private async transfer(archiveFilepath?: string) {
if (this.wasCanceled) { return } // CANCEL POINT
try { try {
// Create destiniation folder if it doesn't exist // Create destiniation folder if it doesn't exist
@@ -117,6 +120,8 @@ export class FileExtractor {
files = await readdir(this.sourceFolder) files = await readdir(this.sourceFolder)
} }
if (this.wasCanceled) { return } // CANCEL POINT
// Copy the files from the temporary directory to the destination // Copy the files from the temporary directory to the destination
for (const file of files) { for (const file of files) {
await copyFile(join(this.sourceFolder, file), join(destinationFolder, file)) await copyFile(join(this.sourceFolder, file), join(destinationFolder, file))
@@ -133,4 +138,8 @@ export class FileExtractor {
this.callbacks.error({ header: 'Transfer Failed', body: `Unable to transfer downloaded files to the library folder: ${e.name}` }, undefined) this.callbacks.error({ header: 'Transfer Failed', body: `Unable to transfer downloaded files to the library folder: ${e.name}` }, undefined)
} }
} }
cancelExtract() {
this.wasCanceled = true
}
} }

View File

@@ -37,6 +37,7 @@ export default class Database {
} }
}) })
// TODO: make this error message more user-friendly (retry option?)
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
this.conn.connect(err => { this.conn.connect(err => {
if (err) { if (err) {

View File

@@ -3,8 +3,8 @@ import { VersionResult, AlbumArtResult } from './interfaces/songDetails.interfac
import SearchHandler from '../ipc/SearchHandler.ipc' import SearchHandler from '../ipc/SearchHandler.ipc'
import SongDetailsHandler from '../ipc/SongDetailsHandler.ipc' import SongDetailsHandler from '../ipc/SongDetailsHandler.ipc'
import AlbumArtHandler from '../ipc/AlbumArtHandler.ipc' import AlbumArtHandler from '../ipc/AlbumArtHandler.ipc'
import { Download, NewDownload } from './interfaces/download.interface' import { Download, NewDownload, DownloadProgress } from './interfaces/download.interface'
import { AddDownloadHandler } from '../ipc/download/AddDownloadHandler' import { DownloadHandler } from '../ipc/download/DownloadHandler'
import { Settings } from './Settings' import { Settings } from './Settings'
import InitSettingsHandler from '../ipc/InitSettingsHandler.ipc' import InitSettingsHandler from '../ipc/InitSettingsHandler.ipc'
@@ -51,13 +51,13 @@ export interface IPCInvokeHandler<E extends keyof IPCInvokeEvents> {
export function getIPCEmitHandlers(): IPCEmitHandler<keyof IPCEmitEvents>[]{ export function getIPCEmitHandlers(): IPCEmitHandler<keyof IPCEmitEvents>[]{
return [ return [
new AddDownloadHandler() new DownloadHandler()
] ]
} }
export type IPCEmitEvents = { export type IPCEmitEvents = {
'add-download': NewDownload 'download': Download
'download-updated': Download 'download-updated': DownloadProgress
} }
export interface IPCEmitHandler<E extends keyof IPCEmitEvents> { export interface IPCEmitHandler<E extends keyof IPCEmitEvents> {

View File

@@ -1,8 +1,13 @@
export interface Download {
action: 'add' | 'retry' | 'continue' | 'cancel'
versionID: number
data ?: NewDownload
}
/** /**
* Contains the data required to start downloading a single chart * Contains the data required to start downloading a single chart
*/ */
export interface NewDownload { export interface NewDownload {
versionID: number
avTagName: string avTagName: string
artist: string artist: string
charter: string charter: string
@@ -12,10 +17,11 @@ export interface NewDownload {
/** /**
* Represents the download progress of a single chart * Represents the download progress of a single chart
*/ */
export interface Download { export interface DownloadProgress {
versionID: number versionID: number
title: string title: string
header: string header: string
description: string description: string
percent: number percent: number
type: 'good' | 'warning' | 'error' | 'cancel'
} }