├── .editorconfig ├── .gitignore ├── .prettierrc.json ├── angular.json ├── dist ├── 3rdpartylicenses.txt ├── i18n │ ├── .gitkeep │ ├── en.json │ ├── fr.json │ └── pl.json ├── manifest.json └── plugin.js ├── gulpfile.js ├── ionic.config.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── projects └── plugin │ ├── src │ ├── i18n │ │ ├── .gitkeep │ │ ├── en.json │ │ ├── fr.json │ │ └── pl.json │ ├── main.ts │ ├── manifest.json │ └── plugin │ │ ├── entities │ │ ├── kodi-open-media.ts │ │ └── kodi-plugin.ts │ │ ├── episode-button │ │ ├── episode-button.component.html │ │ ├── episode-button.component.scss │ │ └── episode-button.component.ts │ │ ├── episode-item-option │ │ ├── episode-item-option.component.html │ │ ├── episode-item-option.component.scss │ │ └── episode-item-option.component.ts │ │ ├── movie-button │ │ ├── movie-button.component.html │ │ ├── movie-button.component.scss │ │ └── movie-button.component.ts │ │ ├── open-button │ │ ├── open-button.component.html │ │ ├── open-button.component.scss │ │ └── open-button.component.ts │ │ ├── plugin.module.ts │ │ ├── services │ │ ├── kodi-plugin.service.ts │ │ ├── plugin.service.ts │ │ ├── toast.service.ts │ │ └── tools.ts │ │ └── settings │ │ ├── settings.component.html │ │ ├── settings.component.scss │ │ └── settings.component.ts │ └── tsconfig.app.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.module.ts │ ├── app.wako-like.module.ts │ ├── episode │ │ ├── episode.module.ts │ │ ├── episode.page.html │ │ ├── episode.page.scss │ │ └── episode.page.ts │ ├── movie │ │ ├── movie.module.ts │ │ ├── movie.page.html │ │ ├── movie.page.scss │ │ └── movie.page.ts │ ├── services │ │ ├── app.service.ts │ │ ├── plugin-loader-fake.service.ts │ │ └── plugin-loader.service.ts │ ├── settings │ │ ├── addon-detail │ │ │ ├── addon-detail.component.html │ │ │ ├── addon-detail.component.scss │ │ │ └── addon-detail.component.ts │ │ ├── addon-settings │ │ │ ├── addon-settings.component.html │ │ │ ├── addon-settings.component.scss │ │ │ └── addon-settings.component.ts │ │ ├── settings.module.ts │ │ ├── settings.page.html │ │ ├── settings.page.scss │ │ └── settings.page.ts │ └── tabs │ │ ├── tabs.module.ts │ │ ├── tabs.page.html │ │ ├── tabs.page.scss │ │ ├── tabs.page.ts │ │ └── tabs.router.module.ts ├── assets │ ├── icon │ │ └── favicon.png │ └── plugins │ │ ├── 3rdpartylicenses.txt │ │ ├── i18n │ │ ├── .gitkeep │ │ ├── el.json │ │ ├── en.json │ │ ├── fr.json │ │ ├── it.json │ │ └── pl.json │ │ ├── manifest.json │ │ └── plugin.js ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── global.scss ├── index.html ├── main-wako-like.ts ├── main.ts ├── polyfills.ts ├── test.ts ├── theme │ └── variables.scss ├── typings.d.ts └── zone-flags.ts ├── tsconfig.app.json ├── tsconfig.app.wako-like.json ├── tsconfig.json └── tsconfig.spec.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.angular/cache 2 | # Specifies intentionally untracked files to ignore when using Git 3 | # http://git-scm.com/docs/gitignore 4 | 5 | *~ 6 | *.sw[mnpcod] 7 | .tmp 8 | *.tmp 9 | *.tmp.* 10 | *.sublime-project 11 | *.sublime-workspace 12 | .DS_Store 13 | Thumbs.db 14 | UserInterfaceState.xcuserstate 15 | $RECYCLE.BIN/ 16 | 17 | *.log 18 | log.txt 19 | npm-debug.log* 20 | 21 | /.idea 22 | /.ionic 23 | /.sass-cache 24 | /.sourcemaps 25 | /.versions 26 | /.vscode 27 | /coverage 28 | /node_modules 29 | /platforms 30 | /plugins 31 | /www 32 | 33 | 34 | 35 | resources/android/icon 36 | resources/android/splash 37 | resources/ios/icon 38 | resources/ios/splash 39 | 40 | ## Local history plugin vscode 41 | .history 42 | 43 | ## Android 44 | .gradle/ 45 | android/.idea 46 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "app": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | }, 12 | "@schematics/angular:application": { 13 | "strict": true 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:browser", 22 | "options": { 23 | "outputPath": "www", 24 | "index": "src/index.html", 25 | "main": "src/main.ts", 26 | "polyfills": "src/polyfills.ts", 27 | "tsConfig": "tsconfig.app.json", 28 | "inlineStyleLanguage": "scss", 29 | "assets": [ 30 | { 31 | "glob": "**/*", 32 | "input": "projects/plugin/src", 33 | "output": "assets" 34 | }, 35 | { 36 | "glob": "**/*", 37 | "input": "src/assets", 38 | "output": "assets" 39 | }, 40 | { 41 | "glob": "**/*.svg", 42 | "input": "node_modules/ionicons/dist/ionicons/svg", 43 | "output": "./svg" 44 | } 45 | ], 46 | "styles": [ 47 | { 48 | "input": "src/theme/variables.scss" 49 | }, 50 | { 51 | "input": "src/global.scss" 52 | } 53 | ], 54 | "scripts": [ 55 | "node_modules/systemjs/dist/s.js", 56 | "node_modules/systemjs/dist/extras/named-register.js", 57 | "node_modules/systemjs/dist/extras/amd.js" 58 | ], 59 | "aot": false, 60 | "vendorChunk": true, 61 | "extractLicenses": false, 62 | "buildOptimizer": false, 63 | "sourceMap": true, 64 | "optimization": false, 65 | "namedChunks": true 66 | }, 67 | "configurations": { 68 | "production": { 69 | "fileReplacements": [ 70 | { 71 | "replace": "src/environments/environment.ts", 72 | "with": "src/environments/environment.prod.ts" 73 | } 74 | ], 75 | "optimization": true, 76 | "outputHashing": "all", 77 | "sourceMap": false, 78 | "namedChunks": false, 79 | "aot": true, 80 | "extractLicenses": true, 81 | "vendorChunk": false, 82 | "buildOptimizer": true, 83 | "budgets": [ 84 | { 85 | "type": "initial", 86 | "maximumWarning": "2mb", 87 | "maximumError": "5mb" 88 | } 89 | ] 90 | }, 91 | "ci": { 92 | "progress": false 93 | } 94 | } 95 | }, 96 | "serve": { 97 | "builder": "@angular-devkit/build-angular:dev-server", 98 | "options": { 99 | "browserTarget": "app:build" 100 | }, 101 | "configurations": { 102 | "production": { 103 | "browserTarget": "app:build:production" 104 | }, 105 | "ci": {} 106 | } 107 | }, 108 | "extract-i18n": { 109 | "builder": "@angular-devkit/build-angular:extract-i18n", 110 | "options": { 111 | "browserTarget": "app:build" 112 | } 113 | }, 114 | "test": { 115 | "builder": "@angular-devkit/build-angular:karma", 116 | "options": { 117 | "main": "src/test.ts", 118 | "polyfills": "src/polyfills.ts", 119 | "tsConfig": "tsconfig.spec.json", 120 | "karmaConfig": "karma.conf.js", 121 | "styles": [], 122 | "scripts": [], 123 | "assets": [ 124 | { 125 | "glob": "favicon.ico", 126 | "input": "src/", 127 | "output": "/" 128 | }, 129 | { 130 | "glob": "**/*", 131 | "input": "src/assets", 132 | "output": "/assets" 133 | } 134 | ] 135 | }, 136 | "configurations": { 137 | "ci": { 138 | "progress": false, 139 | "watch": false 140 | } 141 | } 142 | }, 143 | "lint": { 144 | "builder": "@angular-eslint/builder:lint", 145 | "options": { 146 | "lintFilePatterns": [ 147 | "src/**/*.ts", 148 | "src/**/*.html" 149 | ] 150 | } 151 | }, 152 | "e2e": { 153 | "builder": "@angular-devkit/build-angular:protractor", 154 | "options": { 155 | "protractorConfig": "e2e/protractor.conf.js", 156 | "devServerTarget": "app:serve" 157 | }, 158 | "configurations": { 159 | "production": { 160 | "devServerTarget": "app:serve:production" 161 | }, 162 | "ci": { 163 | "devServerTarget": "app:serve:ci" 164 | } 165 | } 166 | }, 167 | "ionic-cordova-build": { 168 | "builder": "@ionic/angular-toolkit:cordova-build", 169 | "options": { 170 | "browserTarget": "app:build" 171 | }, 172 | "configurations": { 173 | "production": { 174 | "browserTarget": "app:build:production" 175 | } 176 | } 177 | }, 178 | "ionic-cordova-serve": { 179 | "builder": "@ionic/angular-toolkit:cordova-serve", 180 | "options": { 181 | "cordovaBuildTarget": "app:ionic-cordova-build", 182 | "devServerTarget": "app:serve" 183 | }, 184 | "configurations": { 185 | "production": { 186 | "cordovaBuildTarget": "app:ionic-cordova-build:production", 187 | "devServerTarget": "app:serve:production" 188 | } 189 | } 190 | } 191 | } 192 | }, 193 | "plugin": { 194 | "root": "projects/plugin/", 195 | "sourceRoot": "projects/plugin/src", 196 | "projectType": "application", 197 | "prefix": "app", 198 | "schematics": {}, 199 | "architect": { 200 | "build": { 201 | "builder": "@wako-app/mobile-sdk/builders:plugin", 202 | "options": { 203 | "aot": true, 204 | "outputPath": "dist", 205 | "index": "", 206 | "main": "projects/plugin/src/main.ts", 207 | "polyfills": "", 208 | "tsConfig": "projects/plugin/tsconfig.app.json", 209 | "assets": [], 210 | "styles": [], 211 | "scripts": [] 212 | }, 213 | "configurations": { 214 | "production": { 215 | "fileReplacements": [], 216 | "optimization": true, 217 | "outputHashing": "all", 218 | "sourceMap": false, 219 | "namedChunks": false, 220 | "aot": true, 221 | "extractLicenses": true, 222 | "vendorChunk": false, 223 | "buildOptimizer": true, 224 | "budgets": [ 225 | { 226 | "type": "initial", 227 | "maximumWarning": "2mb", 228 | "maximumError": "5mb" 229 | } 230 | ] 231 | } 232 | } 233 | } 234 | } 235 | }, 236 | "wako-like": { 237 | "root": "", 238 | "sourceRoot": "src", 239 | "projectType": "application", 240 | "prefix": "app", 241 | "schematics": {}, 242 | "architect": { 243 | "build": { 244 | "builder": "@angular-devkit/build-angular:browser", 245 | "options": { 246 | "aot": true, 247 | "outputPath": "www", 248 | "index": "src/index.html", 249 | "main": "src/main-wako-like.ts", 250 | "polyfills": "src/polyfills.ts", 251 | "tsConfig": "tsconfig.app.wako-like.json", 252 | "assets": [ 253 | { 254 | "glob": "**/*", 255 | "input": "src/assets", 256 | "output": "assets" 257 | }, 258 | { 259 | "glob": "**/*.svg", 260 | "input": "node_modules/ionicons/dist/ionicons/svg", 261 | "output": "./svg" 262 | } 263 | ], 264 | "styles": [ 265 | { 266 | "input": "src/theme/variables.scss" 267 | }, 268 | { 269 | "input": "src/global.scss" 270 | } 271 | ], 272 | "scripts": [ 273 | "node_modules/systemjs/dist/s.js", 274 | "node_modules/systemjs/dist/extras/named-register.js", 275 | "node_modules/systemjs/dist/extras/amd.js" 276 | ] 277 | }, 278 | "configurations": { 279 | "production": { 280 | "fileReplacements": [ 281 | { 282 | "replace": "src/environments/environment.ts", 283 | "with": "src/environments/environment.prod.ts" 284 | } 285 | ], 286 | "optimization": true, 287 | "outputHashing": "all", 288 | "sourceMap": false, 289 | "namedChunks": false, 290 | "aot": true, 291 | "extractLicenses": true, 292 | "vendorChunk": false, 293 | "buildOptimizer": true, 294 | "budgets": [ 295 | { 296 | "type": "initial", 297 | "maximumWarning": "2mb", 298 | "maximumError": "5mb" 299 | } 300 | ] 301 | }, 302 | "ci": { 303 | "budgets": [ 304 | { 305 | "type": "anyComponentStyle", 306 | "maximumWarning": "6kb" 307 | } 308 | ], 309 | "progress": false 310 | } 311 | } 312 | }, 313 | "serve": { 314 | "builder": "@angular-devkit/build-angular:dev-server", 315 | "options": { 316 | "browserTarget": "wako-like:build:production", 317 | "disableHostCheck": true, 318 | "host": "0.0.0.0" 319 | }, 320 | "configurations": { 321 | "production": { 322 | "browserTarget": "wako-like:build:production" 323 | } 324 | } 325 | }, 326 | "extract-i18n": { 327 | "builder": "@angular-devkit/build-angular:extract-i18n", 328 | "options": { 329 | "browserTarget": "wako-like:build" 330 | } 331 | } 332 | } 333 | } 334 | }, 335 | "cli": { 336 | "schematicCollections": [ 337 | "@ionic/angular-toolkit" 338 | ] 339 | }, 340 | "schematics": { 341 | "@ionic/angular-toolkit:component": { 342 | "styleext": "scss" 343 | }, 344 | "@ionic/angular-toolkit:page": { 345 | "styleext": "scss" 346 | } 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /dist/3rdpartylicenses.txt: -------------------------------------------------------------------------------- 1 | @ionic/storage-angular 2 | MIT 3 | 4 | rxjs 5 | Apache-2.0 6 | Apache License 7 | Version 2.0, January 2004 8 | http://www.apache.org/licenses/ 9 | 10 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 11 | 12 | 1. Definitions. 13 | 14 | "License" shall mean the terms and conditions for use, reproduction, 15 | and distribution as defined by Sections 1 through 9 of this document. 16 | 17 | "Licensor" shall mean the copyright owner or entity authorized by 18 | the copyright owner that is granting the License. 19 | 20 | "Legal Entity" shall mean the union of the acting entity and all 21 | other entities that control, are controlled by, or are under common 22 | control with that entity. For the purposes of this definition, 23 | "control" means (i) the power, direct or indirect, to cause the 24 | direction or management of such entity, whether by contract or 25 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 26 | outstanding shares, or (iii) beneficial ownership of such entity. 27 | 28 | "You" (or "Your") shall mean an individual or Legal Entity 29 | exercising permissions granted by this License. 30 | 31 | "Source" form shall mean the preferred form for making modifications, 32 | including but not limited to software source code, documentation 33 | source, and configuration files. 34 | 35 | "Object" form shall mean any form resulting from mechanical 36 | transformation or translation of a Source form, including but 37 | not limited to compiled object code, generated documentation, 38 | and conversions to other media types. 39 | 40 | "Work" shall mean the work of authorship, whether in Source or 41 | Object form, made available under the License, as indicated by a 42 | copyright notice that is included in or attached to the work 43 | (an example is provided in the Appendix below). 44 | 45 | "Derivative Works" shall mean any work, whether in Source or Object 46 | form, that is based on (or derived from) the Work and for which the 47 | editorial revisions, annotations, elaborations, or other modifications 48 | represent, as a whole, an original work of authorship. For the purposes 49 | of this License, Derivative Works shall not include works that remain 50 | separable from, or merely link (or bind by name) to the interfaces of, 51 | the Work and Derivative Works thereof. 52 | 53 | "Contribution" shall mean any work of authorship, including 54 | the original version of the Work and any modifications or additions 55 | to that Work or Derivative Works thereof, that is intentionally 56 | submitted to Licensor for inclusion in the Work by the copyright owner 57 | or by an individual or Legal Entity authorized to submit on behalf of 58 | the copyright owner. For the purposes of this definition, "submitted" 59 | means any form of electronic, verbal, or written communication sent 60 | to the Licensor or its representatives, including but not limited to 61 | communication on electronic mailing lists, source code control systems, 62 | and issue tracking systems that are managed by, or on behalf of, the 63 | Licensor for the purpose of discussing and improving the Work, but 64 | excluding communication that is conspicuously marked or otherwise 65 | designated in writing by the copyright owner as "Not a Contribution." 66 | 67 | "Contributor" shall mean Licensor and any individual or Legal Entity 68 | on behalf of whom a Contribution has been received by Licensor and 69 | subsequently incorporated within the Work. 70 | 71 | 2. Grant of Copyright License. Subject to the terms and conditions of 72 | this License, each Contributor hereby grants to You a perpetual, 73 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 74 | copyright license to reproduce, prepare Derivative Works of, 75 | publicly display, publicly perform, sublicense, and distribute the 76 | Work and such Derivative Works in Source or Object form. 77 | 78 | 3. Grant of Patent License. Subject to the terms and conditions of 79 | this License, each Contributor hereby grants to You a perpetual, 80 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 81 | (except as stated in this section) patent license to make, have made, 82 | use, offer to sell, sell, import, and otherwise transfer the Work, 83 | where such license applies only to those patent claims licensable 84 | by such Contributor that are necessarily infringed by their 85 | Contribution(s) alone or by combination of their Contribution(s) 86 | with the Work to which such Contribution(s) was submitted. If You 87 | institute patent litigation against any entity (including a 88 | cross-claim or counterclaim in a lawsuit) alleging that the Work 89 | or a Contribution incorporated within the Work constitutes direct 90 | or contributory patent infringement, then any patent licenses 91 | granted to You under this License for that Work shall terminate 92 | as of the date such litigation is filed. 93 | 94 | 4. Redistribution. You may reproduce and distribute copies of the 95 | Work or Derivative Works thereof in any medium, with or without 96 | modifications, and in Source or Object form, provided that You 97 | meet the following conditions: 98 | 99 | (a) You must give any other recipients of the Work or 100 | Derivative Works a copy of this License; and 101 | 102 | (b) You must cause any modified files to carry prominent notices 103 | stating that You changed the files; and 104 | 105 | (c) You must retain, in the Source form of any Derivative Works 106 | that You distribute, all copyright, patent, trademark, and 107 | attribution notices from the Source form of the Work, 108 | excluding those notices that do not pertain to any part of 109 | the Derivative Works; and 110 | 111 | (d) If the Work includes a "NOTICE" text file as part of its 112 | distribution, then any Derivative Works that You distribute must 113 | include a readable copy of the attribution notices contained 114 | within such NOTICE file, excluding those notices that do not 115 | pertain to any part of the Derivative Works, in at least one 116 | of the following places: within a NOTICE text file distributed 117 | as part of the Derivative Works; within the Source form or 118 | documentation, if provided along with the Derivative Works; or, 119 | within a display generated by the Derivative Works, if and 120 | wherever such third-party notices normally appear. The contents 121 | of the NOTICE file are for informational purposes only and 122 | do not modify the License. You may add Your own attribution 123 | notices within Derivative Works that You distribute, alongside 124 | or as an addendum to the NOTICE text from the Work, provided 125 | that such additional attribution notices cannot be construed 126 | as modifying the License. 127 | 128 | You may add Your own copyright statement to Your modifications and 129 | may provide additional or different license terms and conditions 130 | for use, reproduction, or distribution of Your modifications, or 131 | for any such Derivative Works as a whole, provided Your use, 132 | reproduction, and distribution of the Work otherwise complies with 133 | the conditions stated in this License. 134 | 135 | 5. Submission of Contributions. Unless You explicitly state otherwise, 136 | any Contribution intentionally submitted for inclusion in the Work 137 | by You to the Licensor shall be under the terms and conditions of 138 | this License, without any additional terms or conditions. 139 | Notwithstanding the above, nothing herein shall supersede or modify 140 | the terms of any separate license agreement you may have executed 141 | with Licensor regarding such Contributions. 142 | 143 | 6. Trademarks. This License does not grant permission to use the trade 144 | names, trademarks, service marks, or product names of the Licensor, 145 | except as required for reasonable and customary use in describing the 146 | origin of the Work and reproducing the content of the NOTICE file. 147 | 148 | 7. Disclaimer of Warranty. Unless required by applicable law or 149 | agreed to in writing, Licensor provides the Work (and each 150 | Contributor provides its Contributions) on an "AS IS" BASIS, 151 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 152 | implied, including, without limitation, any warranties or conditions 153 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 154 | PARTICULAR PURPOSE. You are solely responsible for determining the 155 | appropriateness of using or redistributing the Work and assume any 156 | risks associated with Your exercise of permissions under this License. 157 | 158 | 8. Limitation of Liability. In no event and under no legal theory, 159 | whether in tort (including negligence), contract, or otherwise, 160 | unless required by applicable law (such as deliberate and grossly 161 | negligent acts) or agreed to in writing, shall any Contributor be 162 | liable to You for damages, including any direct, indirect, special, 163 | incidental, or consequential damages of any character arising as a 164 | result of this License or out of the use or inability to use the 165 | Work (including but not limited to damages for loss of goodwill, 166 | work stoppage, computer failure or malfunction, or any and all 167 | other commercial damages or losses), even if such Contributor 168 | has been advised of the possibility of such damages. 169 | 170 | 9. Accepting Warranty or Additional Liability. While redistributing 171 | the Work or Derivative Works thereof, You may choose to offer, 172 | and charge a fee for, acceptance of support, warranty, indemnity, 173 | or other liability obligations and/or rights consistent with this 174 | License. However, in accepting such obligations, You may act only 175 | on Your own behalf and on Your sole responsibility, not on behalf 176 | of any other Contributor, and only if You agree to indemnify, 177 | defend, and hold each Contributor harmless for any liability 178 | incurred by, or claims asserted against, such Contributor by reason 179 | of your accepting any such warranty or additional liability. 180 | 181 | END OF TERMS AND CONDITIONS 182 | 183 | APPENDIX: How to apply the Apache License to your work. 184 | 185 | To apply the Apache License to your work, attach the following 186 | boilerplate notice, with the fields enclosed by brackets "[]" 187 | replaced with your own identifying information. (Don't include 188 | the brackets!) The text should be enclosed in the appropriate 189 | comment syntax for the file format. We also recommend that a 190 | file or class name and description of purpose be included on the 191 | same "printed page" as the copyright notice for easier 192 | identification within third-party archives. 193 | 194 | Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors 195 | 196 | Licensed under the Apache License, Version 2.0 (the "License"); 197 | you may not use this file except in compliance with the License. 198 | You may obtain a copy of the License at 199 | 200 | http://www.apache.org/licenses/LICENSE-2.0 201 | 202 | Unless required by applicable law or agreed to in writing, software 203 | distributed under the License is distributed on an "AS IS" BASIS, 204 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 205 | See the License for the specific language governing permissions and 206 | limitations under the License. 207 | -------------------------------------------------------------------------------- /dist/i18n/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wako-unofficial-addons/nomos/98c2dc864679bec9fca15b22eb8dd966fcfe927e/dist/i18n/.gitkeep -------------------------------------------------------------------------------- /dist/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": { 3 | "kodi-open-button": { 4 | "title": "Kodi Open Button", 5 | "list": { 6 | "header": "Setup", 7 | "pluginsUrl": "Plugins URL:", 8 | "noUrl": "Click here to add one" 9 | }, 10 | "openRemoteList": { 11 | "header": "Remote", 12 | "item": "Open remote after clicking" 13 | }, 14 | "pluginList": { 15 | "header": "Plugins" 16 | } 17 | } 18 | }, 19 | "toasts": { 20 | "kodi": { 21 | "open": "Open on {{hostName}} using {{pluginName}}", 22 | "hostUnreachable": "Ouch, your kodi host {{hostName}} is unreachable", 23 | "noHost": "No host set for kodi", 24 | "noSupportedPluginsInstalled": "Ouch, None of the supported plugins are installed, please install at least one of them", 25 | "failedToOpen": "Failed to open" 26 | }, 27 | "kodi-open-button": { 28 | "plugingUrlAdded": "Plugins added", 29 | "plugingUrlFailedToAdd": "Failed to add plugins, please check the given URL" 30 | }, 31 | "pluginsUpdated": "Plugins have been updated" 32 | }, 33 | "shared": { 34 | "buttonOpenKodi": "Open on Kodi" 35 | }, 36 | "actionSheets": { 37 | "kodi": { 38 | "openTitle": "Open with Kodi using..." 39 | } 40 | }, 41 | "alerts": { 42 | "okButton": "Ok", 43 | "cancelButton": "Cancel" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /dist/i18n/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": { 3 | "kodi-open-button": { 4 | "title": "Bouton Ouvrir", 5 | "list": { 6 | "header": "Configuration", 7 | "pluginsUrl": "Plugins URL:", 8 | "noUrl": "aucune" 9 | }, 10 | "openRemoteList": { 11 | "header": "Télécommande", 12 | "item": "Ouvrir la télécommande après avoir cliqué" 13 | }, 14 | "pluginList": { 15 | "header": "Plugins" 16 | } 17 | } 18 | }, 19 | "toasts": { 20 | "kodi": { 21 | "noSupportedPluginsInstalled": "Aïe, aucun des plugins supportés n'est installé, veuillez en installer au moins un.", 22 | "failedToOpen": "N'a pas réussi à ouvrir" 23 | }, 24 | "pluginsUpdated": "Les plugins ont été mis à jour" 25 | }, 26 | "shared": { 27 | "buttonOpenKodi": "Ouvrir avec Kodi" 28 | }, 29 | "actionSheets": { 30 | "kodi": { 31 | "openTitle": "Ouvrir avec Kodi via..." 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /dist/i18n/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": { 3 | "kodi-open-button": { 4 | "title": "Przycisk odtwórz w Kodi", 5 | "list": { 6 | "header": "Konfiguracja", 7 | "pluginsUrl": "Adres URL wtyczek:", 8 | "noUrl": "nic nie ustawiono" 9 | }, 10 | "openRemoteList": { 11 | "header": "Pilot", 12 | "item": "Otwórz pilota po kliknięciu" 13 | }, 14 | "pluginList": { 15 | "header": "Wtyczki" 16 | } 17 | } 18 | }, 19 | "toasts": { 20 | "kodi": { 21 | "noSupportedPluginsInstalled": "Ałć... Żadna ze wspieranych wtyczek nie jest zainstalowana, zainstaluj przynajmniej jedną ze wspieranych wtyczek.", 22 | "failedToOpen": "Błąd podczas otwierania" 23 | }, 24 | "pluginsUpdated": "Wtyczki zostały zaktualizowane" 25 | }, 26 | "shared": { 27 | "buttonOpenKodi": "Odtwórz na Kodi" 28 | }, 29 | "actionSheets": { 30 | "kodi": { 31 | "openTitle": "Odtwórz na Kodi używając..." 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /dist/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/@wako-app/mobile-sdk/dist/manifest-schema.json", 3 | "version": "1.1.4", 4 | "id": "plugin.nomos", 5 | "name": "Nomos", 6 | "author": "Davy", 7 | "description": "Allow to open Movie/TV Show on your favorite Kodi add-on", 8 | "actions": ["movies", "episodes", "episodes-item-option"], 9 | "entryPointV3": "https://raw.githubusercontent.com/wako-unofficial-addons/nomos/master/dist/plugin.js", 10 | "languages": { 11 | "en": "https://raw.githubusercontent.com/wako-unofficial-addons/nomos/master/dist/i18n/en.json", 12 | "fr": "https://raw.githubusercontent.com/wako-unofficial-addons/nomos/master/dist/i18n/fr.json", 13 | "pl": "https://raw.githubusercontent.com/wako-unofficial-addons/nomos/master/dist/i18n/pl.json" 14 | }, 15 | "changeLogs": { 16 | "1.1.4": "Use latest SDK", 17 | "1.1.2": "Upgrade deps", 18 | "1.1.1": "Initial version" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /dist/plugin.js: -------------------------------------------------------------------------------- 1 | !function(_,k){"object"==typeof exports&&"object"==typeof module?module.exports=k(require("ionic.angular"),require("ionic.storage"),require("ng.common"),require("ng.core"),require("ng.forms"),require("ngx-translate.core"),require("rxjs"),require("tslib"),require("wako-app.mobile-sdk")):"function"==typeof define&&define.amd?define(["ionic.angular","ionic.storage","ng.common","ng.core","ng.forms","ngx-translate.core","rxjs","tslib","wako-app.mobile-sdk"],k):"object"==typeof exports?exports.plugin=k(require("ionic.angular"),require("ionic.storage"),require("ng.common"),require("ng.core"),require("ng.forms"),require("ngx-translate.core"),require("rxjs"),require("tslib"),require("wako-app.mobile-sdk")):_.plugin=k(_["ionic.angular"],_["ionic.storage"],_["ng.common"],_["ng.core"],_["ng.forms"],_["ngx-translate.core"],_.rxjs,_.tslib,_["wako-app.mobile-sdk"])}(typeof self<"u"?self:this,(W,_,k,be,ye,Se,Pe,we,Ce)=>(()=>{"use strict";var ke={372:d=>{d.exports=W},278:d=>{d.exports=_},223:d=>{d.exports=k},888:d=>{d.exports=be},951:d=>{d.exports=ye},584:d=>{d.exports=Se},832:d=>{d.exports=Pe},748:d=>{d.exports=we},95:d=>{d.exports=Ce}},J={};function m(d){var p=J[d];if(void 0!==p)return p.exports;var c=J[d]={exports:{}};return ke[d](c,c.exports,m),c.exports}m.d=(d,p)=>{for(var c in p)m.o(p,c)&&!m.o(d,c)&&Object.defineProperty(d,c,{enumerable:!0,get:p[c]})},m.o=(d,p)=>Object.prototype.hasOwnProperty.call(d,p),m.r=d=>{typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(d,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(d,"__esModule",{value:!0})};var K={};return(()=>{m.r(K),m.d(K,{PluginModule:()=>ve,default:()=>dt});var d=m(223),p=m(372),c=m(95),o=m(888);function Q(t,n,e,i,r,s,l){try{var a=t[s](l),u=a.value}catch(f){return void e(f)}a.done?n(u):Promise.resolve(u).then(i,r)}function X(t){return function(){var n=this,e=arguments;return new Promise(function(i,r){var s=t.apply(n,e);function l(u){Q(s,i,r,l,a,"next",u)}function a(u){Q(s,i,r,l,a,"throw",u)}l(void 0)})}}var v=m(832),h=m(748);function g(t){return"function"==typeof t}const L=function Ee(t){const e=t(i=>{Error.call(i),i.stack=(new Error).stack});return e.prototype=Object.create(Error.prototype),e.prototype.constructor=e,e}(t=>function(e){t(this),this.message=e?`${e.length} errors occurred during unsubscription:\n${e.map((i,r)=>`${r+1}) ${i.toString()}`).join("\n ")}`:"",this.name="UnsubscriptionError",this.errors=e});function Z(t,n){if(t){const e=t.indexOf(n);0<=e&&t.splice(e,1)}}class S{constructor(n){this.initialTeardown=n,this.closed=!1,this._parentage=null,this._finalizers=null}unsubscribe(){let n;if(!this.closed){this.closed=!0;const{_parentage:e}=this;if(e)if(this._parentage=null,Array.isArray(e))for(const s of e)s.remove(this);else e.remove(this);const{initialTeardown:i}=this;if(g(i))try{i()}catch(s){n=s instanceof L?s.errors:[s]}const{_finalizers:r}=this;if(r){this._finalizers=null;for(const s of r)try{te(s)}catch(l){n=n??[],l instanceof L?n=[...n,...l.errors]:n.push(l)}}if(n)throw new L(n)}}add(n){var e;if(n&&n!==this)if(this.closed)te(n);else{if(n instanceof S){if(n.closed||n._hasParent(this))return;n._addParent(this)}(this._finalizers=null!==(e=this._finalizers)&&void 0!==e?e:[]).push(n)}}_hasParent(n){const{_parentage:e}=this;return e===n||Array.isArray(e)&&e.includes(n)}_addParent(n){const{_parentage:e}=this;this._parentage=Array.isArray(e)?(e.push(n),e):e?[e,n]:n}_removeParent(n){const{_parentage:e}=this;e===n?this._parentage=null:Array.isArray(e)&&Z(e,n)}remove(n){const{_finalizers:e}=this;e&&Z(e,n),n instanceof S&&n._removeParent(this)}}function ee(t){return t instanceof S||t&&"closed"in t&&g(t.remove)&&g(t.add)&&g(t.unsubscribe)}function te(t){g(t)?t():t.unsubscribe()}S.EMPTY=(()=>{const t=new S;return t.closed=!0,t})();const P={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1},M={setTimeout(t,n,...e){const{delegate:i}=M;return i?.setTimeout?i.setTimeout(t,n,...e):setTimeout(t,n,...e)},clearTimeout(t){const{delegate:n}=M;return(n?.clearTimeout||clearTimeout)(t)},delegate:void 0};function ne(t){M.setTimeout(()=>{const{onUnhandledError:n}=P;if(!n)throw t;n(t)})}function ie(){}const Oe=N("C",void 0,void 0);function N(t,n,e){return{kind:t,value:n,error:e}}let w=null;class D extends S{constructor(n){super(),this.isStopped=!1,n?(this.destination=n,ee(n)&&n.add(this)):this.destination=Re}static create(n,e,i){return new H(n,e,i)}next(n){this.isStopped?z(function Ae(t){return N("N",t,void 0)}(n),this):this._next(n)}error(n){this.isStopped?z(function Te(t){return N("E",void 0,t)}(n),this):(this.isStopped=!0,this._error(n))}complete(){this.isStopped?z(Oe,this):(this.isStopped=!0,this._complete())}unsubscribe(){this.closed||(this.isStopped=!0,super.unsubscribe(),this.destination=null)}_next(n){this.destination.next(n)}_error(n){try{this.destination.error(n)}finally{this.unsubscribe()}}_complete(){try{this.destination.complete()}finally{this.unsubscribe()}}}const je=Function.prototype.bind;function q(t,n){return je.call(t,n)}class Fe{constructor(n){this.partialObserver=n}next(n){const{partialObserver:e}=this;if(e.next)try{e.next(n)}catch(i){U(i)}}error(n){const{partialObserver:e}=this;if(e.error)try{e.error(n)}catch(i){U(i)}else U(n)}complete(){const{partialObserver:n}=this;if(n.complete)try{n.complete()}catch(e){U(e)}}}class H extends D{constructor(n,e,i){let r;if(super(),g(n)||!n)r={next:n??void 0,error:e??void 0,complete:i??void 0};else{let s;this&&P.useDeprecatedNextContext?(s=Object.create(n),s.unsubscribe=()=>this.unsubscribe(),r={next:n.next&&q(n.next,s),error:n.error&&q(n.error,s),complete:n.complete&&q(n.complete,s)}):r=n}this.destination=new Fe(r)}}function U(t){P.useDeprecatedSynchronousErrorHandling?function Ue(t){P.useDeprecatedSynchronousErrorHandling&&w&&(w.errorThrown=!0,w.error=t)}(t):ne(t)}function z(t,n){const{onStoppedNotification:e}=P;e&&M.setTimeout(()=>e(t,n))}const Re={closed:!0,next:ie,error:function Be(t){throw t},complete:ie},V="function"==typeof Symbol&&Symbol.observable||"@@observable";function re(t){return t}let x=(()=>{class t{constructor(e){e&&(this._subscribe=e)}lift(e){const i=new t;return i.source=this,i.operator=e,i}subscribe(e,i,r){const s=function Le(t){return t&&t instanceof D||function Ke(t){return t&&g(t.next)&&g(t.error)&&g(t.complete)}(t)&&ee(t)}(e)?e:new H(e,i,r);return function Me(t){if(P.useDeprecatedSynchronousErrorHandling){const n=!w;if(n&&(w={errorThrown:!1,error:null}),t(),n){const{errorThrown:e,error:i}=w;if(w=null,e)throw i}}else t()}(()=>{const{operator:l,source:a}=this;s.add(l?l.call(s,a):a?this._subscribe(s):this._trySubscribe(s))}),s}_trySubscribe(e){try{return this._subscribe(e)}catch(i){e.error(i)}}forEach(e,i){return new(i=se(i))((r,s)=>{const l=new H({next:a=>{try{e(a)}catch(u){s(u),l.unsubscribe()}},error:s,complete:r});this.subscribe(l)})}_subscribe(e){var i;return null===(i=this.source)||void 0===i?void 0:i.subscribe(e)}[V](){return this}pipe(...e){return function oe(t){return 0===t.length?re:1===t.length?t[0]:function(e){return t.reduce((i,r)=>r(i),e)}}(e)(this)}toPromise(e){return new(e=se(e))((i,r)=>{let s;this.subscribe(l=>s=l,l=>r(l),()=>i(s))})}}return t.create=n=>new t(n),t})();function se(t){var n;return null!==(n=t??P.Promise)&&void 0!==n?n:Promise}const ze=function He(){return"function"==typeof Symbol&&Symbol.iterator?Symbol.iterator:"@@iterator"}();function le(t){if(t instanceof x)return t;if(null!=t){if(function Ne(t){return g(t[V])}(t))return function Ye(t){return new x(n=>{const e=t[V]();if(g(e.subscribe))return e.subscribe(n);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}(t);if((t=>t&&"number"==typeof t.length&&"function"!=typeof t)(t))return function We(t){return new x(n=>{for(let e=0;e{t.then(e=>{n.closed||(n.next(e),n.complete())},e=>n.error(e)).then(null,ne)})}(t);if(function De(t){return Symbol.asyncIterator&&g(t?.[Symbol.asyncIterator])}(t))return ae(t);if(function Ve(t){return g(t?.[ze])}(t))return function Qe(t){return new x(n=>{for(const e of t)if(n.next(e),n.closed)return;n.complete()})}(t);if(function $e(t){return g(t?.getReader)}(t))return function Xe(t){return ae(function Ge(t){return(0,h.__asyncGenerator)(this,arguments,function*(){const e=t.getReader();try{for(;;){const{value:i,done:r}=yield(0,h.__await)(e.read());if(r)return yield(0,h.__await)(void 0);yield yield(0,h.__await)(i)}}finally{e.releaseLock()}})}(t))}(t)}throw function qe(t){return new TypeError(`You provided ${null!==t&&"object"==typeof t?"an invalid object":`'${t}'`} where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.`)}(t)}function ae(t){return new x(n=>{(function Ze(t,n){var e,i,r,s;return(0,h.__awaiter)(this,void 0,void 0,function*(){try{for(e=(0,h.__asyncValues)(t);!(i=yield e.next()).done;)if(n.next(i.value),n.closed)return}catch(l){r={error:l}}finally{try{i&&!i.done&&(s=e.return)&&(yield s.call(e))}finally{if(r)throw r.error}}n.complete()})})(t,n).catch(e=>n.error(e))})}function E(t){return n=>{if(function et(t){return g(t?.lift)}(n))return n.lift(function(e){try{return t(e,this)}catch(i){this.error(i)}});throw new TypeError("Unable to lift unknown Observable type")}}function O(t,n,e,i,r){return new tt(t,n,e,i,r)}class tt extends D{constructor(n,e,i,r,s,l){super(n),this.onFinalize=s,this.shouldUnsubscribe=l,this._next=e?function(a){try{e(a)}catch(u){n.error(u)}}:super._next,this._error=r?function(a){try{r(a)}catch(u){n.error(u)}finally{this.unsubscribe()}}:super._error,this._complete=i?function(){try{i()}catch(a){n.error(a)}finally{this.unsubscribe()}}:super._complete}unsubscribe(){var n;if(!this.shouldUnsubscribe||this.shouldUnsubscribe()){const{closed:e}=this;super.unsubscribe(),!e&&(null===(n=this.onFinalize)||void 0===n||n.call(this))}}}function b(t,n){return E((e,i)=>{let r=null,s=0,l=!1;const a=()=>l&&!r&&i.complete();e.subscribe(O(i,u=>{r?.unsubscribe();let f=0;const R=s++;le(t(u,R)).subscribe(r=O(i,A=>i.next(n?n(u,A,R,f++):A),()=>{r=null,a()}))},()=>{l=!0,a()}))})}function T(t,n){return E((e,i)=>{let r=0;e.subscribe(O(i,s=>{i.next(t.call(n,s,r++))}))})}function j(t){return E((n,e)=>{let s,i=null,r=!1;i=n.subscribe(O(e,void 0,void 0,l=>{s=le(t(l,j(t)(n))),i?(i.unsubscribe(),i=null,s.subscribe(e)):r=!0})),r&&(i.unsubscribe(),i=null,s.subscribe(e))})}function ce(t,n,e){const i=g(t)||n||e?{next:t,error:n,complete:e}:t;return i?E((r,s)=>{var l;null===(l=i.subscribe)||void 0===l||l.call(i);let a=!0;r.subscribe(O(s,u=>{var f;null===(f=i.next)||void 0===f||f.call(i,u),s.next(u)},()=>{var u;a=!1,null===(u=i.complete)||void 0===u||u.call(i),s.complete()},u=>{var f;a=!1,null===(f=i.error)||void 0===f||f.call(i,u),s.error(u)},()=>{var u,f;a&&(null===(u=i.unsubscribe)||void 0===u||u.call(i)),null===(f=i.finalize)||void 0===f||f.call(i)}))}):re}function y(...t){(0,c.wakoLog)("plugin.nomos",t)}var C=m(584);let G=(()=>{class t{constructor(e,i){this.toastCtrl=e,this.translateService=i}simpleMessage(e,i,r=2e3,s="top"){this.translateService.get(e,i).subscribe(l=>{this.toastCtrl.create({message:l,mode:"ios",position:s,duration:r}).then(a=>{a.present()})})}}return t.\u0275fac=function(e){return new(e||t)(o.\u0275\u0275inject(p.ToastController),o.\u0275\u0275inject(C.TranslateService))},t.\u0275prov=o.\u0275\u0275defineInjectable({token:t,factory:t.\u0275fac}),t})();const ue="CACHE_KEY_PLUGINS";let F=(()=>{class t{constructor(e,i,r,s){this.toastService=e,this.storage=i,this.actionSheetController=r,this.translateService=s}patchStorageKey(){var e=this;return X(function*(){const i=yield e.storage.get("kodi-plugins-url");i&&(yield e.storage.set(e.getStorageKeyPrefixed("kodi-plugins-url"),i),yield e.storage.remove("kodi-plugins-url"));const r=yield e.storage.get("kodi-plugins-list");r&&(yield e.storage.set(e.getStorageKeyPrefixed("kodi-plugins-list"),r),yield e.storage.remove("kodi-plugins-list"));const s=yield e.storage.get("openRemoteAfterClickOnPlay");s&&(yield e.storage.set(e.getStorageKeyPrefixed("openRemoteAfterClickOnPlay"),s),yield e.storage.remove("openRemoteAfterClickOnPlay"))})()}getStorageKeyPrefixed(e){return`nomos_${e}`}getPluginUrl(){return this.storage.get(this.getStorageKeyPrefixed("kodi-plugins-url"))}setPluginUrl(e){return this.storage.set(this.getStorageKeyPrefixed("kodi-plugins-url"),e)}getPlugins(){return this.storage.get(this.getStorageKeyPrefixed("kodi-plugins-list"))}setPlugins(e,i=!1){return this.getPlugins().then(r=>{if(r&&i){let s=Object.keys(r).length===Object.keys(e).length;s&&Object.keys(r).forEach(l=>{const a=Object.assign({},r[l]),u=Object.assign({},e[l]);a.enabled=!0,u.enabled=!0,JSON.stringify(a)!==JSON.stringify(u)&&(s=!1)}),s?y("no changes in plugins"):this.toastService.simpleMessage("toasts.pluginsUpdated"),Object.keys(r).forEach(l=>{e.hasOwnProperty(l)&&(e[l].enabled=r[l].enabled)})}return this.storage.set(this.getStorageKeyPrefixed("kodi-plugins-list"),e)})}setPluginsFromUrl(e,i=!1){return e=e.trim(),c.WakoHttpRequestService.request({url:e,method:"GET"},null,1e4,!0).pipe(b(r=>"string"==typeof r||0===Object.keys(r).length?(0,v.of)(!1):(i||Object.keys(r).forEach(s=>{r[s].enabled=!0}),(0,v.from)(this.setPlugins(r,i)).pipe(b(()=>(0,v.from)(this.setPluginUrl(e))),T(()=>!0)))),j(()=>(0,v.of)(!1)))}refreshPlugins(){c.WakoCacheService.get(ue).subscribe(e=>{e?y("check plugins later"):(this.getPluginUrl().then(i=>{i&&this.setPluginsFromUrl(i,!0).subscribe()}),c.WakoCacheService.set(ue,!0,"1d"))})}getOpenRemoteAfterClickOnPlaySetting(){return this.storage.get(this.getStorageKeyPrefixed("openRemoteAfterClickOnPlay")).then(e=>!!e)}setOpenRemoteAfterClickOnPlaySetting(e){return this.storage.set(this.getStorageKeyPrefixed("openRemoteAfterClickOnPlay"),e)}getPluginsByCategory(e){return(0,v.from)(this.getPlugins()).pipe(T(i=>{if(!i)return null;const r={};return Object.keys(i).forEach(s=>{const l=i[s];!l.enabled||("movie"===e&&l.movieUrl||"episode"===e&&l.tvUrl)&&(r[s]=l)}),r}))}getInstalledPlugins(){return c.KodiGetAddonsForm.submit().pipe(j(()=>(0,v.of)({addons:[]})),T(e=>{const i=[];return e.addons.forEach(r=>{i.push(r.addonid)}),i}))}open(e){let i={};c.KodiAppService.checkAndConnectToCurrentHost().pipe(j(r=>("hostUnreachable"===r?this.toastService.simpleMessage("toasts.kodi.hostUnreachable",{hostName:c.KodiAppService.currentHost.name},2e3):this.toastService.simpleMessage("toasts.kodi.noHost"),v.NEVER)),b(()=>(y("CONNECTED"),this.getPluginsByCategory(e.category).pipe(ce(r=>{i=r||{}})))),b(()=>this.getInstalledPlugins()),T(r=>{y("_installedPlugins",r);const s=[];return Object.keys(i).forEach(l=>{r.includes(i[l].plugin)&&s.push(l)}),s})).subscribe(r=>{if(y("supportedAndInstalledPlugins",r),0===r.length)return void this.toastService.simpleMessage("toasts.kodi.noSupportedPluginsInstalled");if(1===r.length)return void this.openOnPlugin(r[0],e);const s=[];Object.keys(i).forEach(l=>{const a=i[l];r.includes(l)&&s.push({text:a.name,handler:()=>{this.openOnPlugin(l,e)}})}),y("buttons",s),1!==s.length?this.actionSheetController.create({header:this.translateService.instant("actionSheets.kodi.openTitle"),buttons:s}).then(l=>{l.present()}):s[0].handler()})}openViaPlaylist(e,i,r){return(0,v.of)(!0).pipe(b(()=>c.KodiApiService.doHttpAction("Playlist.Clear",{playlistid:0})),b(()=>c.KodiApiService.doHttpAction("Playlist.Insert",{playlistid:0,item:{file:e},position:0})),b(()=>c.KodiAppService.stopPlayingIfAny()),b(()=>c.KodiApiService.doHttpAction("Player.open",{item:{playlistid:0}})),T(()=>(r&&c.EventService.emit(c.EventCategory.kodiRemote,c.EventName.open),i&&c.KodiAppService.openMedia$.next(i),c.EventService.emit(c.EventCategory.kodi,c.EventName.open),!0)))}openOnPlugin(e,i){this.getPlugins().then(r=>{const s=r[e],a={hostName:c.KodiApiService.host.name,pluginName:s.name},u=this.getKodiPluginParams(s,i);return(0,v.from)(this.getOpenRemoteAfterClickOnPlaySetting()).pipe(b(f=>{const R={movieIds:i.movie?i.movie.ids:null,showIds:i.show?i.show.ids:null,seasonNumber:i.episode?i.episode.seasonNumber:null,episodeNumber:i.episode?i.episode.number:null},A=`plugin://${s.plugin}${u}`;return y("Opening: "+A),this.openViaPlaylist(A,R,f)}),ce(f=>{f?this.toastService.simpleMessage("toasts.kodi.open",a):this.toastService.simpleMessage("toasts.kodi.failedToOpen")})).subscribe()})}replacePlus(e){return e?e.replace(/\s/g,"+"):""}getKodiPluginParams(e,i){if(i.movie){const r=i.movie,s={tmdbId:r.ids.tmdb,rating:r.rating,date:r.released,title:r.title,tagline:r.tagline,plot:r.overview,original_title:r.originalTitle,imdb:r.ids.imdb,year:r.year,duration:60*r.runtime,mpaa:r.certification,genres:r.genres.join(" / "),fanart:r.imagesUrl.backdropOriginal,poster:r.imagesUrl.posterOriginal,id:r.ids.tmdb,slug:"",trakt:r.ids.trakt,showTmdbId:null,seasonNumber:null,episodeNumber:null,"title_+":this.replacePlus(r.title),tvdb:null,season:null,episode:null,"clearname_+":this.replacePlus(r.title),firstaired:r.released,"plot_+":this.replacePlus(r.overview),thumbnail:r.imagesUrl.backdropOriginal,now:Date.now().toString(),banner:r.imagesUrl.backdropOriginal,epimdb:null,eptmdb:null,eptrakt:null,epid:null,network_clean:"",clearname:r.title,epyear:null,tmdb:r.ids.tmdb,episodes:null,seasons:null,series_firstaired:null,seasons_no_specials:null,status:null,premiered:r.released};return this.getPluginParams(e.movieUrl,s)}{const r=i.show,s=i.episode;let l="2019-01-01";r.firstAired&&(l=r.firstAired);let a=new Date;"string"==typeof s.firstAired&&(a=new Date(s.firstAired));const u={showTmdbId:r.ids.tmdb,seasonNumber:s.seasonNumber,episodeNumber:s.number,"title_+":this.replacePlus(s.title),year:r.year,imdb:r.ids.imdb,tvdb:+r.ids.tvdb,season:s.seasonNumber,episode:s.number,"clearname_+":this.replacePlus(r.title),firstaired:l,duration:60*s.runtime,"plot_+":this.replacePlus(s.overview),thumbnail:r.imagesUrl.backdropOriginal,id:+r.ids.tvdb,poster:r.imagesUrl.posterOriginal,fanart:r.imagesUrl.backdropOriginal,now:Date.now().toString(),banner:r.imagesUrl.backdropOriginal,epimdb:s.ids.imdb,eptmdb:0,eptrakt:0,epid:+s.ids.tvdb,genres:"",mpaa:r.certification,title:s.title,plot:s.overview,rating:s.rating,network_clean:"",clearname:r.title,epyear:a?a.getFullYear():null,slug:"",tmdb:r.ids.tmdb,trakt:r.ids.trakt,episodes:1,seasons:1,series_firstaired:l,seasons_no_specials:1,status:"Continuing",tmdbId:r.ids.tmdb,date:l,tagline:"",original_title:s.title,premiered:l};return this.getPluginParams(e.tvUrl,u)}}getPluginParams(e,i){return Object.keys(i).forEach(r=>{const s=(0,c.escapeText)(i[r]);r.match(/\+/g)||(i[r]=encodeURIComponent(s))}),(0,c.replacer)(e,i)}}return t.\u0275fac=function(e){return new(e||t)(o.\u0275\u0275inject(G),o.\u0275\u0275inject(c.WakoStorage),o.\u0275\u0275inject(p.ActionSheetController),o.\u0275\u0275inject(C.TranslateService))},t.\u0275prov=o.\u0275\u0275defineInjectable({token:t,factory:t.\u0275fac}),t})();function it(t,n){if(1&t){const e=o.\u0275\u0275getCurrentView();o.\u0275\u0275elementContainerStart(0),o.\u0275\u0275elementStart(1,"ion-button",1),o.\u0275\u0275listener("click",function(){o.\u0275\u0275restoreView(e);const r=o.\u0275\u0275nextContext();return o.\u0275\u0275resetView(r.open())}),o.\u0275\u0275element(2,"ion-icon",2),o.\u0275\u0275elementStart(3,"ion-text",3),o.\u0275\u0275text(4),o.\u0275\u0275pipe(5,"translate"),o.\u0275\u0275elementEnd()(),o.\u0275\u0275elementContainerEnd()}2&t&&(o.\u0275\u0275advance(4),o.\u0275\u0275textInterpolate(o.\u0275\u0275pipeBind1(5,1,"shared.buttonOpenKodi")))}function rt(t,n){if(1&t){const e=o.\u0275\u0275getCurrentView();o.\u0275\u0275elementContainerStart(0),o.\u0275\u0275elementStart(1,"div",4)(2,"ion-item-option",5),o.\u0275\u0275listener("click",function(){o.\u0275\u0275restoreView(e);const r=o.\u0275\u0275nextContext();return o.\u0275\u0275resetView(r.open())}),o.\u0275\u0275elementStart(3,"div",6),o.\u0275\u0275element(4,"ion-icon",7),o.\u0275\u0275elementStart(5,"ion-text"),o.\u0275\u0275text(6," Nomos "),o.\u0275\u0275elementEnd()()()(),o.\u0275\u0275elementContainerEnd()}}let B=(()=>{class t{constructor(e){this.kodiPluginService=e,this.type="button"}open(){var e=this;return X(function*(){e.kodiPluginService.open({category:e.movie?"movie":"episode",movie:e.movie,show:e.show,episode:e.episode})})()}}return t.\u0275fac=function(e){return new(e||t)(o.\u0275\u0275directiveInject(F))},t.\u0275cmp=o.\u0275\u0275defineComponent({type:t,selectors:[["wk-open-button"]],inputs:{movie:"movie",show:"show",episode:"episode",type:"type"},decls:2,vars:2,consts:[[4,"ngIf"],["color","nomos","expand","block",1,"open-on-kodi",3,"click"],["name","play","color","light","slot","start"],["color","light"],[1,"ion-item-options"],["color","nomos",3,"click"],[1,"ion-item-detail"],["name","play"]],template:function(e,i){1&e&&(o.\u0275\u0275template(0,it,6,3,"ng-container",0),o.\u0275\u0275template(1,rt,7,0,"ng-container",0)),2&e&&(o.\u0275\u0275property("ngIf","button"===i.type),o.\u0275\u0275advance(1),o.\u0275\u0275property("ngIf","item-option"===i.type))},dependencies:[d.NgIf,p.IonButton,p.IonIcon,p.IonItemOption,p.IonText,C.TranslatePipe],styles:[".ion-color-nomos[_ngcontent-%COMP%]{--ion-color-base: #4b1d52;--ion-color-base-rgb: 53, 66, 82;--ion-color-contrast: #ffffff;--ion-color-contrast-rgb: 255, 255, 255;--ion-color-shade: #483247;--ion-color-tint: #635260}ion-button[_ngcontent-%COMP%]{--ripple-color: var(--ion-color-nomos);--background-activated: var(--ion-color-nomos);--background-focused: var(--ion-color-nomos)}.ion-item-options[_ngcontent-%COMP%]{display:flex;height:100%}.ion-item-options[_ngcontent-%COMP%] .ion-item-detail[_ngcontent-%COMP%]{width:60px;display:grid;text-align:center}.ion-item-options[_ngcontent-%COMP%] .ion-item-detail[_ngcontent-%COMP%] ion-icon[_ngcontent-%COMP%]{font-size:2em;width:100%}.ion-item-options[_ngcontent-%COMP%] .ion-item-detail[_ngcontent-%COMP%] ion-text[_ngcontent-%COMP%]{margin-top:3px;font-size:8px;text-transform:none}"]}),t})(),de=(()=>{class t extends c.MovieDetailBaseComponent{setMovie(e){this.movie=e}}return t.\u0275fac=function(){let n;return function(i){return(n||(n=o.\u0275\u0275getInheritedFactory(t)))(i||t)}}(),t.\u0275cmp=o.\u0275\u0275defineComponent({type:t,selectors:[["app-movie-button"]],features:[o.\u0275\u0275InheritDefinitionFeature],decls:1,vars:1,consts:[[3,"movie"]],template:function(e,i){1&e&&o.\u0275\u0275element(0,"wk-open-button",0),2&e&&o.\u0275\u0275property("movie",i.movie)},dependencies:[B]}),t})(),pe=(()=>{class t extends c.PluginBaseService{constructor(e,i){super(),this.translate=e,this.kodiPluginService=i}initialize(){y("plugin initialized"),setTimeout(()=>{this.kodiPluginService.refreshPlugins()},1e4),this.kodiPluginService.patchStorageKey()}afterInstall(){y("plugin installed")}setTranslation(e,i){this.translate.setDefaultLang(e),this.translate.use(e),this.translate.setTranslation(e,i)}customAction(e,i){}afterEpisodeMiddleware(e,i){return Promise.resolve(void 0)}afterMovieMiddleware(e){return Promise.resolve(void 0)}afterShowMiddleware(e){return Promise.resolve(void 0)}beforeEpisodeMiddleware(e,i){return Promise.resolve(void 0)}beforeMovieMiddleware(e){return Promise.resolve(void 0)}beforeShowMiddleware(e){return Promise.resolve(void 0)}fetchExplorerFolderItem(){throw new Error("Method not implemented.")}getFileActionButtons(e,i,r,s,l,a){throw new Error("Method not implemented.")}}return t.\u0275fac=function(e){return new(e||t)(o.\u0275\u0275inject(C.TranslateService),o.\u0275\u0275inject(F))},t.\u0275prov=o.\u0275\u0275defineInjectable({token:t,factory:t.\u0275fac}),t})();var fe,$=m(951),Y=m(278);const me=new o.InjectionToken("STORAGE_CONFIG_TOKEN");class ot extends Y.Storage{constructor(){super()}create(){return(0,h.__awaiter)(this,void 0,void 0,function*(){return this})}defineDriver(){return(0,h.__awaiter)(this,void 0,void 0,function*(){})}get driver(){return"noop"}get(n){return(0,h.__awaiter)(this,void 0,void 0,function*(){return null})}set(n,e){return(0,h.__awaiter)(this,void 0,void 0,function*(){})}remove(n){return(0,h.__awaiter)(this,void 0,void 0,function*(){})}clear(){return(0,h.__awaiter)(this,void 0,void 0,function*(){})}length(){return(0,h.__awaiter)(this,void 0,void 0,function*(){return 0})}keys(){return(0,h.__awaiter)(this,void 0,void 0,function*(){return[]})}forEach(n){return(0,h.__awaiter)(this,void 0,void 0,function*(){})}setEncryptionKey(n){}}function st(t){return(0,d.isPlatformServer)(this.platformId)?new ot:new Y.Storage(t)}let I=fe=class{static forRoot(n=null){return{ngModule:fe,providers:[{provide:me,useValue:n},{provide:Y.Storage,useFactory:st,deps:[me]}]}}};I.\u0275fac=function(n){return new(n||I)},I.\u0275mod=o.\u0275\u0275defineNgModule({type:I}),I.\u0275inj=o.\u0275\u0275defineInjector({});let ge=(()=>{class t extends c.EpisodeDetailBaseComponent{setShowEpisode(e,i){this.show=e,this.episode=i}}return t.\u0275fac=function(){let n;return function(i){return(n||(n=o.\u0275\u0275getInheritedFactory(t)))(i||t)}}(),t.\u0275cmp=o.\u0275\u0275defineComponent({type:t,selectors:[["ng-component"]],features:[o.\u0275\u0275InheritDefinitionFeature],decls:1,vars:2,consts:[[3,"show","episode"]],template:function(e,i){1&e&&o.\u0275\u0275element(0,"wk-open-button",0),2&e&&o.\u0275\u0275property("show",i.show)("episode",i.episode)},dependencies:[B]}),t})(),he=(()=>{class t extends c.EpisodeDetailBaseComponent{setShowEpisode(e,i){this.show=e,this.episode=i}}return t.\u0275fac=function(){let n;return function(i){return(n||(n=o.\u0275\u0275getInheritedFactory(t)))(i||t)}}(),t.\u0275cmp=o.\u0275\u0275defineComponent({type:t,selectors:[["ng-component"]],features:[o.\u0275\u0275InheritDefinitionFeature],decls:1,vars:2,consts:[["type","item-option",3,"show","episode"]],template:function(e,i){1&e&&o.\u0275\u0275element(0,"wk-open-button",0),2&e&&o.\u0275\u0275property("show",i.show)("episode",i.episode)},dependencies:[B]}),t})();function at(t,n){1&t&&o.\u0275\u0275element(0,"ion-spinner")}function ct(t,n){if(1&t){const e=o.\u0275\u0275getCurrentView();o.\u0275\u0275elementStart(0,"ion-item")(1,"ion-label"),o.\u0275\u0275text(2),o.\u0275\u0275elementEnd(),o.\u0275\u0275elementStart(3,"ion-toggle",4),o.\u0275\u0275listener("ionChange",function(r){const l=o.\u0275\u0275restoreView(e).$implicit,a=o.\u0275\u0275nextContext(2);return o.\u0275\u0275resetView(a.toggleProvider(l.key,r.detail.checked))}),o.\u0275\u0275elementEnd()()}if(2&t){const e=n.$implicit;o.\u0275\u0275advance(2),o.\u0275\u0275textInterpolate1("",e.name," "),o.\u0275\u0275advance(1),o.\u0275\u0275property("ngModel",e.enabled)}}function ut(t,n){if(1&t&&(o.\u0275\u0275elementStart(0,"ion-list",0)(1,"ion-list-header")(2,"ion-label"),o.\u0275\u0275element(3,"ion-icon",6),o.\u0275\u0275text(4),o.\u0275\u0275pipe(5,"translate"),o.\u0275\u0275elementEnd()(),o.\u0275\u0275template(6,ct,4,2,"ion-item",7),o.\u0275\u0275elementEnd()),2&t){const e=o.\u0275\u0275nextContext();o.\u0275\u0275advance(4),o.\u0275\u0275textInterpolate1(" ",o.\u0275\u0275pipeBind1(5,2,"pages.kodi-open-button.pluginList.header")," "),o.\u0275\u0275advance(2),o.\u0275\u0275property("ngForOf",e.pluginArray)}}let _e=(()=>{class t{constructor(e,i,r,s){this.kodiPluginService=e,this.alertController=i,this.translateService=r,this.toastService=s,this.pluginArray=[],this.pluginsUrl=null,this.pluginsList=null,this.isLoading=!1}ngOnInit(){this.refresh(),this.kodiPluginService.getOpenRemoteAfterClickOnPlaySetting().then(e=>{this.openRemoteAfterClickOnPlay=e})}refresh(){this.kodiPluginService.getPluginUrl().then(e=>{this.pluginsUrl=e}),this.kodiPluginService.getPlugins().then(e=>{this.pluginsList=e,this.pluginArray=[],e&&Object.keys(e).forEach(i=>{const r=e[i];this.pluginArray.push({key:i,enabled:r.enabled,name:r.name})})})}setUrl(){this.alertController.create({header:"URL",inputs:[{name:"url",type:"url",placeholder:"URL",value:this.pluginsUrl?this.pluginsUrl:""}],buttons:[{text:this.translateService.instant("alerts.cancelButton"),role:"cancel",cssClass:"secondary"},{text:"Ok",handler:e=>{this.isLoading=!0,this.kodiPluginService.setPluginsFromUrl(e.url).pipe(function lt(t){return E((n,e)=>{try{n.subscribe(e)}finally{e.add(t)}})}(()=>this.isLoading=!1)).subscribe(i=>{i?(this.toastService.simpleMessage("toasts.kodi-open-button.plugingUrlAdded"),this.refresh()):this.toastService.simpleMessage("toasts.kodi-open-button.plugingUrlFailedToAdd")})}}]}).then(e=>{e.present()})}toggleProvider(e,i){this.pluginsList[e].enabled=i,this.kodiPluginService.setPlugins(this.pluginsList)}openRemoteAfterClickOnPlayChange(e){this.openRemoteAfterClickOnPlay=e,this.kodiPluginService.setOpenRemoteAfterClickOnPlaySetting(this.openRemoteAfterClickOnPlay)}}return t.\u0275fac=function(e){return new(e||t)(o.\u0275\u0275directiveInject(F),o.\u0275\u0275directiveInject(p.AlertController),o.\u0275\u0275directiveInject(C.TranslateService),o.\u0275\u0275directiveInject(G))},t.\u0275cmp=o.\u0275\u0275defineComponent({type:t,selectors:[["ng-component"]],decls:22,vars:18,consts:[["lines","full"],["name","cog","slot","start"],[4,"ngIf"],["button","",3,"click"],[3,"ngModel","ionChange"],["lines","full",4,"ngIf"],["name","list","slot","start"],[4,"ngFor","ngForOf"]],template:function(e,i){1&e&&(o.\u0275\u0275elementStart(0,"ion-list",0)(1,"ion-list-header")(2,"ion-label"),o.\u0275\u0275element(3,"ion-icon",1),o.\u0275\u0275text(4),o.\u0275\u0275pipe(5,"translate"),o.\u0275\u0275elementEnd(),o.\u0275\u0275template(6,at,1,0,"ion-spinner",2),o.\u0275\u0275elementEnd(),o.\u0275\u0275elementStart(7,"ion-item",3),o.\u0275\u0275listener("click",function(){return i.setUrl()}),o.\u0275\u0275text(8),o.\u0275\u0275pipe(9,"translate"),o.\u0275\u0275pipe(10,"translate"),o.\u0275\u0275elementEnd()(),o.\u0275\u0275elementStart(11,"ion-list",0)(12,"ion-list-header")(13,"ion-label"),o.\u0275\u0275text(14),o.\u0275\u0275pipe(15,"translate"),o.\u0275\u0275elementEnd()(),o.\u0275\u0275elementStart(16,"ion-item")(17,"ion-label"),o.\u0275\u0275text(18),o.\u0275\u0275pipe(19,"translate"),o.\u0275\u0275elementEnd(),o.\u0275\u0275elementStart(20,"ion-toggle",4),o.\u0275\u0275listener("ionChange",function(s){return i.openRemoteAfterClickOnPlayChange(s.detail.checked)}),o.\u0275\u0275elementEnd()()(),o.\u0275\u0275template(21,ut,7,4,"ion-list",5)),2&e&&(o.\u0275\u0275advance(4),o.\u0275\u0275textInterpolate1(" ",o.\u0275\u0275pipeBind1(5,8,"pages.kodi-open-button.list.header")," "),o.\u0275\u0275advance(2),o.\u0275\u0275property("ngIf",i.isLoading),o.\u0275\u0275advance(2),o.\u0275\u0275textInterpolate2(" ",o.\u0275\u0275pipeBind1(9,10,"pages.kodi-open-button.list.pluginsUrl")," ",i.pluginsUrl?i.pluginsUrl:o.\u0275\u0275pipeBind1(10,12,"pages.kodi-open-button.list.noUrl")," "),o.\u0275\u0275advance(6),o.\u0275\u0275textInterpolate1(" ",o.\u0275\u0275pipeBind1(15,14,"pages.kodi-open-button.openRemoteList.header")," "),o.\u0275\u0275advance(4),o.\u0275\u0275textInterpolate1(" ",o.\u0275\u0275pipeBind1(19,16,"pages.kodi-open-button.openRemoteList.item"),""),o.\u0275\u0275advance(2),o.\u0275\u0275property("ngModel",i.openRemoteAfterClickOnPlay),o.\u0275\u0275advance(1),o.\u0275\u0275property("ngIf",i.pluginsList))},dependencies:[d.NgForOf,d.NgIf,$.NgControlStatus,$.NgModel,p.IonIcon,p.IonItem,p.IonLabel,p.IonList,p.IonListHeader,p.IonSpinner,p.IonToggle,p.BooleanValueAccessor,C.TranslatePipe]}),t})(),ve=(()=>{class t extends c.PluginBaseModule{}return t.pluginService=pe,t.settingsComponent=_e,t.movieComponent=de,t.episodeComponent=ge,t.episodeItemOptionComponent=he,t.\u0275fac=function(){let n;return function(i){return(n||(n=o.\u0275\u0275getInheritedFactory(t)))(i||t)}}(),t.\u0275mod=o.\u0275\u0275defineNgModule({type:t}),t.\u0275inj=o.\u0275\u0275defineInjector({providers:[pe,F,G,...c.WakoProviders],imports:[d.CommonModule,$.FormsModule,p.IonicModule.forRoot(),I.forRoot({}),C.TranslateModule.forRoot()]}),t})();const dt=ve})(),K})()); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const exec = require('child_process').exec; 2 | const { watch, task } = require('gulp'); 3 | 4 | function build(cb) { 5 | exec('npm run build:plugin:dev', (err, stdout, stderr) => { 6 | console.log(stdout); 7 | console.log(stderr); 8 | cb(err); 9 | }); 10 | } 11 | 12 | const watchPlugin = () => { 13 | watch(['./projects/plugin/src/plugin/**/*.*', './projects/plugin/src/i18n/**/*.*'], build); 14 | }; 15 | 16 | task('build', build); 17 | 18 | task('watch', watchPlugin); 19 | -------------------------------------------------------------------------------- /ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wako-addon-starter-kit", 3 | "integrations": {}, 4 | "type": "angular" 5 | } 6 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nomos", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng run app:serve", 7 | "start:wako-like": "npm run clean && npm run build:plugin:dev && concurrently \"ng run wako-like:serve\" \"npm run watch:plugin\" ", 8 | "build": "npm run clean && npm run build:plugin", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e", 12 | "clean": "rm -rf ./dist", 13 | "watch:plugin": "gulp watch", 14 | "build:plugin": "ng run plugin:build:production && npm run copy:resources", 15 | "build:plugin:dev": "ng run plugin:build:production --output-path=src/assets/plugins && npm run copy:resources:dev", 16 | "copy:resources:dev": "cp projects/plugin/src/manifest.json ./src/assets/plugins && cp -r projects/plugin/src/i18n ./src/assets/plugins", 17 | "copy:resources": "cp projects/plugin/src/manifest.json ./dist && cp -r projects/plugin/src/i18n ./dist", 18 | "prepare": "husky install" 19 | }, 20 | "private": true, 21 | "dependencies": { 22 | "@angular/animations": "^14.0.1", 23 | "@angular/cdk": "^14.0.1", 24 | "@angular/common": "^14.0.1", 25 | "@angular/compiler": "^14.0.1", 26 | "@angular/core": "^14.0.1", 27 | "@angular/forms": "^14.0.1", 28 | "@angular/platform-browser": "^14.0.1", 29 | "@angular/platform-browser-dynamic": "^14.0.1", 30 | "@angular/router": "^14.0.1", 31 | "@ionic/angular": "^6.1.11", 32 | "@ionic/storage-angular": "^3.0.6", 33 | "@wako-app/mobile-sdk": "^7.0.2", 34 | "concurrently": "^4.1.1", 35 | "ngx-clipboard": "^15.1.0", 36 | "rxjs": "^7.5.5", 37 | "systemjs": "^6.10.3", 38 | "tslib": "^2.3.1", 39 | "zone.js": "~0.11.4" 40 | }, 41 | "devDependencies": { 42 | "@angular-devkit/build-angular": "^14.0.1", 43 | "@angular-eslint/builder": "14.0.0", 44 | "@angular-eslint/eslint-plugin": "14.0.0", 45 | "@angular-eslint/eslint-plugin-template": "14.0.0", 46 | "@angular-eslint/schematics": "14.0.0", 47 | "@angular-eslint/template-parser": "14.0.0", 48 | "@angular/cli": "^14.0.1", 49 | "@angular/compiler-cli": "^14.0.1", 50 | "@angular/language-service": "^14.0.1", 51 | "@types/jasmine": "~3.3.0", 52 | "@types/jasminewd2": "~2.0.6", 53 | "@types/node": "^12.11.1", 54 | "@typescript-eslint/eslint-plugin": "^5.29.0", 55 | "@typescript-eslint/parser": "^5.29.0", 56 | "concurrently": "^7.1.0", 57 | "eslint": "^8.18.0", 58 | "eslint-plugin-import": "2.26.0", 59 | "eslint-plugin-jsdoc": "^39.3.3", 60 | "eslint-plugin-prefer-arrow": "^1.2.3", 61 | "gulp": "^4.0.2", 62 | "husky": "^8.0.1", 63 | "jasmine-core": "~4.2.0", 64 | "jasmine-spec-reporter": "~5.0.0", 65 | "karma": "~6.3.3", 66 | "karma-chrome-launcher": "~3.1.0", 67 | "karma-coverage-istanbul-reporter": "~3.0.2", 68 | "karma-jasmine": "~3.3.0", 69 | "karma-jasmine-html-reporter": "^1.5.0", 70 | "prettier": "2.7.1", 71 | "pretty-quick": "^3.1.3", 72 | "protractor": "~7.0.0", 73 | "ts-node": "~8.10.2", 74 | "typescript": "~4.6.4" 75 | }, 76 | "description": "An Ionic project" 77 | } 78 | -------------------------------------------------------------------------------- /projects/plugin/src/i18n/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wako-unofficial-addons/nomos/98c2dc864679bec9fca15b22eb8dd966fcfe927e/projects/plugin/src/i18n/.gitkeep -------------------------------------------------------------------------------- /projects/plugin/src/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": { 3 | "kodi-open-button": { 4 | "title": "Kodi Open Button", 5 | "list": { 6 | "header": "Setup", 7 | "pluginsUrl": "Plugins URL:", 8 | "noUrl": "Click here to add one" 9 | }, 10 | "openRemoteList": { 11 | "header": "Remote", 12 | "item": "Open remote after clicking" 13 | }, 14 | "pluginList": { 15 | "header": "Plugins" 16 | } 17 | } 18 | }, 19 | "toasts": { 20 | "kodi": { 21 | "open": "Open on {{hostName}} using {{pluginName}}", 22 | "hostUnreachable": "Ouch, your kodi host {{hostName}} is unreachable", 23 | "noHost": "No host set for kodi", 24 | "noSupportedPluginsInstalled": "Ouch, None of the supported plugins are installed, please install at least one of them", 25 | "failedToOpen": "Failed to open" 26 | }, 27 | "kodi-open-button": { 28 | "plugingUrlAdded": "Plugins added", 29 | "plugingUrlFailedToAdd": "Failed to add plugins, please check the given URL" 30 | }, 31 | "pluginsUpdated": "Plugins have been updated" 32 | }, 33 | "shared": { 34 | "buttonOpenKodi": "Open on Kodi" 35 | }, 36 | "actionSheets": { 37 | "kodi": { 38 | "openTitle": "Open with Kodi using..." 39 | } 40 | }, 41 | "alerts": { 42 | "okButton": "Ok", 43 | "cancelButton": "Cancel" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /projects/plugin/src/i18n/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": { 3 | "kodi-open-button": { 4 | "title": "Bouton Ouvrir", 5 | "list": { 6 | "header": "Configuration", 7 | "pluginsUrl": "Plugins URL:", 8 | "noUrl": "aucune" 9 | }, 10 | "openRemoteList": { 11 | "header": "Télécommande", 12 | "item": "Ouvrir la télécommande après avoir cliqué" 13 | }, 14 | "pluginList": { 15 | "header": "Plugins" 16 | } 17 | } 18 | }, 19 | "toasts": { 20 | "kodi": { 21 | "noSupportedPluginsInstalled": "Aïe, aucun des plugins supportés n'est installé, veuillez en installer au moins un.", 22 | "failedToOpen": "N'a pas réussi à ouvrir" 23 | }, 24 | "pluginsUpdated": "Les plugins ont été mis à jour" 25 | }, 26 | "shared": { 27 | "buttonOpenKodi": "Ouvrir avec Kodi" 28 | }, 29 | "actionSheets": { 30 | "kodi": { 31 | "openTitle": "Ouvrir avec Kodi via..." 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /projects/plugin/src/i18n/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": { 3 | "kodi-open-button": { 4 | "title": "Przycisk odtwórz w Kodi", 5 | "list": { 6 | "header": "Konfiguracja", 7 | "pluginsUrl": "Adres URL wtyczek:", 8 | "noUrl": "nic nie ustawiono" 9 | }, 10 | "openRemoteList": { 11 | "header": "Pilot", 12 | "item": "Otwórz pilota po kliknięciu" 13 | }, 14 | "pluginList": { 15 | "header": "Wtyczki" 16 | } 17 | } 18 | }, 19 | "toasts": { 20 | "kodi": { 21 | "noSupportedPluginsInstalled": "Ałć... Żadna ze wspieranych wtyczek nie jest zainstalowana, zainstaluj przynajmniej jedną ze wspieranych wtyczek.", 22 | "failedToOpen": "Błąd podczas otwierania" 23 | }, 24 | "pluginsUpdated": "Wtyczki zostały zaktualizowane" 25 | }, 26 | "shared": { 27 | "buttonOpenKodi": "Odtwórz na Kodi" 28 | }, 29 | "actionSheets": { 30 | "kodi": { 31 | "openTitle": "Odtwórz na Kodi używając..." 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /projects/plugin/src/main.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wako-unofficial-addons/nomos/98c2dc864679bec9fca15b22eb8dd966fcfe927e/projects/plugin/src/main.ts -------------------------------------------------------------------------------- /projects/plugin/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/@wako-app/mobile-sdk/dist/manifest-schema.json", 3 | "version": "1.1.4", 4 | "id": "plugin.nomos", 5 | "name": "Nomos", 6 | "author": "Davy", 7 | "description": "Allow to open Movie/TV Show on your favorite Kodi add-on", 8 | "actions": ["movies", "episodes", "episodes-item-option"], 9 | "entryPointV3": "https://raw.githubusercontent.com/wako-unofficial-addons/nomos/master/dist/plugin.js", 10 | "languages": { 11 | "en": "https://raw.githubusercontent.com/wako-unofficial-addons/nomos/master/dist/i18n/en.json", 12 | "fr": "https://raw.githubusercontent.com/wako-unofficial-addons/nomos/master/dist/i18n/fr.json", 13 | "pl": "https://raw.githubusercontent.com/wako-unofficial-addons/nomos/master/dist/i18n/pl.json" 14 | }, 15 | "changeLogs": { 16 | "1.1.4": "Use latest SDK", 17 | "1.1.2": "Upgrade deps", 18 | "1.1.1": "Initial version" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /projects/plugin/src/plugin/entities/kodi-open-media.ts: -------------------------------------------------------------------------------- 1 | import { Episode, Movie, Show } from '@wako-app/mobile-sdk'; 2 | 3 | export interface KodiOpenMedia { 4 | movie?: Movie; 5 | show?: Show; 6 | episode?: Episode; 7 | category?: 'movie' | 'episode'; 8 | } 9 | -------------------------------------------------------------------------------- /projects/plugin/src/plugin/entities/kodi-plugin.ts: -------------------------------------------------------------------------------- 1 | export interface KodiPlugin { 2 | name: string; 3 | handleAddonExecute: boolean; 4 | plugin: string; 5 | enabled?: boolean; 6 | tvUrl?: string; 7 | movieUrl?: string; 8 | } 9 | 10 | export interface KodiPluginList { 11 | [key: string]: KodiPlugin; 12 | } 13 | -------------------------------------------------------------------------------- /projects/plugin/src/plugin/episode-button/episode-button.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /projects/plugin/src/plugin/episode-button/episode-button.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wako-unofficial-addons/nomos/98c2dc864679bec9fca15b22eb8dd966fcfe927e/projects/plugin/src/plugin/episode-button/episode-button.component.scss -------------------------------------------------------------------------------- /projects/plugin/src/plugin/episode-button/episode-button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Episode, EpisodeDetailBaseComponent, Show } from '@wako-app/mobile-sdk'; 3 | 4 | @Component({ 5 | templateUrl: './episode-button.component.html', 6 | styleUrls: ['./episode-button.component.scss'] 7 | }) 8 | export class EpisodeButtonComponent extends EpisodeDetailBaseComponent { 9 | show: Show; 10 | episode: Episode; 11 | 12 | setShowEpisode(show: Show, episode: Episode): any { 13 | this.show = show; 14 | this.episode = episode; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /projects/plugin/src/plugin/episode-item-option/episode-item-option.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /projects/plugin/src/plugin/episode-item-option/episode-item-option.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wako-unofficial-addons/nomos/98c2dc864679bec9fca15b22eb8dd966fcfe927e/projects/plugin/src/plugin/episode-item-option/episode-item-option.component.scss -------------------------------------------------------------------------------- /projects/plugin/src/plugin/episode-item-option/episode-item-option.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Episode, EpisodeDetailBaseComponent, Show } from '@wako-app/mobile-sdk'; 3 | 4 | @Component({ 5 | templateUrl: './episode-item-option.component.html', 6 | styleUrls: ['./episode-item-option.component.scss'] 7 | }) 8 | export class EpisodeItemOptionComponent extends EpisodeDetailBaseComponent { 9 | show: Show; 10 | episode: Episode; 11 | 12 | setShowEpisode(show: Show, episode: Episode): any { 13 | this.show = show; 14 | this.episode = episode; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /projects/plugin/src/plugin/movie-button/movie-button.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /projects/plugin/src/plugin/movie-button/movie-button.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wako-unofficial-addons/nomos/98c2dc864679bec9fca15b22eb8dd966fcfe927e/projects/plugin/src/plugin/movie-button/movie-button.component.scss -------------------------------------------------------------------------------- /projects/plugin/src/plugin/movie-button/movie-button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Movie, MovieDetailBaseComponent } from '@wako-app/mobile-sdk'; 3 | 4 | @Component({ 5 | selector: 'app-movie-button', 6 | templateUrl: './movie-button.component.html', 7 | styleUrls: ['./movie-button.component.scss'] 8 | }) 9 | export class MovieButtonComponent extends MovieDetailBaseComponent { 10 | movie: Movie; 11 | 12 | setMovie(movie: Movie): any { 13 | this.movie = movie; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /projects/plugin/src/plugin/open-button/open-button.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ 'shared.buttonOpenKodi' | translate }} 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 | 15 | 16 | Nomos 17 | 18 |
19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /projects/plugin/src/plugin/open-button/open-button.component.scss: -------------------------------------------------------------------------------- 1 | .ion-color-nomos { 2 | --ion-color-base: #4b1d52; 3 | --ion-color-base-rgb: 53, 66, 82; 4 | --ion-color-contrast: #ffffff; 5 | --ion-color-contrast-rgb: 255, 255, 255; 6 | --ion-color-shade: #483247; 7 | --ion-color-tint: #635260; 8 | } 9 | 10 | 11 | ion-button { 12 | --ripple-color: var(--ion-color-nomos); 13 | --background-activated: var(--ion-color-nomos); 14 | --background-focused: var(--ion-color-nomos); 15 | } 16 | 17 | .ion-item-options { 18 | display: flex; 19 | height: 100%; 20 | 21 | .ion-item-detail { 22 | width: 60px; 23 | display: grid; 24 | text-align: center; 25 | 26 | ion-icon { 27 | font-size: 2em; 28 | width: 100%; 29 | } 30 | 31 | ion-text { 32 | margin-top: 3px; 33 | font-size: 8px; 34 | text-transform: none; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /projects/plugin/src/plugin/open-button/open-button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { KodiPluginService } from '../services/kodi-plugin.service'; 3 | import { Episode, Movie, Show } from '@wako-app/mobile-sdk'; 4 | 5 | @Component({ 6 | selector: 'wk-open-button', 7 | templateUrl: './open-button.component.html', 8 | styleUrls: ['./open-button.component.scss'] 9 | }) 10 | export class OpenButtonComponent { 11 | @Input() movie: Movie; 12 | @Input() show: Show; 13 | @Input() episode: Episode; 14 | @Input() type: 'button' | 'item-option' = 'button'; 15 | 16 | constructor(private kodiPluginService: KodiPluginService) { 17 | } 18 | 19 | async open() { 20 | this.kodiPluginService.open({ 21 | category: this.movie ? 'movie' : 'episode', 22 | movie: this.movie, 23 | show: this.show, 24 | episode: this.episode 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /projects/plugin/src/plugin/plugin.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { IonicModule } from '@ionic/angular'; 5 | import { MovieButtonComponent } from './movie-button/movie-button.component'; 6 | import { PluginService } from './services/plugin.service'; 7 | 8 | import { FormsModule } from '@angular/forms'; 9 | import { IonicStorageModule } from '@ionic/storage-angular'; 10 | import { TranslateModule } from '@ngx-translate/core'; 11 | import { PluginBaseModule, WakoProviders } from '@wako-app/mobile-sdk'; 12 | import { EpisodeButtonComponent } from './episode-button/episode-button.component'; 13 | import { EpisodeItemOptionComponent } from './episode-item-option/episode-item-option.component'; 14 | import { OpenButtonComponent } from './open-button/open-button.component'; 15 | import { KodiPluginService } from './services/kodi-plugin.service'; 16 | import { ToastService } from './services/toast.service'; 17 | import { SettingsComponent } from './settings/settings.component'; 18 | 19 | const components = [MovieButtonComponent, EpisodeButtonComponent, EpisodeItemOptionComponent, SettingsComponent, OpenButtonComponent]; 20 | 21 | @NgModule({ 22 | imports: [CommonModule, FormsModule, IonicModule.forRoot(), IonicStorageModule.forRoot({}), TranslateModule.forRoot()], 23 | declarations: [...components], 24 | entryComponents: [...components], 25 | providers: [PluginService, KodiPluginService, ToastService, 26 | ...WakoProviders, 27 | ], // Add your services here. Do not use provideIn: 'root' in your services 28 | }) 29 | export class PluginModule extends PluginBaseModule { 30 | static pluginService = PluginService; 31 | static settingsComponent = SettingsComponent; 32 | static movieComponent = MovieButtonComponent; 33 | static episodeComponent = EpisodeButtonComponent; 34 | static episodeItemOptionComponent = EpisodeItemOptionComponent; 35 | } 36 | -------------------------------------------------------------------------------- /projects/plugin/src/plugin/services/kodi-plugin.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActionSheetController } from '@ionic/angular'; 3 | import { TranslateService } from '@ngx-translate/core'; 4 | import { 5 | escapeText, 6 | EventCategory, 7 | EventName, 8 | EventService, 9 | KodiApiService, 10 | KodiAppService, 11 | KodiGetAddonsForm, 12 | OpenMedia, 13 | replacer, 14 | WakoCacheService, 15 | WakoHttpRequestService, 16 | WakoStorage 17 | } from '@wako-app/mobile-sdk'; 18 | import { from, NEVER, of } from 'rxjs'; 19 | import { catchError, map, switchMap, tap } from 'rxjs/operators'; 20 | import { KodiOpenMedia } from '../entities/kodi-open-media'; 21 | import { KodiPlugin, KodiPluginList } from '../entities/kodi-plugin'; 22 | import { ToastService } from './toast.service'; 23 | import { logData } from './tools'; 24 | 25 | const CACHE_KEY_PLUGINS = 'CACHE_KEY_PLUGINS'; 26 | const CACHE_TIMEOUT_PLUGINS = '1d'; 27 | 28 | @Injectable() 29 | export class KodiPluginService { 30 | constructor( 31 | private toastService: ToastService, 32 | private storage: WakoStorage, 33 | private actionSheetController: ActionSheetController, 34 | private translateService: TranslateService 35 | ) {} 36 | 37 | async patchStorageKey() { 38 | const url = await this.storage.get('kodi-plugins-url'); 39 | if (url) { 40 | await this.storage.set(this.getStorageKeyPrefixed('kodi-plugins-url'), url); 41 | await this.storage.remove('kodi-plugins-url'); 42 | } 43 | 44 | const list = await this.storage.get('kodi-plugins-list'); 45 | if (list) { 46 | await this.storage.set(this.getStorageKeyPrefixed('kodi-plugins-list'), list); 47 | await this.storage.remove('kodi-plugins-list'); 48 | } 49 | 50 | const open = await this.storage.get('openRemoteAfterClickOnPlay'); 51 | if (open) { 52 | await this.storage.set(this.getStorageKeyPrefixed('openRemoteAfterClickOnPlay'), open); 53 | await this.storage.remove('openRemoteAfterClickOnPlay'); 54 | } 55 | } 56 | 57 | private getStorageKeyPrefixed(key: string) { 58 | return `nomos_${key}`; 59 | } 60 | 61 | getPluginUrl(): Promise { 62 | return this.storage.get(this.getStorageKeyPrefixed('kodi-plugins-url')); 63 | } 64 | 65 | private setPluginUrl(url: string): Promise { 66 | return this.storage.set(this.getStorageKeyPrefixed('kodi-plugins-url'), url); 67 | } 68 | 69 | getPlugins(): Promise { 70 | return this.storage.get(this.getStorageKeyPrefixed('kodi-plugins-list')); 71 | } 72 | 73 | setPlugins(plugins: KodiPluginList, isAutomatic = false) { 74 | return this.getPlugins().then((oldPlugins) => { 75 | if (oldPlugins && isAutomatic) { 76 | let areEquals = Object.keys(oldPlugins).length === Object.keys(plugins).length; 77 | 78 | if (areEquals) { 79 | Object.keys(oldPlugins).forEach((key) => { 80 | const _old = Object.assign({}, oldPlugins[key]); 81 | const _new = Object.assign({}, plugins[key]); 82 | _old.enabled = true; 83 | _new.enabled = true; 84 | if (JSON.stringify(_old) !== JSON.stringify(_new)) { 85 | areEquals = false; 86 | } 87 | }); 88 | } 89 | 90 | if (!areEquals) { 91 | this.toastService.simpleMessage('toasts.pluginsUpdated'); 92 | } else { 93 | logData('no changes in plugins'); 94 | } 95 | 96 | Object.keys(oldPlugins).forEach((key) => { 97 | if (plugins.hasOwnProperty(key)) { 98 | plugins[key].enabled = oldPlugins[key].enabled; 99 | } 100 | }); 101 | } 102 | return this.storage.set(this.getStorageKeyPrefixed('kodi-plugins-list'), plugins); 103 | }); 104 | } 105 | 106 | setPluginsFromUrl(url: string, isAutomatic = false) { 107 | url = url.trim(); 108 | 109 | return WakoHttpRequestService.request( 110 | { 111 | url, 112 | method: 'GET' 113 | }, 114 | null, 115 | 10000, 116 | true 117 | ).pipe( 118 | switchMap((pluginList) => { 119 | if (typeof pluginList === 'string' || Object.keys(pluginList).length === 0) { 120 | return of(false); 121 | } 122 | 123 | if (!isAutomatic) { 124 | Object.keys(pluginList).forEach((key) => { 125 | pluginList[key].enabled = true; 126 | }); 127 | } 128 | 129 | return from(this.setPlugins(pluginList, isAutomatic)).pipe( 130 | switchMap(() => { 131 | return from(this.setPluginUrl(url)); 132 | }), 133 | map(() => { 134 | return true; 135 | }) 136 | ); 137 | }), 138 | catchError(() => { 139 | return of(false); 140 | }) 141 | ); 142 | } 143 | 144 | refreshPlugins() { 145 | WakoCacheService.get(CACHE_KEY_PLUGINS).subscribe((cache) => { 146 | if (cache) { 147 | logData('check plugins later'); 148 | return; 149 | } 150 | this.getPluginUrl().then((url) => { 151 | if (url) { 152 | this.setPluginsFromUrl(url, true).subscribe(); 153 | } 154 | }); 155 | 156 | WakoCacheService.set(CACHE_KEY_PLUGINS, true, CACHE_TIMEOUT_PLUGINS); 157 | }); 158 | } 159 | 160 | getOpenRemoteAfterClickOnPlaySetting() { 161 | return this.storage.get(this.getStorageKeyPrefixed('openRemoteAfterClickOnPlay')).then((openRemoteAfterClickOnPlay) => { 162 | return !!openRemoteAfterClickOnPlay; 163 | }); 164 | } 165 | 166 | setOpenRemoteAfterClickOnPlaySetting(openRemoteAfterClickOnPlay) { 167 | return this.storage.set(this.getStorageKeyPrefixed('openRemoteAfterClickOnPlay'), openRemoteAfterClickOnPlay); 168 | } 169 | 170 | private getPluginsByCategory(category?: 'movie' | 'episode') { 171 | return from(this.getPlugins()).pipe( 172 | map((list) => { 173 | if (!list) { 174 | return null; 175 | } 176 | const newList: KodiPluginList = {}; 177 | 178 | Object.keys(list).forEach((key) => { 179 | const value = list[key]; 180 | if (!value.enabled) { 181 | return; 182 | } 183 | if ((category === 'movie' && value.movieUrl) || (category === 'episode' && value.tvUrl)) { 184 | newList[key] = value; 185 | } 186 | }); 187 | 188 | return newList; 189 | }) 190 | ); 191 | } 192 | 193 | getInstalledPlugins() { 194 | return KodiGetAddonsForm.submit().pipe( 195 | catchError(() => { 196 | return of({ addons: [] }); 197 | }), 198 | map((plugins) => { 199 | const installedPlugins: string[] = []; 200 | 201 | plugins.addons.forEach((addon) => { 202 | installedPlugins.push(addon.addonid); 203 | }); 204 | return installedPlugins; 205 | }) 206 | ); 207 | } 208 | 209 | open(kodiOpenMedia: KodiOpenMedia) { 210 | let pluginList: KodiPluginList = {}; 211 | 212 | KodiAppService.checkAndConnectToCurrentHost() 213 | .pipe( 214 | catchError((err) => { 215 | if (err === 'hostUnreachable') { 216 | this.toastService.simpleMessage('toasts.kodi.hostUnreachable', { hostName: KodiAppService.currentHost.name }, 2000); 217 | } else { 218 | this.toastService.simpleMessage('toasts.kodi.noHost'); 219 | } 220 | return NEVER; 221 | }), 222 | switchMap(() => { 223 | logData('CONNECTED'); 224 | 225 | return this.getPluginsByCategory(kodiOpenMedia.category).pipe( 226 | tap((list) => { 227 | pluginList = list || {}; 228 | }) 229 | ); 230 | }), 231 | switchMap(() => this.getInstalledPlugins()), 232 | map((_installedPlugins) => { 233 | logData('_installedPlugins', _installedPlugins); 234 | const supportedAndInstalledPlugins = []; 235 | 236 | Object.keys(pluginList).forEach((pluginId) => { 237 | const pluginDetail = pluginList[pluginId]; 238 | if (_installedPlugins.includes(pluginDetail.plugin)) { 239 | supportedAndInstalledPlugins.push(pluginId); 240 | } 241 | }); 242 | return supportedAndInstalledPlugins; 243 | }) 244 | ) 245 | .subscribe((supportedAndInstalledPlugins) => { 246 | logData('supportedAndInstalledPlugins', supportedAndInstalledPlugins); 247 | if (supportedAndInstalledPlugins.length === 0) { 248 | this.toastService.simpleMessage('toasts.kodi.noSupportedPluginsInstalled'); 249 | 250 | return; 251 | } 252 | 253 | if (supportedAndInstalledPlugins.length === 1) { 254 | this.openOnPlugin(supportedAndInstalledPlugins[0], kodiOpenMedia); 255 | return; 256 | } 257 | 258 | const buttons = []; 259 | Object.keys(pluginList).forEach((pluginId) => { 260 | const pluginDetail = pluginList[pluginId]; 261 | 262 | if (supportedAndInstalledPlugins.includes(pluginId)) { 263 | buttons.push({ 264 | text: pluginDetail.name, 265 | handler: () => { 266 | this.openOnPlugin(pluginId, kodiOpenMedia); 267 | } 268 | }); 269 | } 270 | }); 271 | 272 | logData('buttons', buttons); 273 | if (buttons.length === 1) { 274 | buttons[0].handler(); 275 | return; 276 | } 277 | 278 | this.actionSheetController 279 | .create({ 280 | header: this.translateService.instant('actionSheets.kodi.openTitle'), 281 | buttons 282 | }) 283 | .then((action) => { 284 | action.present(); 285 | }); 286 | }); 287 | } 288 | 289 | private openViaPlaylist(url: string, openMedia: OpenMedia, openRemoteAfterClickOnPlay: boolean) { 290 | return of(true).pipe( 291 | switchMap(() => 292 | KodiApiService.doHttpAction('Playlist.Clear', { 293 | playlistid: 0 294 | }) 295 | ), 296 | switchMap(() => 297 | KodiApiService.doHttpAction('Playlist.Insert', { 298 | playlistid: 0, 299 | item: { 300 | file: url 301 | }, 302 | position: 0 303 | }) 304 | ), 305 | switchMap(() => KodiAppService.stopPlayingIfAny()), 306 | switchMap(() => 307 | KodiApiService.doHttpAction('Player.open', { 308 | item: { 309 | playlistid: 0 310 | } 311 | }) 312 | ), 313 | map(() => { 314 | if (openRemoteAfterClickOnPlay) { 315 | EventService.emit(EventCategory.kodiRemote, EventName.open); 316 | } 317 | if (openMedia) { 318 | KodiAppService.openMedia$.next(openMedia); 319 | } 320 | 321 | EventService.emit(EventCategory.kodi, EventName.open); 322 | 323 | return true; 324 | }) 325 | ); 326 | } 327 | 328 | private openOnPlugin(pluginKey: string, kodiOpenMedia: KodiOpenMedia) { 329 | this.getPlugins().then((pluginList) => { 330 | const plugin = pluginList[pluginKey]; 331 | 332 | const toastMessage = 'toasts.kodi.open'; 333 | const toastParams = { 334 | hostName: KodiApiService.host.name, 335 | pluginName: plugin.name 336 | }; 337 | const params = this.getKodiPluginParams(plugin, kodiOpenMedia); 338 | 339 | return from(this.getOpenRemoteAfterClickOnPlaySetting()) 340 | .pipe( 341 | switchMap((openRemoteAfterClickOnPlay) => { 342 | const openMedia: OpenMedia = { 343 | movieIds: kodiOpenMedia.movie ? kodiOpenMedia.movie.ids : null, 344 | showIds: kodiOpenMedia.show ? kodiOpenMedia.show.ids : null, 345 | seasonNumber: kodiOpenMedia.episode ? kodiOpenMedia.episode.seasonNumber : null, 346 | episodeNumber: kodiOpenMedia.episode ? kodiOpenMedia.episode.number : null 347 | }; 348 | const url = `plugin://${plugin.plugin}${params}`; 349 | 350 | logData('Opening: ' + url); 351 | 352 | return this.openViaPlaylist(url, openMedia, openRemoteAfterClickOnPlay); 353 | }), 354 | tap((done) => { 355 | if (!done) { 356 | this.toastService.simpleMessage('toasts.kodi.failedToOpen'); 357 | return; 358 | } 359 | 360 | this.toastService.simpleMessage(toastMessage, toastParams); 361 | }) 362 | ) 363 | .subscribe(); 364 | }); 365 | } 366 | 367 | private replacePlus(str: string) { 368 | return str ? str.replace(/\s/g, '+') : ''; 369 | } 370 | 371 | private getKodiPluginParams(plugin: KodiPlugin, kodiOpenMedia: KodiOpenMedia) { 372 | if (kodiOpenMedia.movie) { 373 | const movie = kodiOpenMedia.movie; 374 | 375 | const rpl: MediaReplacement = { 376 | tmdbId: movie.ids.tmdb, 377 | rating: movie.rating, 378 | date: movie.released, 379 | title: movie.title, 380 | tagline: movie.tagline, 381 | plot: movie.overview, 382 | original_title: movie.originalTitle, 383 | imdb: movie.ids.imdb, 384 | year: movie.year, 385 | duration: movie.runtime * 60, 386 | mpaa: movie.certification, 387 | genres: movie.genres.join(' / '), 388 | fanart: movie.imagesUrl.backdropOriginal, 389 | poster: movie.imagesUrl.posterOriginal, 390 | id: movie.ids.tmdb, 391 | slug: '', 392 | trakt: movie.ids.trakt, 393 | showTmdbId: null, 394 | seasonNumber: null, 395 | episodeNumber: null, 396 | 'title_+': this.replacePlus(movie.title), 397 | tvdb: null, 398 | season: null, 399 | episode: null, 400 | 'clearname_+': this.replacePlus(movie.title), 401 | firstaired: movie.released, 402 | 'plot_+': this.replacePlus(movie.overview), 403 | thumbnail: movie.imagesUrl.backdropOriginal, 404 | now: Date.now().toString(), 405 | banner: movie.imagesUrl.backdropOriginal, 406 | epimdb: null, 407 | eptmdb: null, 408 | eptrakt: null, 409 | epid: null, 410 | network_clean: '', 411 | clearname: movie.title, 412 | epyear: null, 413 | tmdb: movie.ids.tmdb, 414 | episodes: null, 415 | seasons: null, 416 | series_firstaired: null, 417 | seasons_no_specials: null, 418 | status: null, 419 | premiered: movie.released 420 | }; 421 | 422 | return this.getPluginParams(plugin.movieUrl, rpl); 423 | } else { 424 | const show = kodiOpenMedia.show; 425 | const episode = kodiOpenMedia.episode; 426 | 427 | let firstAired = '2019-01-01'; 428 | if (show.firstAired) { 429 | firstAired = show.firstAired; 430 | } 431 | 432 | let episodeFirstAired = new Date(); 433 | if (typeof episode.firstAired === 'string') { 434 | episodeFirstAired = new Date(episode.firstAired); 435 | } 436 | 437 | const rpl: MediaReplacement = { 438 | showTmdbId: show.ids.tmdb, 439 | seasonNumber: episode.seasonNumber, 440 | episodeNumber: episode.number, 441 | 'title_+': this.replacePlus(episode.title), 442 | year: show.year, 443 | imdb: show.ids.imdb, 444 | tvdb: +show.ids.tvdb, 445 | season: episode.seasonNumber, 446 | episode: episode.number, 447 | 'clearname_+': this.replacePlus(show.title), 448 | firstaired: firstAired, 449 | duration: episode.runtime * 60, 450 | 'plot_+': this.replacePlus(episode.overview), 451 | thumbnail: show.imagesUrl.backdropOriginal, 452 | id: +show.ids.tvdb, 453 | poster: show.imagesUrl.posterOriginal, 454 | fanart: show.imagesUrl.backdropOriginal, 455 | now: Date.now().toString(), 456 | banner: show.imagesUrl.backdropOriginal, 457 | epimdb: episode.ids.imdb, 458 | eptmdb: 0, 459 | eptrakt: 0, 460 | epid: +episode.ids.tvdb, 461 | genres: '', 462 | mpaa: show.certification, 463 | title: episode.title, 464 | plot: episode.overview, 465 | rating: episode.rating, 466 | network_clean: '', 467 | clearname: show.title, 468 | epyear: episodeFirstAired ? episodeFirstAired.getFullYear() : null, 469 | slug: '', 470 | tmdb: show.ids.tmdb, 471 | trakt: show.ids.trakt, 472 | episodes: 1, 473 | seasons: 1, 474 | series_firstaired: firstAired, 475 | seasons_no_specials: 1, 476 | status: 'Continuing', 477 | tmdbId: show.ids.tmdb, 478 | date: firstAired, 479 | tagline: '', 480 | original_title: episode.title, 481 | premiered: firstAired 482 | }; 483 | 484 | return this.getPluginParams(plugin.tvUrl, rpl); 485 | } 486 | } 487 | 488 | private getPluginParams(url: string, rpl: MediaReplacement) { 489 | Object.keys(rpl).forEach((key) => { 490 | const value = escapeText(rpl[key]); 491 | 492 | if (!key.match(/\+/g)) { 493 | rpl[key] = encodeURIComponent(value); 494 | } 495 | }); 496 | 497 | return replacer(url, rpl); 498 | } 499 | } 500 | 501 | interface MediaReplacement { 502 | tmdbId: number; 503 | rating: number; 504 | date: string; 505 | title: string; 506 | tagline: string; 507 | plot: string; 508 | original_title: string; 509 | imdb: string; 510 | year: number; 511 | duration: number; 512 | mpaa: string; 513 | genres: string; 514 | fanart: string; 515 | poster: string; 516 | id: number; 517 | slug: string; 518 | trakt: number; 519 | showTmdbId: number; 520 | seasonNumber: number; 521 | episodeNumber: number; 522 | 'title_+': string; 523 | tvdb: number; 524 | season: number; 525 | episode: number; 526 | 'clearname_+': string; 527 | firstaired: string; 528 | 'plot_+': string; 529 | thumbnail: string; 530 | now: string; 531 | banner: string; 532 | epimdb: string; 533 | eptmdb: number; 534 | eptrakt: number; 535 | epid: number; 536 | network_clean: string; 537 | clearname: string; 538 | epyear: number; 539 | tmdb: number; 540 | episodes: number; 541 | seasons: number; 542 | series_firstaired: string; 543 | seasons_no_specials: number; 544 | status: string; 545 | premiered: string; 546 | } 547 | -------------------------------------------------------------------------------- /projects/plugin/src/plugin/services/plugin.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | Episode, 4 | ExplorerFile, 5 | ExplorerFolderItem, 6 | KodiOpenParams, 7 | Movie, 8 | OpenMedia, 9 | PluginBaseService, 10 | Show, 11 | WakoFileActionButton 12 | } from '@wako-app/mobile-sdk'; 13 | import { TranslateService } from '@ngx-translate/core'; 14 | import { KodiPluginService } from './kodi-plugin.service'; 15 | import { logData } from './tools'; 16 | 17 | @Injectable() 18 | export class PluginService extends PluginBaseService { 19 | constructor(protected translate: TranslateService, private kodiPluginService: KodiPluginService) { 20 | super(); 21 | } 22 | 23 | initialize() { 24 | logData('plugin initialized'); 25 | 26 | setTimeout(() => { 27 | this.kodiPluginService.refreshPlugins(); 28 | }, 10000); 29 | 30 | this.kodiPluginService.patchStorageKey(); 31 | } 32 | 33 | afterInstall() { 34 | logData('plugin installed'); 35 | } 36 | 37 | setTranslation(lang: string, translations: any): any { 38 | this.translate.setDefaultLang(lang); 39 | this.translate.use(lang); 40 | this.translate.setTranslation(lang, translations); 41 | } 42 | 43 | customAction(action: string, data: any): any {} 44 | 45 | afterEpisodeMiddleware(show: Show, episode: Episode): Promise { 46 | return Promise.resolve(undefined); 47 | } 48 | 49 | afterMovieMiddleware(movie: Movie): Promise { 50 | return Promise.resolve(undefined); 51 | } 52 | 53 | afterShowMiddleware(show: Show): Promise { 54 | return Promise.resolve(undefined); 55 | } 56 | 57 | beforeEpisodeMiddleware(show: Show, episode: Episode): Promise { 58 | return Promise.resolve(undefined); 59 | } 60 | 61 | beforeMovieMiddleware(movie: Movie): Promise { 62 | return Promise.resolve(undefined); 63 | } 64 | 65 | beforeShowMiddleware(show: Show): Promise { 66 | return Promise.resolve(undefined); 67 | } 68 | 69 | fetchExplorerFolderItem(): Promise { 70 | throw new Error('Method not implemented.'); 71 | } 72 | getFileActionButtons( 73 | file: ExplorerFile, 74 | title?: string, 75 | posterUrl?: string, 76 | seekTo?: number, 77 | openMedia?: OpenMedia, 78 | kodiOpenParams?: KodiOpenParams 79 | ): Promise { 80 | throw new Error('Method not implemented.'); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /projects/plugin/src/plugin/services/toast.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ToastController } from '@ionic/angular'; 3 | import { TranslateService } from '@ngx-translate/core'; 4 | 5 | @Injectable() 6 | export class ToastService { 7 | constructor(private toastCtrl: ToastController, private translateService: TranslateService) {} 8 | 9 | simpleMessage(translateKey: string, interpolateParams?: any, duration = 2000, position = 'top') { 10 | this.translateService.get(translateKey, interpolateParams).subscribe((message) => { 11 | this.toastCtrl 12 | .create({ 13 | message: message, 14 | mode: 'ios', 15 | position: position as any, 16 | duration: duration 17 | }) 18 | .then((alert) => { 19 | alert.present(); 20 | }); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /projects/plugin/src/plugin/services/tools.ts: -------------------------------------------------------------------------------- 1 | import { wakoLog } from '@wako-app/mobile-sdk'; 2 | 3 | export function logData(...data: any) { 4 | wakoLog('plugin.nomos', data); 5 | } 6 | -------------------------------------------------------------------------------- /projects/plugin/src/plugin/settings/settings.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ 'pages.kodi-open-button.list.header' | translate }} 6 | 7 | 8 | 9 | 10 | {{ 'pages.kodi-open-button.list.pluginsUrl' | translate }} 11 | {{ pluginsUrl ? pluginsUrl : ('pages.kodi-open-button.list.noUrl' | translate) }} 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{ 'pages.kodi-open-button.openRemoteList.header' | translate }} 19 | 20 | 21 | 22 | {{ 'pages.kodi-open-button.openRemoteList.item' | translate }} 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {{ 'pages.kodi-open-button.pluginList.header' | translate }} 35 | 36 | 37 | 38 | {{ plugin.name }} 39 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /projects/plugin/src/plugin/settings/settings.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wako-unofficial-addons/nomos/98c2dc864679bec9fca15b22eb8dd966fcfe927e/projects/plugin/src/plugin/settings/settings.component.scss -------------------------------------------------------------------------------- /projects/plugin/src/plugin/settings/settings.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { AlertController } from '@ionic/angular'; 3 | import { TranslateService } from '@ngx-translate/core'; 4 | import { finalize } from 'rxjs/operators'; 5 | import { KodiPluginList } from '../entities/kodi-plugin'; 6 | import { KodiPluginService } from '../services/kodi-plugin.service'; 7 | import { ToastService } from '../services/toast.service'; 8 | 9 | interface PluginArray { 10 | key: string; 11 | name: string; 12 | enabled: boolean; 13 | } 14 | 15 | @Component({ 16 | templateUrl: './settings.component.html', 17 | styleUrls: ['./settings.component.scss'] 18 | }) 19 | export class SettingsComponent implements OnInit { 20 | pluginArray: PluginArray[] = []; 21 | 22 | pluginsUrl = null; 23 | 24 | pluginsList: KodiPluginList = null; 25 | 26 | openRemoteAfterClickOnPlay: boolean; 27 | 28 | isLoading = false; 29 | 30 | constructor( 31 | private kodiPluginService: KodiPluginService, 32 | private alertController: AlertController, 33 | private translateService: TranslateService, 34 | private toastService: ToastService 35 | ) {} 36 | 37 | ngOnInit() { 38 | this.refresh(); 39 | 40 | this.kodiPluginService.getOpenRemoteAfterClickOnPlaySetting().then((openRemoteAfterClickOnPlay) => { 41 | this.openRemoteAfterClickOnPlay = openRemoteAfterClickOnPlay; 42 | }); 43 | } 44 | 45 | private refresh() { 46 | this.kodiPluginService.getPluginUrl().then((url) => { 47 | this.pluginsUrl = url; 48 | }); 49 | 50 | this.kodiPluginService.getPlugins().then((plugins) => { 51 | this.pluginsList = plugins; 52 | 53 | this.pluginArray = []; 54 | 55 | if (!plugins) { 56 | return; 57 | } 58 | 59 | Object.keys(plugins).forEach((key) => { 60 | const plugin = plugins[key]; 61 | this.pluginArray.push({ 62 | key, 63 | enabled: plugin.enabled, 64 | name: plugin.name 65 | }); 66 | }); 67 | }); 68 | } 69 | 70 | setUrl() { 71 | this.alertController 72 | .create({ 73 | header: 'URL', 74 | inputs: [ 75 | { 76 | name: 'url', 77 | type: 'url', 78 | placeholder: 'URL', 79 | value: this.pluginsUrl ? this.pluginsUrl : '' 80 | } 81 | ], 82 | buttons: [ 83 | { 84 | text: this.translateService.instant('alerts.cancelButton'), 85 | role: 'cancel', 86 | cssClass: 'secondary' 87 | }, 88 | { 89 | text: 'Ok', 90 | handler: (data) => { 91 | this.isLoading = true; 92 | this.kodiPluginService 93 | .setPluginsFromUrl(data.url) 94 | .pipe(finalize(() => (this.isLoading = false))) 95 | .subscribe((success) => { 96 | if (success) { 97 | this.toastService.simpleMessage('toasts.kodi-open-button.plugingUrlAdded'); 98 | this.refresh(); 99 | } else { 100 | this.toastService.simpleMessage('toasts.kodi-open-button.plugingUrlFailedToAdd'); 101 | } 102 | }); 103 | } 104 | } 105 | ] 106 | }) 107 | .then((alert) => { 108 | alert.present(); 109 | }); 110 | } 111 | 112 | toggleProvider(key: string, enabled: boolean) { 113 | this.pluginsList[key].enabled = enabled; 114 | this.kodiPluginService.setPlugins(this.pluginsList); 115 | } 116 | 117 | openRemoteAfterClickOnPlayChange(showHidden: boolean) { 118 | this.openRemoteAfterClickOnPlay = showHidden; 119 | 120 | this.kodiPluginService.setOpenRemoteAfterClickOnPlaySetting(this.openRemoteAfterClickOnPlay); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /projects/plugin/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/plugins", 5 | "types": [] 6 | }, 7 | "files": ["./src/main.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { PreloadAllModules, RouterModule, Routes } from '@angular/router'; 3 | 4 | const routes: Routes = [{ path: '', loadChildren: () => import('./tabs/tabs.module').then((m) => m.TabsPageModule) }]; 5 | @NgModule({ 6 | imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules, relativeLinkResolution: 'legacy' })], 7 | exports: [RouterModule] 8 | }) 9 | export class AppRoutingModule {} 10 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wako-unofficial-addons/nomos/98c2dc864679bec9fca15b22eb8dd966fcfe927e/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { Platform } from '@ionic/angular'; 4 | import { AppService } from './services/app.service'; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | templateUrl: 'app.component.html', 9 | styleUrls: ['app.component.scss'] 10 | }) 11 | export class AppComponent { 12 | ready = false; 13 | 14 | constructor(private platform: Platform, private appService: AppService) { 15 | this.initializeApp(); 16 | } 17 | 18 | initializeApp() { 19 | this.platform.ready().then(() => { 20 | this.appService.loadPlugins().subscribe(() => { 21 | this.ready = true; 22 | }); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { RouteReuseStrategy } from '@angular/router'; 4 | import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; 5 | import { TranslateModule } from '@ngx-translate/core'; 6 | import { WakoProviders } from '@wako-app/mobile-sdk'; 7 | import { PluginModule } from '../../projects/plugin/src/plugin/plugin.module'; 8 | import { AppRoutingModule } from './app-routing.module'; 9 | import { AppComponent } from './app.component'; 10 | import { PluginLoaderFakeService } from './services/plugin-loader-fake.service'; 11 | import { PluginLoaderService } from './services/plugin-loader.service'; 12 | 13 | @NgModule({ 14 | declarations: [AppComponent], 15 | imports: [ 16 | BrowserModule, 17 | IonicModule.forRoot({ 18 | swipeBackEnabled: true, 19 | backButtonText: '', 20 | mode: 'md', 21 | }), 22 | AppRoutingModule, 23 | TranslateModule.forRoot(), 24 | PluginModule, 25 | ], 26 | providers: [ 27 | { 28 | provide: RouteReuseStrategy, 29 | useClass: IonicRouteStrategy, 30 | }, 31 | { 32 | provide: PluginLoaderService, 33 | useClass: PluginLoaderFakeService, 34 | }, 35 | ...WakoProviders, 36 | ], 37 | bootstrap: [AppComponent], 38 | }) 39 | export class AppModule {} 40 | -------------------------------------------------------------------------------- /src/app/app.wako-like.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { RouteReuseStrategy } from '@angular/router'; 4 | 5 | import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; 6 | 7 | import { IonicStorageModule } from '@ionic/storage-angular'; 8 | import { TranslateModule } from '@ngx-translate/core'; 9 | import { WakoProviders } from '@wako-app/mobile-sdk'; 10 | import { AppRoutingModule } from './app-routing.module'; 11 | import { AppComponent } from './app.component'; 12 | 13 | @NgModule({ 14 | declarations: [AppComponent], 15 | entryComponents: [], 16 | imports: [ 17 | BrowserModule, 18 | IonicModule.forRoot({ 19 | swipeBackEnabled: true, 20 | backButtonText: '', 21 | mode: 'md', 22 | }), 23 | AppRoutingModule, 24 | TranslateModule.forRoot(), 25 | IonicStorageModule.forRoot({}), 26 | ], 27 | providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, ...WakoProviders], 28 | bootstrap: [AppComponent], 29 | }) 30 | export class AppWakoLikeModule {} 31 | -------------------------------------------------------------------------------- /src/app/episode/episode.module.ts: -------------------------------------------------------------------------------- 1 | import { IonicModule } from '@ionic/angular'; 2 | import { RouterModule } from '@angular/router'; 3 | import { NgModule } from '@angular/core'; 4 | import { CommonModule } from '@angular/common'; 5 | import { FormsModule } from '@angular/forms'; 6 | import { EpisodePage } from './episode.page'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | IonicModule, 11 | CommonModule, 12 | FormsModule, 13 | RouterModule.forChild([{path: '', component: EpisodePage}]) 14 | ], 15 | declarations: [EpisodePage] 16 | }) 17 | export class EpisodePageModule { 18 | } 19 | -------------------------------------------------------------------------------- /src/app/episode/episode.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Episode 5 | 6 | 7 | 8 | 9 | 10 |

Test Button

11 | 12 | 13 |

Test Item

14 | 15 | 16 | {{data.show.title + ' - ' + data.episode.code}} 17 | 18 | 19 | 20 | 21 | 22 |
23 | -------------------------------------------------------------------------------- /src/app/episode/episode.page.scss: -------------------------------------------------------------------------------- 1 | .welcome-card ion-img { 2 | max-height: 35vh; 3 | overflow: hidden; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/episode/episode.page.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; 2 | import { PluginLoaderService } from '../services/plugin-loader.service'; 3 | import { Episode, Show } from '@wako-app/mobile-sdk'; 4 | 5 | @Component({ 6 | selector: 'app-tab1', 7 | templateUrl: 'episode.page.html', 8 | styleUrls: ['episode.page.scss'] 9 | }) 10 | export class EpisodePage implements OnInit { 11 | @ViewChild('episodeRef', { read: ViewContainerRef, static: true }) 12 | episodeVCRef: ViewContainerRef; 13 | 14 | @ViewChild('episodeItemOptionRef', { read: ViewContainerRef, static: true }) 15 | episodeItemOptionVCRef: ViewContainerRef; 16 | 17 | data: { show: Show; episode: Episode }; 18 | 19 | constructor(private pluginLoader: PluginLoaderService) {} 20 | 21 | ngOnInit() { 22 | this.data = JSON.parse( 23 | `{"show":{"title":"Love Island Australia","year":2018,"imdbId":"tt8467296","tmdbId":79792,"tvdbId":347813,"traktId":130970,"slug":"love-island-australia","overview":"The Australian version of the hit UK's reality show, Love Island. In Mallorca, Spain, 10 Aussie singles will play the ultimate game of love. After finding their match, they must stay together while surviving temptations as new singles enter the villa.","trailer":null,"firstAired":"2018-05-27T10:00:00.000Z","runtime":55,"rating":6.7,"votes":67,"language":"en","genres":["reality"],"certification":"TV-MA","airedEpisodes":59,"images_url":{"poster":"https://image.tmdb.org/t/p/w300/zwDSJviY4rn57dHLex9L7H8sEgi.jpg","backdrop":"https://image.tmdb.org/t/p/w500/eTgj9hC6xGBMmTNSDsrJJDDy9fL.jpg","poster_original":"https://image.tmdb.org/t/p/original/zwDSJviY4rn57dHLex9L7H8sEgi.jpg","backdrop_original":"https://image.tmdb.org/t/p/original/eTgj9hC6xGBMmTNSDsrJJDDy9fL.jpg"},"status":"returning series","network":"Nine Network","alternativeTitles":{"us":"Love Island Australia","il":"אי האהבה אוסטרליה","br":"Love Island Australia"},"originalTitle":"Love Island Australia","seasons":[{"traktNumber":0,"code":"S00","isSpecial":true,"title":"Specials","tmdbId":null,"tvdbId":null,"overview":null,"firstAired":null,"rating":0,"votes":0,"airedEpisodes":0,"network":"Nine Network","episodeCount":1,"totalEpisodesWatched":0,"episodes":[{"traktSeasonNumber":0,"traktNumber":1,"absoluteNumber":null,"code":"S00E01","title":"1.2","imdbId":null,"tmdbId":null,"tvdbId":7259812,"traktId":3601453,"overview":"","firstAired":null,"rating":0,"votes":0,"runtime":55}]},{"traktNumber":1,"code":"S01","isSpecial":false,"title":"Season 1","tmdbId":104257,"tvdbId":null,"overview":"Hosted by Sophie Monk from the Spanish island of Mallorca, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2018-05-26T13:30:00.000Z","rating":8.5,"votes":11,"airedEpisodes":30,"network":"Nine Network","episodeCount":30,"totalEpisodesWatched":0,"episodes":[{"traktSeasonNumber":1,"traktNumber":1,"absoluteNumber":null,"code":"S01E01","title":"Love was in the Air","imdbId":null,"tmdbId":1495376,"tvdbId":6699288,"traktId":3010098,"overview":"Sophie kicks off tonight’s proceedings by showing us a soft porn montage of our contestants overlaid with a soundtrack of early 2000’s ringtones… just in case you’ve been wondering how that crazy frog’s been going. Basically the girls show up in their best stripper combos (bikini and heels for the uninitiated) and wait for the male contestants to show up and choose them as a partner, based purely on their appearance. #feminism","firstAired":"2018-05-27T10:00:00.000Z","rating":7.3,"votes":101,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":2,"absoluteNumber":null,"code":"S01E02","title":"Kim Divides the Household","imdbId":null,"tmdbId":1495377,"tvdbId":6699344,"traktId":3010113,"overview":"It’s the second morning in our Love Island mansion and Hurricane Kim has left a path of destruction in her wake. Last night the bikini model bounced into the house in a whirl of hair extensions, fake tan and excessively white teeth - the likes of which we haven’t seen since Britney’s Las Vegas residency.\\n\\nFortunately for us Kim is a flirty kind of gal, systematically going through each and every guy to decide which one will be her safest bet. It’s pretty darn easy when she has the ability to turn each of them into a blubbering pool of drool. The best Justin can come up with is ‘what’s your favourite colour?’ (Green) and ‘do you remember how oranges used to make your fingers smell?’ (Seriously what the hell?)","firstAired":"2018-05-28T10:00:00.000Z","rating":7.3,"votes":62,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":3,"absoluteNumber":null,"code":"S01E03","title":"Is Grant Dense?","imdbId":null,"tmdbId":1495378,"tvdbId":6699346,"traktId":3010115,"overview":"The girls and boys are realising that the next coupling is fast approaching, and it’s sent the islanders into a flurry of pacts and flirting, analysing every conversation with each other. It was all reminiscent of the pre-PE anxiety brought on when the teacher told you to pair up in primary school.","firstAired":"2018-05-29T10:00:00.000Z","rating":7.6,"votes":55,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":4,"absoluteNumber":null,"code":"S01E04","title":"Time for Fresh Meat","imdbId":null,"tmdbId":1495379,"tvdbId":6699348,"traktId":3010120,"overview":"Sophie Monk - she’s just like us! Apparently last night Soph was also yelling at the idiot box - telling Cassidy and Tayla to run for the hills… solidarity sister. It remains a mystery as to why she wasn’t allowed to go in and yell at them in person - would have made for some seriously compelling viewing. Over in the bedroom, Kim is still trying to get away from Josh. It’s like the bed is no man’s land and she feels safer down in the trenches. We find out they finally spooned (well Josh IS the best at it) but Kim 150% denied Josh any mouth on mouth action. And boy did he try.","firstAired":"2018-05-30T10:00:00.000Z","rating":7.1,"votes":49,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":5,"absoluteNumber":null,"code":"S01E05","title":"The Next Love Circle","imdbId":null,"tmdbId":1495380,"tvdbId":6699351,"traktId":3010122,"overview":"Sleep. The great equalizer. You can be as beautiful, fit or tanned as you like, but the moment you close your eyes and the night vision switches on… we’re all the same. We have our mouths open and are inelegantly sprawled across the bed (two thumbs up Tash), or we elbow our bedmate directly in the face without waking up (great work Justin).\\n\\nAfter rising and applying 45 layers of fresh tan, Millie and Tash both decide to make a play for John James… probably wasn’t the best strategy turning up in the exact same yellow bikini though? Thankfully they have different coloured hair and are thus, easily identifiable. As usual Millie is front and centre - and as usual Tash is annoyed about it.","firstAired":"2018-05-31T10:00:00.000Z","rating":7.3,"votes":51,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":6,"absoluteNumber":null,"code":"S01E06","title":"Tayla Loses Her Mind","imdbId":null,"tmdbId":1499166,"tvdbId":6703221,"traktId":3013414,"overview":"After Eden’s poetry reading last week, it seems Josh too has become inspired - treating us to the kind of gems Yeats would be jealous of. Calling himself the ‘poetry killa’ he lets us know he feels ‘fresher than a newly washed piece of corn’ and the romance is almost too much to take.\\n\\nJosh trots outside to try some more fresh rhymes on Tayla, the current focus of his attention, after Kim made it pretty darn clear she has 0% interest in him. Tayla and Josh have a solid D&M about the length of her leg hair, after which she tells him not to hold her hand - so naturally he tries to hold her hand. He’s really working hard to hold the title of most oblivious man in the house (currently held by Justin).","firstAired":"2018-06-03T10:00:00.000Z","rating":7.2,"votes":44,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":7,"absoluteNumber":null,"code":"S01E07","title":"Time to Slut-Shame a Girl Out of the Villa","imdbId":null,"tmdbId":1499167,"tvdbId":6705375,"traktId":3017543,"overview":"Wounded yet stoic, Cassie cautions Tayla that she’s not ready to talk to her yet, lest she say anything nasty that she’ll regret later. Proving her deep sensitivity, Tayla keeps flapping her gums while following Cassie around, making sure the jilted blonde knows just how willing she is to respect those boundaries that she definitely will respect just as soon as she finishes delivering her neverending monologue about just how respectful she intends to be.\\n\\nIn an almost identical conversation, Grant approaches Josh, whose interest in Tayla was thwarted by the previous night’s re-coupling saga as well.","firstAired":"2018-06-04T10:00:00.000Z","rating":7,"votes":44,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":8,"absoluteNumber":null,"code":"S01E08","title":"Yes we Know you're Together","imdbId":null,"tmdbId":1501135,"tvdbId":6705377,"traktId":3017544,"overview":"Just in case you missed it, Grant and Tayla are a couple now. Tayla is telling anyone and everyone who’ll listen, including the camera in the diary room... like we can’t see the two of them constantly making out like two drunk kids at the year 12 formal. Luckily there’s no chance of anyone’s braces getting stuck together.\\n\\nTayla is all of a sudden head over heels for Grant - despite being indifferent to him three days ago - and wants us all to know it. She can’t stop smiling and telling us Grant has her back. Poor fool.","firstAired":"2018-06-05T10:00:00.000Z","rating":7.2,"votes":45,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":9,"absoluteNumber":null,"code":"S01E09","title":"Episode 9","imdbId":null,"tmdbId":1501134,"tvdbId":6706458,"traktId":3017545,"overview":null,"firstAired":"2018-06-06T10:00:00.000Z","rating":7.4,"votes":37,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":10,"absoluteNumber":null,"code":"S01E10","title":"Episode 10","imdbId":null,"tmdbId":1501133,"tvdbId":6708071,"traktId":3017546,"overview":null,"firstAired":"2018-06-07T10:00:00.000Z","rating":7.5,"votes":44,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":11,"absoluteNumber":null,"code":"S01E11","title":"Episode 11","imdbId":null,"tmdbId":1501136,"tvdbId":6711528,"traktId":3022606,"overview":null,"firstAired":"2018-06-10T10:00:00.000Z","rating":7.1,"votes":42,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":12,"absoluteNumber":null,"code":"S01E12","title":"Episode 12","imdbId":null,"tmdbId":1501572,"tvdbId":6712533,"traktId":3022607,"overview":null,"firstAired":"2018-06-11T10:00:00.000Z","rating":7.2,"votes":37,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":13,"absoluteNumber":null,"code":"S01E13","title":"Episode 13","imdbId":null,"tmdbId":1501573,"tvdbId":6715086,"traktId":3025120,"overview":null,"firstAired":"2018-06-12T10:00:00.000Z","rating":6.9,"votes":38,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":14,"absoluteNumber":null,"code":"S01E14","title":"Episode 14","imdbId":null,"tmdbId":1501575,"tvdbId":6715088,"traktId":3025121,"overview":null,"firstAired":"2018-06-13T10:00:00.000Z","rating":7.2,"votes":34,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":15,"absoluteNumber":null,"code":"S01E15","title":"Episode 15","imdbId":null,"tmdbId":1501577,"tvdbId":6716867,"traktId":3027103,"overview":null,"firstAired":"2018-06-14T10:00:00.000Z","rating":7.3,"votes":35,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":16,"absoluteNumber":null,"code":"S01E16","title":"Episode 16","imdbId":null,"tmdbId":1507727,"tvdbId":6719853,"traktId":3030892,"overview":null,"firstAired":"2018-06-17T10:00:00.000Z","rating":7.1,"votes":36,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":17,"absoluteNumber":null,"code":"S01E17","title":"Episode 17","imdbId":null,"tmdbId":1508739,"tvdbId":6722405,"traktId":3035016,"overview":null,"firstAired":"2018-06-18T10:00:00.000Z","rating":7.1,"votes":39,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":18,"absoluteNumber":null,"code":"S01E18","title":"Episode 18","imdbId":null,"tmdbId":1509036,"tvdbId":6722406,"traktId":3035019,"overview":null,"firstAired":"2018-06-19T10:00:00.000Z","rating":7.4,"votes":38,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":19,"absoluteNumber":null,"code":"S01E19","title":"Episode 19","imdbId":null,"tmdbId":1509188,"tvdbId":6724681,"traktId":3042680,"overview":null,"firstAired":"2018-06-20T10:00:00.000Z","rating":7.3,"votes":37,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":20,"absoluteNumber":null,"code":"S01E20","title":"Episode 20","imdbId":null,"tmdbId":1509556,"tvdbId":6724683,"traktId":3042681,"overview":null,"firstAired":"2018-06-21T10:00:00.000Z","rating":7.3,"votes":35,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":21,"absoluteNumber":null,"code":"S01E21","title":"Episode 21","imdbId":null,"tmdbId":1510938,"tvdbId":6728883,"traktId":3047580,"overview":null,"firstAired":"2018-06-24T10:00:00.000Z","rating":7.2,"votes":29,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":22,"absoluteNumber":null,"code":"S01E22","title":"Episode 22","imdbId":null,"tmdbId":1511278,"tvdbId":6729572,"traktId":3047581,"overview":null,"firstAired":"2018-06-25T10:00:00.000Z","rating":7.5,"votes":32,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":23,"absoluteNumber":null,"code":"S01E23","title":"Episode 23","imdbId":null,"tmdbId":1511585,"tvdbId":6734985,"traktId":3055543,"overview":"A surprise text is sprung upon the girls on their spa day out, forcing them to make a difficult decision that will drastically change their time left in the Villa.","firstAired":"2018-06-26T10:00:00.000Z","rating":7,"votes":29,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":24,"absoluteNumber":null,"code":"S01E24","title":"Episode 24","imdbId":null,"tmdbId":1512035,"tvdbId":6734987,"traktId":3055544,"overview":null,"firstAired":"2018-06-27T10:00:00.000Z","rating":7.2,"votes":33,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":25,"absoluteNumber":null,"code":"S01E25","title":"Episode 25","imdbId":null,"tmdbId":1512684,"tvdbId":6734989,"traktId":3055545,"overview":null,"firstAired":"2018-06-28T10:00:00.000Z","rating":7.4,"votes":31,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":26,"absoluteNumber":null,"code":"S01E26","title":"Episode 26","imdbId":null,"tmdbId":1514067,"tvdbId":6738449,"traktId":3058500,"overview":null,"firstAired":"2018-07-01T10:00:00.000Z","rating":7.3,"votes":32,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":27,"absoluteNumber":null,"code":"S01E27","title":"Episode 27","imdbId":null,"tmdbId":1514310,"tvdbId":6738451,"traktId":3058509,"overview":null,"firstAired":"2018-07-02T10:00:00.000Z","rating":6.9,"votes":27,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":28,"absoluteNumber":null,"code":"S01E28","title":"Episode 28","imdbId":null,"tmdbId":1514968,"tvdbId":6740594,"traktId":3059536,"overview":null,"firstAired":"2018-07-03T10:00:00.000Z","rating":7.3,"votes":24,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":29,"absoluteNumber":null,"code":"S01E29","title":"Episode 29","imdbId":null,"tmdbId":1515321,"tvdbId":6740595,"traktId":3059537,"overview":null,"firstAired":"2018-07-04T10:00:00.000Z","rating":7.4,"votes":24,"runtime":55,"watched":false},{"traktSeasonNumber":1,"traktNumber":30,"absoluteNumber":null,"code":"S01E30","title":"Episode 30","imdbId":null,"tmdbId":1515613,"tvdbId":6742135,"traktId":3063091,"overview":null,"firstAired":"2018-07-05T10:00:00.000Z","rating":7.5,"votes":24,"runtime":55,"watched":false}]},{"traktNumber":2,"code":"S02","isSpecial":false,"title":"Season 2","tmdbId":133736,"tvdbId":null,"overview":null,"firstAired":"2019-10-07T09:30:00.000Z","rating":6.5,"votes":2,"airedEpisodes":29,"network":"Nine Network","episodeCount":29,"totalEpisodesWatched":0,"episodes":[{"traktSeasonNumber":2,"traktNumber":1,"absoluteNumber":null,"code":"S02E01","title":"Episode 1","imdbId":null,"tmdbId":1947629,"tvdbId":7259813,"traktId":3601454,"overview":"The hottest show is back! Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-10-07T09:30:00.000Z","rating":7.4,"votes":31,"runtime":55},{"traktSeasonNumber":2,"traktNumber":2,"absoluteNumber":null,"code":"S02E02","title":"Episode 2","imdbId":null,"tmdbId":1949252,"tvdbId":7369090,"traktId":3723118,"overview":"The hottest show is back! Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-10-08T09:30:00.000Z","rating":7.1,"votes":23,"runtime":55},{"traktSeasonNumber":2,"traktNumber":3,"absoluteNumber":null,"code":"S02E03","title":"Episode 3","imdbId":null,"tmdbId":1950461,"tvdbId":7369091,"traktId":3723119,"overview":"The hottest show is back! Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-10-09T09:30:00.000Z","rating":7.2,"votes":21,"runtime":55},{"traktSeasonNumber":2,"traktNumber":4,"absoluteNumber":null,"code":"S02E04","title":"Episode 4","imdbId":null,"tmdbId":1950467,"tvdbId":7369092,"traktId":3723133,"overview":"The hottest show is back! Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa","firstAired":"2019-10-10T09:30:00.000Z","rating":7.4,"votes":23,"runtime":55},{"traktSeasonNumber":2,"traktNumber":5,"absoluteNumber":null,"code":"S02E05","title":"Episode 5","imdbId":null,"tmdbId":1950468,"tvdbId":7369093,"traktId":3723134,"overview":"The hottest show is back! Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-10-11T09:30:00.000Z","rating":7.6,"votes":22,"runtime":55},{"traktSeasonNumber":2,"traktNumber":6,"absoluteNumber":null,"code":"S02E06","title":"Episode 6","imdbId":null,"tmdbId":1952488,"tvdbId":7383607,"traktId":3744130,"overview":"The hottest show is back! Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-10-14T09:30:00.000Z","rating":7.2,"votes":21,"runtime":55},{"traktSeasonNumber":2,"traktNumber":7,"absoluteNumber":null,"code":"S02E07","title":"Episode 7","imdbId":null,"tmdbId":1952885,"tvdbId":7383608,"traktId":3744138,"overview":"The hottest show is back! Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-10-15T09:30:00.000Z","rating":7.4,"votes":19,"runtime":55},{"traktSeasonNumber":2,"traktNumber":8,"absoluteNumber":null,"code":"S02E08","title":"Episode 8","imdbId":null,"tmdbId":1954131,"tvdbId":7383611,"traktId":3744139,"overview":"The hottest show is back! Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-10-16T09:30:00.000Z","rating":7.2,"votes":20,"runtime":55},{"traktSeasonNumber":2,"traktNumber":9,"absoluteNumber":null,"code":"S02E09","title":"Episode 9","imdbId":null,"tmdbId":1954782,"tvdbId":7383614,"traktId":3744140,"overview":"The hottest show is back! Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-10-17T09:30:00.000Z","rating":6.8,"votes":19,"runtime":55},{"traktSeasonNumber":2,"traktNumber":10,"absoluteNumber":null,"code":"S02E10","title":"Episode 10","imdbId":null,"tmdbId":1957490,"tvdbId":7383616,"traktId":3744141,"overview":"The hottest show is back! Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-10-18T09:30:00.000Z","rating":7.8,"votes":17,"runtime":55},{"traktSeasonNumber":2,"traktNumber":11,"absoluteNumber":null,"code":"S02E11","title":"Episode 11","imdbId":null,"tmdbId":1959559,"tvdbId":7383619,"traktId":3744142,"overview":"The hottest show is back! Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-10-21T09:30:00.000Z","rating":7.1,"votes":21,"runtime":55},{"traktSeasonNumber":2,"traktNumber":12,"absoluteNumber":null,"code":"S02E12","title":"Episode 12","imdbId":null,"tmdbId":1960921,"tvdbId":7383622,"traktId":3744143,"overview":"Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-10-22T09:30:00.000Z","rating":7.7,"votes":19,"runtime":55},{"traktSeasonNumber":2,"traktNumber":13,"absoluteNumber":null,"code":"S02E13","title":"Episode 13","imdbId":null,"tmdbId":1961394,"tvdbId":7383624,"traktId":3744144,"overview":"Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-10-23T09:30:00.000Z","rating":7.3,"votes":18,"runtime":55},{"traktSeasonNumber":2,"traktNumber":14,"absoluteNumber":null,"code":"S02E14","title":"Episode 14","imdbId":null,"tmdbId":1963298,"tvdbId":7385488,"traktId":3760566,"overview":"Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-10-24T09:30:00.000Z","rating":8.1,"votes":16,"runtime":55},{"traktSeasonNumber":2,"traktNumber":15,"absoluteNumber":null,"code":"S02E15","title":"Episode 15","imdbId":null,"tmdbId":1968124,"tvdbId":7385490,"traktId":3760567,"overview":"Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-10-27T09:30:00.000Z","rating":7.2,"votes":16,"runtime":55},{"traktSeasonNumber":2,"traktNumber":16,"absoluteNumber":null,"code":"S02E16","title":"Episode 16","imdbId":null,"tmdbId":1969098,"tvdbId":7385491,"traktId":3760568,"overview":"Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-10-28T09:30:00.000Z","rating":6.9,"votes":14,"runtime":55},{"traktSeasonNumber":2,"traktNumber":17,"absoluteNumber":null,"code":"S02E17","title":"Episode 17","imdbId":null,"tmdbId":1970345,"tvdbId":7385492,"traktId":3760569,"overview":"Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-10-29T09:30:00.000Z","rating":7.3,"votes":21,"runtime":55},{"traktSeasonNumber":2,"traktNumber":18,"absoluteNumber":null,"code":"S02E18","title":"Episode 18","imdbId":null,"tmdbId":1971214,"tvdbId":7385493,"traktId":3760570,"overview":"Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-10-30T09:30:00.000Z","rating":7.4,"votes":17,"runtime":55},{"traktSeasonNumber":2,"traktNumber":19,"absoluteNumber":null,"code":"S02E19","title":"Episode 19","imdbId":null,"tmdbId":1972243,"tvdbId":7385494,"traktId":3760572,"overview":"Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-10-31T09:30:00.000Z","rating":7.5,"votes":17,"runtime":55},{"traktSeasonNumber":2,"traktNumber":20,"absoluteNumber":null,"code":"S02E20","title":"Episode 20","imdbId":null,"tmdbId":1975270,"tvdbId":7385495,"traktId":3760573,"overview":"One islander’s decision rocks the villa during an emotional recoupling and couples begin to crumble under the pressure.","firstAired":"2019-11-03T09:30:00.000Z","rating":7.7,"votes":13,"runtime":55},{"traktSeasonNumber":2,"traktNumber":21,"absoluteNumber":null,"code":"S02E21","title":"Episode 21","imdbId":null,"tmdbId":1976450,"tvdbId":7426770,"traktId":3788458,"overview":"Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-11-04T09:30:00.000Z","rating":7.4,"votes":18,"runtime":55},{"traktSeasonNumber":2,"traktNumber":22,"absoluteNumber":null,"code":"S02E22","title":"Episode 22","imdbId":null,"tmdbId":1976964,"tvdbId":7426772,"traktId":3788459,"overview":"Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-11-05T09:30:00.000Z","rating":7.2,"votes":14,"runtime":55},{"traktSeasonNumber":2,"traktNumber":23,"absoluteNumber":null,"code":"S02E23","title":"Episode 23","imdbId":null,"tmdbId":1977410,"tvdbId":7426773,"traktId":3788460,"overview":"Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-11-06T09:30:00.000Z","rating":7.2,"votes":19,"runtime":55},{"traktSeasonNumber":2,"traktNumber":24,"absoluteNumber":null,"code":"S02E24","title":"Episode 24","imdbId":null,"tmdbId":1977781,"tvdbId":7426774,"traktId":3788461,"overview":"Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-11-07T09:30:00.000Z","rating":6.9,"votes":16,"runtime":55},{"traktSeasonNumber":2,"traktNumber":25,"absoluteNumber":null,"code":"S02E25","title":"Episode 25","imdbId":null,"tmdbId":1979705,"tvdbId":7437899,"traktId":3799944,"overview":"Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-11-10T09:30:00.000Z","rating":7.5,"votes":12,"runtime":55},{"traktSeasonNumber":2,"traktNumber":26,"absoluteNumber":null,"code":"S02E26","title":"Episode 26","imdbId":null,"tmdbId":1980860,"tvdbId":7441359,"traktId":3804402,"overview":"Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-11-11T09:30:00.000Z","rating":7.3,"votes":12,"runtime":55},{"traktSeasonNumber":2,"traktNumber":27,"absoluteNumber":null,"code":"S02E27","title":"Episode 27","imdbId":null,"tmdbId":1981120,"tvdbId":7441362,"traktId":3804403,"overview":"Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-11-12T09:30:00.000Z","rating":7.2,"votes":10,"runtime":55},{"traktSeasonNumber":2,"traktNumber":28,"absoluteNumber":null,"code":"S02E28","title":"Episode 28","imdbId":null,"tmdbId":1982868,"tvdbId":7441364,"traktId":3804404,"overview":"Hosted by Sophie Monk, 10 beautiful Aussie singles will play the ultimate game of love. After they find their match, they must stay together while surviving temptations as new singles enter the villa.","firstAired":"2019-11-13T09:30:00.000Z","rating":7.2,"votes":10,"runtime":55},{"traktSeasonNumber":2,"traktNumber":29,"absoluteNumber":null,"code":"S02E29","title":"Episode 29","imdbId":null,"tmdbId":1983533,"tvdbId":7441367,"traktId":3804405,"overview":"Your favourite Islanders have spent six weeks in a seaside villa, and now it's time to find out who's the winning couple as voted by the viewers. The winning couple is given $50,000 to start their new lives together, who will it be?","firstAired":"2019-11-14T09:30:00.000Z","rating":7.5,"votes":10,"runtime":55}]}]},"episode":{"traktSeasonNumber":1,"traktNumber":3,"absoluteNumber":null,"code":"S01E03","title":"Is Grant Dense?","imdbId":null,"tmdbId":1495378,"tvdbId":6699346,"traktId":3010115,"overview":"The girls and boys are realising that the next coupling is fast approaching, and it’s sent the islanders into a flurry of pacts and flirting, analysing every conversation with each other. It was all reminiscent of the pre-PE anxiety brought on when the teacher told you to pair up in primary school.","firstAired":"2018-05-29T10:00:00.000Z","rating":7.6,"votes":55,"runtime":55,"watched":false}}` 24 | ); 25 | 26 | this.loadPlugin(); 27 | 28 | this.test(); 29 | } 30 | 31 | loadPlugin() { 32 | this.pluginLoader.createComponent('episodes', this.episodeVCRef, this.data); 33 | } 34 | 35 | private test() {} 36 | } 37 | -------------------------------------------------------------------------------- /src/app/movie/movie.module.ts: -------------------------------------------------------------------------------- 1 | import { IonicModule } from '@ionic/angular'; 2 | import { RouterModule } from '@angular/router'; 3 | import { NgModule } from '@angular/core'; 4 | import { CommonModule } from '@angular/common'; 5 | import { FormsModule } from '@angular/forms'; 6 | import { MoviePage } from './movie.page'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | IonicModule, 11 | CommonModule, 12 | FormsModule, 13 | RouterModule.forChild([{path: '', component: MoviePage}]) 14 | ], 15 | declarations: [MoviePage] 16 | }) 17 | export class MoviePageModule { 18 | } 19 | -------------------------------------------------------------------------------- /src/app/movie/movie.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Movie 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/app/movie/movie.page.scss: -------------------------------------------------------------------------------- 1 | .welcome-card ion-img { 2 | max-height: 35vh; 3 | overflow: hidden; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/movie/movie.page.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; 2 | import { PluginLoaderService } from '../services/plugin-loader.service'; 3 | 4 | @Component({ 5 | selector: 'app-tab1', 6 | templateUrl: 'movie.page.html', 7 | styleUrls: ['movie.page.scss'] 8 | }) 9 | export class MoviePage implements OnInit { 10 | @ViewChild('movieRef', { read: ViewContainerRef, static: true }) 11 | movieVCRef: ViewContainerRef; 12 | 13 | constructor(private pluginLoader: PluginLoaderService) {} 14 | 15 | ngOnInit() { 16 | this.loadPlugin(); 17 | } 18 | 19 | loadPlugin() { 20 | const data = JSON.parse( 21 | `{"movie":{"relatedIds":[],"title":"Africa Screams","year":1949,"imdbId":"tt0041098","tmdbId":20278,"traktId":12436,"trailer":"http://youtube.com/watch?v=USOHyNuOCZ0","certification":"NR","tagline":"A Zany, Hilarious Romp!","overview":"When bookseller Buzz cons Diana into thinking that his friend Stanley knows all there is to know about Africa, they are abducted and ordered to lead Diana and her henchmen to an African tribe in search of a fortune in jewels.","released":"1949-05-04","runtime":79,"rating":6.2,"votes":45,"language":"en","genres":["comedy"],"images_url":{"poster":"https://image.tmdb.org/t/p/w300/l4imef1WmxynQM8NiOnUeOgtgKj.jpg","backdrop":"https://image.tmdb.org/t/p/w500/fC7SQH4wDCnmpXpySrBP9Qft4A3.jpg","poster_original":"https://image.tmdb.org/t/p/original/l4imef1WmxynQM8NiOnUeOgtgKj.jpg","backdrop_original":"https://image.tmdb.org/t/p/original/fC7SQH4wDCnmpXpySrBP9Qft4A3.jpg"},"alternativeTitles":{"de":"Verrücktes Afrika","us":"Africa Screams","es":"Las Minas del Rey Salmonete","fr":"Africa Screams","pl":"Afrykańska przygoda (1949)","rs":"Афрички врисак"},"originalTitle":"Africa Screams"}}` 22 | ); 23 | 24 | this.pluginLoader.createComponent('movies', this.movieVCRef, data); 25 | 26 | // this.test(); 27 | } 28 | 29 | private test() { 30 | // let a = null; 31 | // 32 | // let tTitle = 'Africa scream'; 33 | // 34 | // let originalQuery = 'Africa scream 1949'; 35 | // 36 | // let sourceQuery: SourceQuery = JSON.parse(` 37 | // {"movie":{"relatedIds":[],"title":"Africa Screams","year":1949,"imdbId":"tt0041098","tmdbId":20278,"traktId":12436,"trailer":"http://youtube.com/watch?v=USOHyNuOCZ0","certification":"NR","tagline":"A Zany, Hilarious Romp!","overview":"When bookseller Buzz cons Diana into thinking that his friend Stanley knows all there is to know about Africa, they are abducted and ordered to lead Diana and her henchmen to an African tribe in search of a fortune in jewels.","released":"1949-05-04","runtime":79,"rating":6.2,"votes":45,"language":"en","genres":["comedy"],"images_url":{"poster":"https://image.tmdb.org/t/p/w300/l4imef1WmxynQM8NiOnUeOgtgKj.jpg","backdrop":"https://image.tmdb.org/t/p/w500/fC7SQH4wDCnmpXpySrBP9Qft4A3.jpg","poster_original":"https://image.tmdb.org/t/p/original/l4imef1WmxynQM8NiOnUeOgtgKj.jpg","backdrop_original":"https://image.tmdb.org/t/p/original/fC7SQH4wDCnmpXpySrBP9Qft4A3.jpg"},"alternativeTitles":{"de":"Verrücktes Afrika","us":"Africa Screams","es":"Las Minas del Rey Salmonete","fr":"Africa Screams","pl":"Afrykańska przygoda (1949)","rs":"Афрички врисак"},"originalTitle":"Africa Screams"}} 38 | // `); 39 | // 40 | // a = SourceUtils.isMovieTitleMatching(tTitle, originalQuery, sourceQuery.movie); 41 | // 42 | // console.log('test matching', tTitle, a); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/services/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { KodiAppService } from '@wako-app/mobile-sdk'; 3 | import { PluginLoaderService } from './plugin-loader.service'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class AppService { 9 | constructor(protected pluginLoader: PluginLoaderService) { 10 | KodiAppService.currentHost = { 11 | name: 'MyHost', 12 | host: '127.0.0.1', 13 | port: 8080 14 | }; 15 | } 16 | 17 | loadPlugins() { 18 | return this.pluginLoader.install('/assets/plugins/manifest.json', 'en'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/services/plugin-loader-fake.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Injector } from '@angular/core'; 2 | import { 3 | PluginBaseService, 4 | PluginDetail, 5 | PluginManifest, 6 | WakoBaseHttpService, 7 | WakoPluginLoaderService, 8 | } from '@wako-app/mobile-sdk'; 9 | import { forkJoin, from, of, throwError } from 'rxjs'; 10 | import { catchError, mapTo, switchMap, tap } from 'rxjs/operators'; 11 | import { PluginModule } from '../../../projects/plugin/src/plugin/plugin.module'; 12 | 13 | @Injectable({ 14 | providedIn: 'root', 15 | }) 16 | export class PluginLoaderFakeService extends WakoPluginLoaderService { 17 | constructor(injector: Injector) { 18 | super(injector); 19 | } 20 | 21 | install(manifestUrl: string, lang: string, loadIt = true) { 22 | manifestUrl = manifestUrl.replace('/plugins/', '/'); 23 | let pluginId = null; 24 | return WakoBaseHttpService.get(manifestUrl).pipe( 25 | switchMap((manifest) => { 26 | manifest.url = manifestUrl; 27 | 28 | pluginId = manifest.id; 29 | 30 | const paths = manifestUrl.split('/'); 31 | paths.pop(); 32 | const baseUrl = paths.join('/'); 33 | 34 | const pluginDetail = new PluginDetail(); 35 | 36 | pluginDetail.manifestUrl = manifestUrl; 37 | pluginDetail.manifest = manifest; 38 | 39 | pluginDetail.source = null; 40 | 41 | if (manifest.languages) { 42 | pluginDetail.languages = {}; 43 | const obss = []; 44 | Object.keys(manifest.languages).forEach((langKey) => { 45 | const langUrl = manifest.languages[langKey].match('http') 46 | ? manifest.languages[langKey] 47 | : baseUrl + manifest.languages[langKey]; 48 | 49 | const obs = WakoBaseHttpService.get(langUrl).pipe( 50 | catchError((err) => { 51 | console.error('Incorrect JSON: ' + langUrl, err); 52 | return throwError(err); 53 | }), 54 | tap((data) => { 55 | pluginDetail.languages[langKey] = data; 56 | }) 57 | ); 58 | 59 | obss.push(obs); 60 | }); 61 | 62 | return forkJoin(obss).pipe(mapTo(pluginDetail)); 63 | } 64 | 65 | return of(pluginDetail); 66 | }), 67 | switchMap((pluginDetail) => { 68 | return from(this.savePluginDetail(pluginDetail.manifest.id, pluginDetail)); 69 | }), 70 | switchMap(() => { 71 | return from(this.addToList(pluginId)); 72 | }), 73 | switchMap(() => { 74 | return this.load(pluginId, lang, true); 75 | }), 76 | tap(() => { 77 | this.loaded$.next(true); 78 | this.newPlugin$.next(true); 79 | }) 80 | ); 81 | } 82 | 83 | protected load(pluginId: string, lang: string, isFirstLoad: boolean) { 84 | return from(this.getPluginDetail(pluginId)).pipe( 85 | tap((pluginDetail) => { 86 | const moduleType = PluginModule; 87 | 88 | const pluginService = this.injector.get(moduleType.pluginService) as PluginBaseService; 89 | 90 | this.pluginModuleMap.set(pluginDetail.manifest.id, { 91 | pluginDetail, 92 | moduleType: moduleType, 93 | injector: null, 94 | }); 95 | 96 | pluginService.initialize(); 97 | 98 | if (isFirstLoad) { 99 | pluginService.afterInstall(); 100 | } 101 | 102 | this.setLang(pluginId, lang); 103 | }) 104 | ); 105 | } 106 | 107 | getPluginService(pluginId: string): any { 108 | const plugin = this.pluginModuleMap.get(pluginId); 109 | if (plugin) { 110 | return this.injector.get(plugin.moduleType.pluginService) as PluginBaseService; 111 | } 112 | return null; 113 | } 114 | 115 | // createComponent(action: PluginAction, viewContainerRef: ViewContainerRef, data?: any) { 116 | // return super.createComponent(action, viewContainerRef, data); 117 | // // this.pluginModuleMap.forEach(pluginMap => { 118 | // // const moduleType = PluginModule; 119 | // // 120 | // // if (action === 'movies' && pluginMap.pluginDetail.manifest.actions.includes(action) && moduleType.movieComponent) { 121 | // // const compFactory = this.cfr.resolveComponentFactory(moduleType.movieComponent); 122 | // // const movieComponent = viewContainerRef.createComponent(compFactory); 123 | // // 124 | // // movieComponent.instance.setMovie(data.movie); 125 | // // } else if (action === 'shows' && pluginMap.pluginDetail.manifest.actions.includes(action) && moduleType.showComponent) { 126 | // // const compFactory = this.cfr.resolveComponentFactory(moduleType.showComponent); 127 | // // const episodeComponent = viewContainerRef.createComponent(compFactory); 128 | // // 129 | // // episodeComponent.instance.setShowEpisode(data.show, data.episode); 130 | // // } else if (action === 'episodes' && pluginMap.pluginDetail.manifest.actions.includes(action) && moduleType.episodeComponent) { 131 | // // const compFactory = this.cfr.resolveComponentFactory(moduleType.episodeComponent); 132 | // // const episodeComponent = viewContainerRef.createComponent(compFactory); 133 | // // 134 | // // episodeComponent.instance.setShowEpisode(data.show, data.episode); 135 | // // } else if (action === 'settings' && moduleType.settingsComponent) { 136 | // // const compFactory = this.cfr.resolveComponentFactory(moduleType.settingsComponent); 137 | // // viewContainerRef.createComponent(compFactory); 138 | // // } else if (action === 'plugin-detail' && moduleType.pluginDetailComponent) { 139 | // // const compFactory = this.cfr.resolveComponentFactory(moduleType.pluginDetailComponent); 140 | // // viewContainerRef.createComponent(compFactory); 141 | // // } 142 | // // }); 143 | // } 144 | } 145 | -------------------------------------------------------------------------------- /src/app/services/plugin-loader.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Injector } from '@angular/core'; 2 | import { WakoPluginLoaderService } from '@wako-app/mobile-sdk'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class PluginLoaderService extends WakoPluginLoaderService { 8 | constructor(injector: Injector) { 9 | super(injector); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/settings/addon-detail/addon-detail.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Addon Detail 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/app/settings/addon-detail/addon-detail.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wako-unofficial-addons/nomos/98c2dc864679bec9fca15b22eb8dd966fcfe927e/src/app/settings/addon-detail/addon-detail.component.scss -------------------------------------------------------------------------------- /src/app/settings/addon-detail/addon-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; 2 | import { PluginLoaderService } from '../../services/plugin-loader.service'; 3 | 4 | @Component({ 5 | selector: 'wk-addon-detail', 6 | templateUrl: './addon-detail.component.html', 7 | styleUrls: ['./addon-detail.component.scss'] 8 | }) 9 | export class AddonDetailComponent implements OnInit { 10 | @ViewChild('detailRef', { read: ViewContainerRef, static: true }) 11 | detailRef: ViewContainerRef; 12 | 13 | constructor(private pluginLoader: PluginLoaderService) {} 14 | 15 | ngOnInit() { 16 | this.pluginLoader.createComponent('plugin-detail', this.detailRef); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/settings/addon-settings/addon-settings.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Addon Settings 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/app/settings/addon-settings/addon-settings.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wako-unofficial-addons/nomos/98c2dc864679bec9fca15b22eb8dd966fcfe927e/src/app/settings/addon-settings/addon-settings.component.scss -------------------------------------------------------------------------------- /src/app/settings/addon-settings/addon-settings.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; 2 | import { PluginLoaderService } from '../../services/plugin-loader.service'; 3 | 4 | @Component({ 5 | selector: 'wk-addon-settings', 6 | templateUrl: './addon-settings.component.html', 7 | styleUrls: ['./addon-settings.component.scss'] 8 | }) 9 | export class AddonSettingsComponent implements OnInit { 10 | @ViewChild('settingsRef', { read: ViewContainerRef, static: true }) 11 | settingsRef: ViewContainerRef; 12 | 13 | constructor(private pluginLoader: PluginLoaderService) {} 14 | 15 | ngOnInit() { 16 | this.pluginLoader.createComponent('settings', this.settingsRef, null); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/settings/settings.module.ts: -------------------------------------------------------------------------------- 1 | import { IonicModule } from '@ionic/angular'; 2 | import { RouterModule } from '@angular/router'; 3 | import { NgModule } from '@angular/core'; 4 | import { CommonModule } from '@angular/common'; 5 | import { FormsModule } from '@angular/forms'; 6 | import { SettingsPage } from './settings.page'; 7 | import { AddonSettingsComponent } from './addon-settings/addon-settings.component'; 8 | import { AddonDetailComponent } from './addon-detail/addon-detail.component'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | IonicModule, 13 | CommonModule, 14 | FormsModule, 15 | RouterModule.forChild([ 16 | { 17 | path: '', 18 | component: SettingsPage 19 | }, 20 | {path: 'addon-settings', component: AddonSettingsComponent}, 21 | {path: 'addon-detail', component: AddonDetailComponent} 22 | ]) 23 | ], 24 | declarations: [SettingsPage, AddonSettingsComponent, AddonDetailComponent], 25 | }) 26 | export class SettingsPageModule { 27 | } 28 | -------------------------------------------------------------------------------- /src/app/settings/settings.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Settings Plugin 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Open Detail (if any) 14 | 15 | 16 | 17 | 18 | Open Settings (if any) 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/app/settings/settings.page.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/settings/settings.page.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; 2 | import { AddonSettingsComponent } from './addon-settings/addon-settings.component'; 3 | import { ModalController } from '@ionic/angular'; 4 | import { PluginLoaderService } from '../services/plugin-loader.service'; 5 | 6 | @Component({ 7 | selector: 'app-tab2', 8 | templateUrl: 'settings.page.html', 9 | styleUrls: ['settings.page.scss'] 10 | }) 11 | export class SettingsPage implements OnInit { 12 | @ViewChild('settingsRef', { read: ViewContainerRef, static: true }) 13 | settingsRef: ViewContainerRef; 14 | 15 | constructor(private pluginLoader: PluginLoaderService, private modalCtrl: ModalController) {} 16 | 17 | ngOnInit() {} 18 | 19 | async addonSettings() { 20 | const modal = await this.modalCtrl.create({ 21 | component: AddonSettingsComponent 22 | }); 23 | 24 | modal.present(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/tabs/tabs.module.ts: -------------------------------------------------------------------------------- 1 | import { IonicModule } from '@ionic/angular'; 2 | import { NgModule } from '@angular/core'; 3 | import { CommonModule } from '@angular/common'; 4 | import { FormsModule } from '@angular/forms'; 5 | 6 | import { TabsPageRoutingModule } from './tabs.router.module'; 7 | 8 | import { TabsPage } from './tabs.page'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | IonicModule, 13 | CommonModule, 14 | FormsModule, 15 | TabsPageRoutingModule 16 | ], 17 | declarations: [TabsPage] 18 | }) 19 | export class TabsPageModule {} 20 | -------------------------------------------------------------------------------- /src/app/tabs/tabs.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/app/tabs/tabs.page.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/tabs/tabs.page.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-tabs', 5 | templateUrl: 'tabs.page.html', 6 | styleUrls: ['tabs.page.scss'] 7 | }) 8 | export class TabsPage { 9 | 10 | constructor() {} 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/app/tabs/tabs.router.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { TabsPage } from './tabs.page'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: 'tabs', 8 | component: TabsPage, 9 | children: [ 10 | { 11 | path: 'movie', 12 | children: [ 13 | { 14 | path: '', 15 | loadChildren: () => import('../movie/movie.module').then((m) => m.MoviePageModule) 16 | } 17 | ] 18 | }, 19 | { 20 | path: 'episode', 21 | children: [ 22 | { 23 | path: '', 24 | loadChildren: () => import('../episode/episode.module').then((m) => m.EpisodePageModule) 25 | } 26 | ] 27 | }, 28 | { 29 | path: 'settings', 30 | children: [ 31 | { 32 | path: '', 33 | loadChildren: () => import('../settings/settings.module').then((m) => m.SettingsPageModule) 34 | } 35 | ] 36 | }, 37 | { 38 | path: '', 39 | redirectTo: '/tabs/movie', 40 | pathMatch: 'full' 41 | } 42 | ] 43 | }, 44 | { 45 | path: '', 46 | redirectTo: '/tabs/movie', 47 | pathMatch: 'full' 48 | } 49 | ]; 50 | 51 | @NgModule({ 52 | imports: [RouterModule.forChild(routes)], 53 | exports: [RouterModule] 54 | }) 55 | export class TabsPageRoutingModule {} 56 | -------------------------------------------------------------------------------- /src/assets/icon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wako-unofficial-addons/nomos/98c2dc864679bec9fca15b22eb8dd966fcfe927e/src/assets/icon/favicon.png -------------------------------------------------------------------------------- /src/assets/plugins/3rdpartylicenses.txt: -------------------------------------------------------------------------------- 1 | @ionic/storage-angular 2 | MIT 3 | 4 | rxjs 5 | Apache-2.0 6 | Apache License 7 | Version 2.0, January 2004 8 | http://www.apache.org/licenses/ 9 | 10 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 11 | 12 | 1. Definitions. 13 | 14 | "License" shall mean the terms and conditions for use, reproduction, 15 | and distribution as defined by Sections 1 through 9 of this document. 16 | 17 | "Licensor" shall mean the copyright owner or entity authorized by 18 | the copyright owner that is granting the License. 19 | 20 | "Legal Entity" shall mean the union of the acting entity and all 21 | other entities that control, are controlled by, or are under common 22 | control with that entity. For the purposes of this definition, 23 | "control" means (i) the power, direct or indirect, to cause the 24 | direction or management of such entity, whether by contract or 25 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 26 | outstanding shares, or (iii) beneficial ownership of such entity. 27 | 28 | "You" (or "Your") shall mean an individual or Legal Entity 29 | exercising permissions granted by this License. 30 | 31 | "Source" form shall mean the preferred form for making modifications, 32 | including but not limited to software source code, documentation 33 | source, and configuration files. 34 | 35 | "Object" form shall mean any form resulting from mechanical 36 | transformation or translation of a Source form, including but 37 | not limited to compiled object code, generated documentation, 38 | and conversions to other media types. 39 | 40 | "Work" shall mean the work of authorship, whether in Source or 41 | Object form, made available under the License, as indicated by a 42 | copyright notice that is included in or attached to the work 43 | (an example is provided in the Appendix below). 44 | 45 | "Derivative Works" shall mean any work, whether in Source or Object 46 | form, that is based on (or derived from) the Work and for which the 47 | editorial revisions, annotations, elaborations, or other modifications 48 | represent, as a whole, an original work of authorship. For the purposes 49 | of this License, Derivative Works shall not include works that remain 50 | separable from, or merely link (or bind by name) to the interfaces of, 51 | the Work and Derivative Works thereof. 52 | 53 | "Contribution" shall mean any work of authorship, including 54 | the original version of the Work and any modifications or additions 55 | to that Work or Derivative Works thereof, that is intentionally 56 | submitted to Licensor for inclusion in the Work by the copyright owner 57 | or by an individual or Legal Entity authorized to submit on behalf of 58 | the copyright owner. For the purposes of this definition, "submitted" 59 | means any form of electronic, verbal, or written communication sent 60 | to the Licensor or its representatives, including but not limited to 61 | communication on electronic mailing lists, source code control systems, 62 | and issue tracking systems that are managed by, or on behalf of, the 63 | Licensor for the purpose of discussing and improving the Work, but 64 | excluding communication that is conspicuously marked or otherwise 65 | designated in writing by the copyright owner as "Not a Contribution." 66 | 67 | "Contributor" shall mean Licensor and any individual or Legal Entity 68 | on behalf of whom a Contribution has been received by Licensor and 69 | subsequently incorporated within the Work. 70 | 71 | 2. Grant of Copyright License. Subject to the terms and conditions of 72 | this License, each Contributor hereby grants to You a perpetual, 73 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 74 | copyright license to reproduce, prepare Derivative Works of, 75 | publicly display, publicly perform, sublicense, and distribute the 76 | Work and such Derivative Works in Source or Object form. 77 | 78 | 3. Grant of Patent License. Subject to the terms and conditions of 79 | this License, each Contributor hereby grants to You a perpetual, 80 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 81 | (except as stated in this section) patent license to make, have made, 82 | use, offer to sell, sell, import, and otherwise transfer the Work, 83 | where such license applies only to those patent claims licensable 84 | by such Contributor that are necessarily infringed by their 85 | Contribution(s) alone or by combination of their Contribution(s) 86 | with the Work to which such Contribution(s) was submitted. If You 87 | institute patent litigation against any entity (including a 88 | cross-claim or counterclaim in a lawsuit) alleging that the Work 89 | or a Contribution incorporated within the Work constitutes direct 90 | or contributory patent infringement, then any patent licenses 91 | granted to You under this License for that Work shall terminate 92 | as of the date such litigation is filed. 93 | 94 | 4. Redistribution. You may reproduce and distribute copies of the 95 | Work or Derivative Works thereof in any medium, with or without 96 | modifications, and in Source or Object form, provided that You 97 | meet the following conditions: 98 | 99 | (a) You must give any other recipients of the Work or 100 | Derivative Works a copy of this License; and 101 | 102 | (b) You must cause any modified files to carry prominent notices 103 | stating that You changed the files; and 104 | 105 | (c) You must retain, in the Source form of any Derivative Works 106 | that You distribute, all copyright, patent, trademark, and 107 | attribution notices from the Source form of the Work, 108 | excluding those notices that do not pertain to any part of 109 | the Derivative Works; and 110 | 111 | (d) If the Work includes a "NOTICE" text file as part of its 112 | distribution, then any Derivative Works that You distribute must 113 | include a readable copy of the attribution notices contained 114 | within such NOTICE file, excluding those notices that do not 115 | pertain to any part of the Derivative Works, in at least one 116 | of the following places: within a NOTICE text file distributed 117 | as part of the Derivative Works; within the Source form or 118 | documentation, if provided along with the Derivative Works; or, 119 | within a display generated by the Derivative Works, if and 120 | wherever such third-party notices normally appear. The contents 121 | of the NOTICE file are for informational purposes only and 122 | do not modify the License. You may add Your own attribution 123 | notices within Derivative Works that You distribute, alongside 124 | or as an addendum to the NOTICE text from the Work, provided 125 | that such additional attribution notices cannot be construed 126 | as modifying the License. 127 | 128 | You may add Your own copyright statement to Your modifications and 129 | may provide additional or different license terms and conditions 130 | for use, reproduction, or distribution of Your modifications, or 131 | for any such Derivative Works as a whole, provided Your use, 132 | reproduction, and distribution of the Work otherwise complies with 133 | the conditions stated in this License. 134 | 135 | 5. Submission of Contributions. Unless You explicitly state otherwise, 136 | any Contribution intentionally submitted for inclusion in the Work 137 | by You to the Licensor shall be under the terms and conditions of 138 | this License, without any additional terms or conditions. 139 | Notwithstanding the above, nothing herein shall supersede or modify 140 | the terms of any separate license agreement you may have executed 141 | with Licensor regarding such Contributions. 142 | 143 | 6. Trademarks. This License does not grant permission to use the trade 144 | names, trademarks, service marks, or product names of the Licensor, 145 | except as required for reasonable and customary use in describing the 146 | origin of the Work and reproducing the content of the NOTICE file. 147 | 148 | 7. Disclaimer of Warranty. Unless required by applicable law or 149 | agreed to in writing, Licensor provides the Work (and each 150 | Contributor provides its Contributions) on an "AS IS" BASIS, 151 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 152 | implied, including, without limitation, any warranties or conditions 153 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 154 | PARTICULAR PURPOSE. You are solely responsible for determining the 155 | appropriateness of using or redistributing the Work and assume any 156 | risks associated with Your exercise of permissions under this License. 157 | 158 | 8. Limitation of Liability. In no event and under no legal theory, 159 | whether in tort (including negligence), contract, or otherwise, 160 | unless required by applicable law (such as deliberate and grossly 161 | negligent acts) or agreed to in writing, shall any Contributor be 162 | liable to You for damages, including any direct, indirect, special, 163 | incidental, or consequential damages of any character arising as a 164 | result of this License or out of the use or inability to use the 165 | Work (including but not limited to damages for loss of goodwill, 166 | work stoppage, computer failure or malfunction, or any and all 167 | other commercial damages or losses), even if such Contributor 168 | has been advised of the possibility of such damages. 169 | 170 | 9. Accepting Warranty or Additional Liability. While redistributing 171 | the Work or Derivative Works thereof, You may choose to offer, 172 | and charge a fee for, acceptance of support, warranty, indemnity, 173 | or other liability obligations and/or rights consistent with this 174 | License. However, in accepting such obligations, You may act only 175 | on Your own behalf and on Your sole responsibility, not on behalf 176 | of any other Contributor, and only if You agree to indemnify, 177 | defend, and hold each Contributor harmless for any liability 178 | incurred by, or claims asserted against, such Contributor by reason 179 | of your accepting any such warranty or additional liability. 180 | 181 | END OF TERMS AND CONDITIONS 182 | 183 | APPENDIX: How to apply the Apache License to your work. 184 | 185 | To apply the Apache License to your work, attach the following 186 | boilerplate notice, with the fields enclosed by brackets "[]" 187 | replaced with your own identifying information. (Don't include 188 | the brackets!) The text should be enclosed in the appropriate 189 | comment syntax for the file format. We also recommend that a 190 | file or class name and description of purpose be included on the 191 | same "printed page" as the copyright notice for easier 192 | identification within third-party archives. 193 | 194 | Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors 195 | 196 | Licensed under the Apache License, Version 2.0 (the "License"); 197 | you may not use this file except in compliance with the License. 198 | You may obtain a copy of the License at 199 | 200 | http://www.apache.org/licenses/LICENSE-2.0 201 | 202 | Unless required by applicable law or agreed to in writing, software 203 | distributed under the License is distributed on an "AS IS" BASIS, 204 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 205 | See the License for the specific language governing permissions and 206 | limitations under the License. 207 | -------------------------------------------------------------------------------- /src/assets/plugins/i18n/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wako-unofficial-addons/nomos/98c2dc864679bec9fca15b22eb8dd966fcfe927e/src/assets/plugins/i18n/.gitkeep -------------------------------------------------------------------------------- /src/assets/plugins/i18n/el.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": { 3 | "tuto": { 4 | "title": "Οδηγός εγκατάστασης", 5 | "skip": "Παράλειψη", 6 | "slide1": { 7 | "title": "Τί είναι το Helios", 8 | "description": "Το Helios είναι ένας ανιχνευτής πηγών, εξάγει δεδομένα από ιστοσελίδες τρίτων (που ονομάζονται πάροχοι) οι οποίες είναι δημοσίως προσβάσιμες στο διαδίκτυο", 9 | "titleDisclaimer": "Αποποίηση ευθυνών", 10 | "disclaimer": "Το Helios δεν φιλοξενεί, ανεβάζει ούτε διαχειρίζεται το οποιοδήποτε υλικό σε αυτές τις σελίδες και το πρόσθετο, οι συνεργάτες καί/ή δημιουργοί δεν εχουν έλεγχο πάνω στο υλικό αυτών τών σελίδων. Το Helios αποποιείται οποιασδήποτε ευθύνης περί πνευματικών δικαιωμάτων, νομιμότητας, ακρίβειας, συμβατότητας, ευπρέπειας ή άλλων χαρακτηριστικών του προβαλλόμενου περιεχομένου. Ο τελικός χρήστης είναι αποκλειστικά υπεύθυνος να αποφύγει οποιεσδήποτε παράνομες πράξεις σύμφωνα με την εθνική ή παγκόσμια νομοθεσία.

Το Helios δεν αναπτύσσεται ούτε εγκρίνεται από τους δημιουργούς του wako, για υποστήριξη δημιουργήστε μια αναφορά προβλήματος στο Github ή μπείτε στη συζήτηση της κοινότητας στο Reddit: https://www.reddit.com/r/Addons4Wako/", 11 | "accept": "Διάβασα τα παραπάνω και συμφωνώ" 12 | }, 13 | "slide2": { 14 | "title": "Προσθήκη παρόχων", 15 | "description": "Όπως προαναφέρθηκε, το Helios απαιτεί παρόχους για την εξεύρεση πηγών, ας προσθέσουμε λοιπόν κάποιους" 16 | }, 17 | "slide3": { 18 | "title": "Επιλογή ποιότητας", 19 | "description": "Επιλέξτε την μέγιστη ποιότητα που θέλετε να αναπαράγετε" 20 | }, 21 | "slide4": { 22 | "title": "Λογαριασμός Debrid", 23 | "description": "Αν θέλετε να αναπαράγετε πηγές χωρίς κολλήματα, συνιστάται να συνδέσετε το Helios με μια υπηρεσία debrid" 24 | }, 25 | "slide5": { 26 | "title": "Επιλογή προγράμματος αναπαραγωγής", 27 | "description": "Το Helios μπορεί να αναπαράγει τα μέσα των πηγών με διαφορά media players, επιλέξτε το προεπιλεγμένο" 28 | }, 29 | "slide6": { 30 | "title": "Είμαστε έτοιμοι", 31 | "description": "Είστε έτοιμοι, αν επιθυμείτε να αλλάξετε αυτές τις ρυθμίσεις, μεταβείτε στο Wako > Ρυθμίσεις > Πρόσθετα > Helios > Ρυθμίσεις", 32 | "button": "Κλείσιμο" 33 | } 34 | }, 35 | "settings": { 36 | "list": { 37 | "wizard": { 38 | "header": "Οδηγός εγκατάστασης", 39 | "item": "Εκκίνηση οδηγού εγκατάστασης" 40 | }, 41 | "provider": { 42 | "header": "Πάροχοι", 43 | "item": "Διαμόρφωση παρόχων" 44 | }, 45 | "debrid-account": { 46 | "header": "Λογαριασμοί Debrid", 47 | "item": "Διαμόρφωση λογαριασμών Debrid" 48 | }, 49 | "play-button": { 50 | "header": "Ενέργεια πλήκτρου αναπαραγωγής", 51 | "item": "Προεπιλεγμένη ενέργεια του πλήκτρου αναπαραγωγής" 52 | }, 53 | "actions": { 54 | "header": "Αλλαγή σειράς και διακόπτες ενεργειών" 55 | }, 56 | "openRemoteList": { 57 | "header": "Τηλεχειριστήριο", 58 | "item": "Άνοιγμα τηλεχειριστηρίου μετά την επιλογή" 59 | }, 60 | "playlist": { 61 | "header": "Λίστα αναπαραγωγής", 62 | "item": "Αυτόματη αναζήτηση επόμενων επεισοδίων και δημιουργία λίστας αναπαραγωγής" 63 | }, 64 | "fileSizeFilter": { 65 | "header": "Φίλτρο μεγέθους αρχείου", 66 | "enabled": "Ενεργό", 67 | "packageWarning": "Το φίλτρο δεν θα εφαρμοστεί σε πηγή πακέτου", 68 | "maxSize": "Μέγιστο μέγεθος (σε GB) - βάλτε την τιμή 0 για απενεργοποίηση", 69 | "minSize": "Ελάχιστο μέγεθος (σε GB)" 70 | } 71 | } 72 | }, 73 | "providers": { 74 | "title": "Πάροχοι", 75 | "list": { 76 | "header": "Εγκατάσταση", 77 | "providersUrl": "Διευθύνσεις URL παρόχων", 78 | "noUrl": "Πιέστε εδώ για προσθήκη", 79 | "addProvider": "Προσθήκη παρόχων" 80 | }, 81 | "enabledSource": { 82 | "header": "Ενεργοποιημένη πηγή", 83 | "howItWorks": "Για λόγους λειτουργικότητας, συνιστάται να ενεργοποιήσετε μόνο την βέλτιστη ποιότητα που επιθυμείτε. Αν βρεθούν λιγότερες από 20 πηγές για αυτήν την ποιότητα, τότε θα χρησιμοποιηθεί η επόμενη κατώτερη κατεβαίνοντας έως την κατώτατη αν χρειαστεί" 84 | }, 85 | "providerList": { 86 | "header": "Πάροχοι" 87 | } 88 | }, 89 | "debrid-account": { 90 | "list": { 91 | "title": "Λογαριασμοί Debrid", 92 | "logout": "Αποσύνδεση", 93 | "login": "Σύνδεση", 94 | "realDebrid": { 95 | "explain": "Το Real-Debrid είναι μία υπηρεσία μεταφόρτωσης αρχείων χωρίς περιορισμούς που σας επιτρέπει να κατεβάζετε αρχεία στο διαδίκτυο με μεγάλες ταχύτητες", 96 | "noAccount": "Δεν διαθέτετε λογαριασμό Real-Debrid; Πατήστε εδώ" 97 | }, 98 | "premiumize": { 99 | "explain": "Το Premiumize.me είναι μία υπηρεσία μεταφόρτωσης αρχείων χωρίς περιορισμούς που σας επιτρέπει να κατεβάζετε αρχεία στο διαδίκτυο με μεγάλες ταχύτητες.

Διαθέτει δωρεάν συνδρομή για δοκιμή (με όριο το ένα αρχείο μεγίστου μέγεθος 5 GB κάθε 120 λεπτά)", 100 | "preferTranscoded": "Prefer transcoded files", 101 | "noAccount": "Δεν διαθέτετε λογαριασμό Premiumize; Πατήστε εδώ" 102 | } 103 | } 104 | }, 105 | "support": { 106 | "list": { 107 | "header": "Υποστήριξη", 108 | "reddit": "Επίσημο Reddit", 109 | "discord": "Επίσημο Discord" 110 | } 111 | } 112 | }, 113 | "toasts": { 114 | "kodi": { 115 | "hostUnreachable": "Ο εξυπηρετητής Kodi {{hostName}} δεν είναι διαθέσιμος", 116 | "noHost": "Δεν έχει καταχωρηθεί εξυπηρετητής Kodi" 117 | }, 118 | "startOpening": "Άνοιγμα του {{title}} στο {{hostName}}", 119 | "providersUpdated": "Οι πάροχοι ενημερωθηκαν", 120 | "providers": { 121 | "providerUrlAdded": "Οι πάροχοι προστέθηκαν", 122 | "providerUrlFailedToAdd": "Αδυναμία προσθήκης παρόχων, παρακαλώ ελέγξτε την διεύθυνση URL" 123 | }, 124 | "cloud-account": { 125 | "needToAddProviders": "Απαιτείται προσθήκη παροχών για να απολαύσετε τις υπηρεσίες debrid." 126 | }, 127 | "premiumize": { 128 | "invalidApiKey": "Το κλειδί API δεν είναι έγκυρο" 129 | }, 130 | "real-debrid": { 131 | "failedToLogin": "Αδυναμία σύνδεσης" 132 | }, 133 | "copyToClipboard": "Το στοιχείο {{element}} αντιγράφηκε στο πρόχειρο", 134 | "open-source": { 135 | "cannotRetrieveUrl": "Αυτή η πηγή δεν μπορεί να αναπαραχθεί, αδυναμία λήψης της διεύθυνσης URL", 136 | "addedToPM": "Η πηγή προστέθηκε στο λογαριασμό PM σας", 137 | "failedToAddToPM": "Η προσθήκη της πηγής στο λογαριασμό PM σας απέτυχε: {{error}}", 138 | "addedToRD": "Η πηγή προστέθηκε στο λογαριασμό RD σας", 139 | "failedToAddToRD": "Η προσθήκη της πηγής στο λογαριασμό RD σας απέτυχε: {{error}}", 140 | "sourceNotCached": "Αποτυχία λήψης της διεύθυνσης URL του βίντεο", 141 | "permissionDenied": "Η άδεια απορρίφθηκε - Παρακαλώ ελέγξτε το λογαριασμό σας" 142 | }, 143 | "elementumNotInstalled": "Για να χρησιμοποιήσετε το πρόσθετο Elementum του Kodi, πρέπει να είναι εγκατεστημένο", 144 | "playlist": "Η λίστα {{playlistName}} δημιουργήθηκε/ενημερώθηκε. Περιέχει {{items}} στοιχεία" 145 | }, 146 | "actionSheets": { 147 | "open-source": { 148 | "openTitle": "Επιλέξτε μία ενέργεια", 149 | "options": { 150 | "open-kodi": "Αναπαραγωγή με Kodi", 151 | "open-browser": "Αναπαραγωγή στο πρόγραμμα περιήγησης ιστού", 152 | "copy-url": "Αντιγραφή διεύθυνσης URL", 153 | "share-url": "Κοινοποίηση διεύθυνσης URL", 154 | "open-vlc": "Αναπαραγωγή με VLC", 155 | "download-vlc": "Λήψη μέσω VLC", 156 | "open-elementum": "Αναπαραγωγή στο Kodi με το Elementum", 157 | "open-with": "Αναπαραγωγή με...", 158 | "open-nplayer": "Αναπαραγωγή με nPlayer", 159 | "open-infuse": "Αναπαραγωγή με Infuse", 160 | "add-to-pm": "Προσθήκη στο Premiumize.me", 161 | "add-to-rd": "Προσθήκη στο Real-Debrid", 162 | "add-to-playlist": "Προσθήκη στη λίστα αναπαραγωγής του wako", 163 | "letMeChoose": "Χειροκίνητη επιλογή" 164 | }, 165 | "selectLink": { 166 | "openTitle": "Η πηγή έχει πολλαπλούς συνδέσμους, ποιόν θέλετε να ανοίξετε;" 167 | } 168 | } 169 | }, 170 | "alerts": { 171 | "cancelButton": "Ακύρωση", 172 | "cloud-account": { 173 | "real-debrid": { 174 | "authHeader": "Ταυτοποίηση Real-Debrid", 175 | "enterCode": "Ανοίξτε τον σύνδεσμο: https://real-debrid.com/device

Εισάγετε τον παρακάτω κωδικό:", 176 | "openUrlButton": "Άνοιγμα συνδέσμου" 177 | } 178 | } 179 | }, 180 | "modals": { 181 | "search-manual": { 182 | "search": "Αναζήτηση πηγής", 183 | "category": "Κατηγορία", 184 | "categoryOptions": { 185 | "movies": "Ταινία", 186 | "tv": "Σειρά", 187 | "anime": "Anime" 188 | } 189 | }, 190 | "search-source": { 191 | "manual": "Χειροκίνητη αναζήτηση πηγών" 192 | } 193 | }, 194 | "source-list": { 195 | "noProviderSet": "Δεν έχουν καταχωρηθεί πάροχοι", 196 | "noProviderSetCategory": "Δεν έχουν καταχωρηθεί πάροχοι για την κατηγορία: {{category}}", 197 | "manualFilter": "Αναζήτηση στα αποτελέσματα" 198 | }, 199 | "open-button": { 200 | "more": "Λίστα πηγών" 201 | }, 202 | "sources": { 203 | "segments": { 204 | "debrid": "Ροές", 205 | "torrents": "Torrents", 206 | "stats": "Στατιστικά" 207 | }, 208 | "statsInfo": "Υποσημείωση: Τα πολλαπλά ίδια torrents αφαιρούνται από την λίστα torrents", 209 | "addToQueue": "Τα επόμενα {{totalEpisode}} επεισόδια μπήκαν στην ουρά αναπαραγωγής" 210 | }, 211 | "shared": { 212 | "noDebridAccountSet": "Δεν έχει καταχωρηθεί λογαριασμός Debrid", 213 | "clickToAddDebridAccount": "Πατήστε εδώ για προσθήκη υπηρεσίας debrid", 214 | "clickToAddProvider": "Πατήστε εδώ για να προσθέσετε κάποιον πάροχο", 215 | "noSourceFound": "Δεν βρέθηκε καμμία πηγή", 216 | "noBestSource": "Το Helios δεν εντόπισε την καλύτερη πηγή για εσάς, παρακαλώ διαλέξτε την χειροκίνητα", 217 | "cancel": "Ακύρωση", 218 | "enabled": "Ενεργό" 219 | } 220 | } -------------------------------------------------------------------------------- /src/assets/plugins/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": { 3 | "kodi-open-button": { 4 | "title": "Kodi Open Button", 5 | "list": { 6 | "header": "Setup", 7 | "pluginsUrl": "Plugins URL:", 8 | "noUrl": "Click here to add one" 9 | }, 10 | "openRemoteList": { 11 | "header": "Remote", 12 | "item": "Open remote after clicking" 13 | }, 14 | "pluginList": { 15 | "header": "Plugins" 16 | } 17 | } 18 | }, 19 | "toasts": { 20 | "kodi": { 21 | "open": "Open on {{hostName}} using {{pluginName}}", 22 | "hostUnreachable": "Ouch, your kodi host {{hostName}} is unreachable", 23 | "noHost": "No host set for kodi", 24 | "noSupportedPluginsInstalled": "Ouch, None of the supported plugins are installed, please install at least one of them", 25 | "failedToOpen": "Failed to open" 26 | }, 27 | "kodi-open-button": { 28 | "plugingUrlAdded": "Plugins added", 29 | "plugingUrlFailedToAdd": "Failed to add plugins, please check the given URL" 30 | }, 31 | "pluginsUpdated": "Plugins have been updated" 32 | }, 33 | "shared": { 34 | "buttonOpenKodi": "Open on Kodi" 35 | }, 36 | "actionSheets": { 37 | "kodi": { 38 | "openTitle": "Open with Kodi using..." 39 | } 40 | }, 41 | "alerts": { 42 | "okButton": "Ok", 43 | "cancelButton": "Cancel" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/assets/plugins/i18n/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": { 3 | "kodi-open-button": { 4 | "title": "Bouton Ouvrir", 5 | "list": { 6 | "header": "Configuration", 7 | "pluginsUrl": "Plugins URL:", 8 | "noUrl": "aucune" 9 | }, 10 | "openRemoteList": { 11 | "header": "Télécommande", 12 | "item": "Ouvrir la télécommande après avoir cliqué" 13 | }, 14 | "pluginList": { 15 | "header": "Plugins" 16 | } 17 | } 18 | }, 19 | "toasts": { 20 | "kodi": { 21 | "noSupportedPluginsInstalled": "Aïe, aucun des plugins supportés n'est installé, veuillez en installer au moins un.", 22 | "failedToOpen": "N'a pas réussi à ouvrir" 23 | }, 24 | "pluginsUpdated": "Les plugins ont été mis à jour" 25 | }, 26 | "shared": { 27 | "buttonOpenKodi": "Ouvrir avec Kodi" 28 | }, 29 | "actionSheets": { 30 | "kodi": { 31 | "openTitle": "Ouvrir avec Kodi via..." 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/assets/plugins/i18n/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": { 3 | "tuto": { 4 | "title": "Installazione Guidata", 5 | "skip": "Salta", 6 | "slide1": { 7 | "title": "Che cos'è Helios", 8 | "description": "Helios è un cercatore di fonti, utilizza siti di terze parti (chiamati provider) che sono disponibili pubblicamente su Internet.", 9 | "disclaimer": "Helios non ospita, carica o gestisce nessuno dei contenuti di questi siti e l'add-on, il personale e/o gli sviluppatori di Helios non hanno alcun controllo sui contenuti di questi siti di terze parti. Helios non si assume alcuna responsabilità per diritti d'autore, legalità, accuratezza, conformità, decenza o qualsiasi altro aspetto del contenuto visualizzato. È positivamente responsabilità dell'utente finale evitare qualsiasi azione che possa costituire una violazione delle leggi nazionali, federali o terrestri a loro applicabili.

Helios non è sviluppato né sostenuto da wako, per ricevere supporto crea un issue sul suo github o discuti con la comunità su reddit: https://www.reddit.com/r/Addons4Wako/" 10 | }, 11 | "slide2": { 12 | "title": "Aggiungi alcuni provider", 13 | "description": "Come detto in precedenza, Helios si affida ai provider per trovare le fonti, quindi aggiungiamone alcuni" 14 | }, 15 | "slide3": { 16 | "title": "Scegli la qualità", 17 | "description": "Seleziona la massima qualità che desideri riprodurre" 18 | }, 19 | "slide4": { 20 | "title": "Account Debrid", 21 | "description": "Se si desidera riprodurre fonti senza problemi di buffering, si consiglia di connettere Helios a un servizio debrid" 22 | }, 23 | "slide5": { 24 | "title": "Dove riprodurre?", 25 | "description": "Helios può riprodurre la fonte su diversi player, selezionare il player predefinito" 26 | }, 27 | "slide6": { 28 | "title": "Fatto tutto", 29 | "description": "Hai fatto, se vuoi cambiare queste impostazioni, vai su Wako > Impostazioni > Add-ons > Helios > Impostazioni Add-on", 30 | "button": "Chiudi" 31 | } 32 | }, 33 | "settings": { 34 | "list": { 35 | "wizard": { 36 | "header": "Installazione guidata", 37 | "item": "Esegui installazione guidata" 38 | }, 39 | "provider": { 40 | "header": "Providers", 41 | "item": "Configura i provider" 42 | }, 43 | "debrid-account": { 44 | "header": "Account Debrid", 45 | "item": "Configura gli account Debrid" 46 | }, 47 | "play-button": { 48 | "header": "Tasto Play", 49 | "item": "Azione predefinita tasto play" 50 | }, 51 | "actions": { 52 | "header": "Riordina/Seleziona Azioni" 53 | }, 54 | "openRemoteList": { 55 | "header": "Telecomando", 56 | "item": "Apri telecomando dopo aver cliccato" 57 | }, 58 | "playlist": { 59 | "header": "Playlist", 60 | "item": "Cerca automaticamente i prossimi episodi e crea una playlist" 61 | } 62 | } 63 | }, 64 | "providers": { 65 | "title": "Providers", 66 | "list": { 67 | "header": "Impostare", 68 | "providersUrl": "URL providers", 69 | "noUrl": "Clicca qui per aggiungerne uno", 70 | "addProvider": "Aggiungi providers" 71 | }, 72 | "enabledSource": { 73 | "header": "Abilita Fonte", 74 | "howItWorks": "Per motivi di prestazioni, si consiglia di abilitare solo la migliore qualità che si preferisce. Se vengono trovate meno di 20 fonti per questa qualità, verrà utilizzata la qualità inferiore fino a raggiungere la qualità più bassa" 75 | }, 76 | "providerList": { 77 | "header": "Providers" 78 | } 79 | }, 80 | "debrid-account": { 81 | "list": { 82 | "title": "Account Debrid", 83 | "logout": "Esci", 84 | "login": "Accedi", 85 | "realDebrid": { 86 | "explain": "RealDebrid è un downloader illimitato che consente di scaricare rapidamente file ospitati su Internet", 87 | "noAccount": "Nessun account RealDebrid? Clicca qui" 88 | }, 89 | "premiumize": { 90 | "explain": "Premiumize.me è un downloader illimitato (e altro) che ti consente di scaricare rapidamente file ospitati su Internet.

Inoltre Premiumize ti consente di testare il loro servizio gratuitamente (puoi riprodurre un file con una dimensione massima di 5 GB ogni 120 minuti)", 91 | "preferTranscoded": "Preferisci file transcodificati", 92 | "noAccount": "Nessun account Premiumize? Clicca qui" 93 | } 94 | } 95 | }, 96 | "support": { 97 | "list": { 98 | "header": "Supporto", 99 | "reddit": "Il nostro Reddit ufficiale", 100 | "discord": "Il nostro Discord ufficiale" 101 | } 102 | } 103 | }, 104 | "toasts": { 105 | "kodi": { 106 | "hostUnreachable": "Ahia! Il tuo host Kodi {{hostName}} non è raggiungibile", 107 | "noHost": "Nessun host impostato per Kodi" 108 | }, 109 | "startOpening": "Apertura di {{title}} su {{hostName}}", 110 | "providersUpdated": "I provider sono stati aggiornati", 111 | "providers": { 112 | "providerUrlAdded": "Provider aggiunti", 113 | "providerUrlFailedToAdd": "Impossibile aggiungere provider, controlla l'URL fornito" 114 | }, 115 | "cloud-account": { 116 | "needToAddProviders": "È necessario aggiungere alcuni provider per usufruire dei servizi debrid." 117 | }, 118 | "premiumize": { 119 | "invalidApiKey": "La API Key non è valida" 120 | }, 121 | "real-debrid": { 122 | "failedToLogin": "Accesso non riuscito" 123 | }, 124 | "copyToClipboard": "{{element}} è stato copiato negli appunti", 125 | "open-source": { 126 | "cannotRetrieveUrl": "Questa fonte non è riproducibile, non in grado di recuperare il suo URL", 127 | "addedToPM": "La fonte è stata aggiunta al tuo account PM", 128 | "failedToAddToPM": "Impossibile aggiungere la fonte al tuo account PM: {{error}}", 129 | "addedToRD": "La fonte è stata aggiunta al tuo account RD", 130 | "failedToAddToRD": "Impossibile aggiungere la fonte al tuo account RD: {{error}}", 131 | "sourceNotCached": "Impossibile recuperare l'URL del video", 132 | "permissionDenied": "Autorizzazione Negata - Controlla il tuo account" 133 | }, 134 | "elementumNotInstalled": "Per utilizzare l'add-on Kodi Elementum, è necessario installarlo su Kodi", 135 | "playlist": "La playlist {{playlistName}} è stata creata/aggiornata. Contiene {{items}} elementi" 136 | }, 137 | "actionSheets": { 138 | "open-source": { 139 | "openTitle": "Cosa fare?", 140 | "options": { 141 | "open-kodi": "Riproduci su Kodi", 142 | "open-browser": "Riproduci sul tuo browser", 143 | "copy-url": "Copia URL", 144 | "share-url": "Condividi URL", 145 | "open-vlc": "Riproduci su VLC", 146 | "download-vlc": "Scarica con VLC", 147 | "open-elementum": "Riproduci su Kodi via Elementum", 148 | "open-with": "Riproduci su...", 149 | "open-nplayer": "Riproduci su nPlayer", 150 | "add-to-pm": "Aggiungi a Premiumize.me", 151 | "add-to-rd": "Aggiungi a RealDebrid", 152 | "add-to-playlist": "Aggiungi alla playlist di wako", 153 | "letMeChoose": "Fammi scegliere" 154 | }, 155 | "selectLink": { 156 | "openTitle": "Questa fonte ha molteplici link, quale vuoi aprire?" 157 | } 158 | } 159 | }, 160 | "alerts": { 161 | "cancelButton": "Cancella", 162 | "cloud-account": { 163 | "real-debrid": { 164 | "authHeader": "Autenticazione Real Debrid", 165 | "enterCode": "Apri questo link: https://real-debrid.com/device

Inserisci questo codice:", 166 | "openUrlButton": "Apri il link" 167 | } 168 | } 169 | }, 170 | "modals": { 171 | "search-manual": { 172 | "search": "Cerca una fonte", 173 | "category": "Categoria", 174 | "categoryOptions": { 175 | "movies": "Film", 176 | "tv": "Programma TV", 177 | "anime": "Anime" 178 | } 179 | }, 180 | "search-source": { 181 | "manual": "Cerca Manualmente Fonti" 182 | } 183 | }, 184 | "source-list": { 185 | "noProviderSet": "Nessun provider impostato", 186 | "noProviderSetCategory": "Nessun provider impostato per la categoria: {{category}}", 187 | "manualFilter": "Cerca nei risultati" 188 | }, 189 | "open-button": { 190 | "more": "Elenco Fonti" 191 | }, 192 | "sources": { 193 | "segments": { 194 | "debrid": "Stream", 195 | "torrents": "Torrent", 196 | "stats": "Statistiche" 197 | }, 198 | "statsInfo": "Nota: I torrent duplicati vengono rimossi dall'elenco dei torrent", 199 | "addToQueue": "{{totalEpisode}} prossimi episodi sono stati messi in coda" 200 | }, 201 | "shared": { 202 | "noDebridAccountSet": "Nessun Account Debrid", 203 | "clickToAddDebridAccount": "Fai clic qui per aggiungere un servizio debrid", 204 | "clickToAddProvider": "Clicca qui per aggiungere un provider", 205 | "noSourceFound": "Nessuna fonte trovata", 206 | "noBestSource": "Helios non ha trovato la fonte migliore per te, per favore sceglila manualmente", 207 | "cancel": "Cancella", 208 | "enabled": "Abilitato" 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/assets/plugins/i18n/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": { 3 | "kodi-open-button": { 4 | "title": "Przycisk odtwórz w Kodi", 5 | "list": { 6 | "header": "Konfiguracja", 7 | "pluginsUrl": "Adres URL wtyczek:", 8 | "noUrl": "nic nie ustawiono" 9 | }, 10 | "openRemoteList": { 11 | "header": "Pilot", 12 | "item": "Otwórz pilota po kliknięciu" 13 | }, 14 | "pluginList": { 15 | "header": "Wtyczki" 16 | } 17 | } 18 | }, 19 | "toasts": { 20 | "kodi": { 21 | "noSupportedPluginsInstalled": "Ałć... Żadna ze wspieranych wtyczek nie jest zainstalowana, zainstaluj przynajmniej jedną ze wspieranych wtyczek.", 22 | "failedToOpen": "Błąd podczas otwierania" 23 | }, 24 | "pluginsUpdated": "Wtyczki zostały zaktualizowane" 25 | }, 26 | "shared": { 27 | "buttonOpenKodi": "Odtwórz na Kodi" 28 | }, 29 | "actionSheets": { 30 | "kodi": { 31 | "openTitle": "Odtwórz na Kodi używając..." 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/assets/plugins/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/@wako-app/mobile-sdk/dist/manifest-schema.json", 3 | "version": "1.1.4", 4 | "id": "plugin.nomos", 5 | "name": "Nomos", 6 | "author": "Davy", 7 | "description": "Allow to open Movie/TV Show on your favorite Kodi add-on", 8 | "actions": ["movies", "episodes", "episodes-item-option"], 9 | "entryPointV2": "https://raw.githubusercontent.com/wako-unofficial-addons/nomos/old-wako/dist/plugin.js", 10 | "entryPointV3": "/plugin.js", 11 | "languages": { 12 | "en": "https://raw.githubusercontent.com/wako-unofficial-addons/nomos/master/dist/i18n/en.json", 13 | "fr": "https://raw.githubusercontent.com/wako-unofficial-addons/nomos/master/dist/i18n/fr.json", 14 | "pl": "https://raw.githubusercontent.com/wako-unofficial-addons/nomos/master/dist/i18n/pl.json" 15 | }, 16 | "changeLogs": { 17 | "1.1.4": "Use latest SDK", 18 | "1.1.2": "Upgrade deps", 19 | "1.1.1": "Initial version" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/global.scss: -------------------------------------------------------------------------------- 1 | // http://ionicframework.com/docs/theming/ 2 | @import '~@ionic/angular/css/core.css'; 3 | @import '~@ionic/angular/css/normalize.css'; 4 | @import '~@ionic/angular/css/structure.css'; 5 | @import '~@ionic/angular/css/typography.css'; 6 | @import '~@ionic/angular/css/display.css'; 7 | @import '~@ionic/angular/css/padding.css'; 8 | @import '~@ionic/angular/css/float-elements.css'; 9 | @import '~@ionic/angular/css/text-alignment.css'; 10 | @import '~@ionic/angular/css/text-transformation.css'; 11 | @import '~@ionic/angular/css/flex-utils.css'; 12 | 13 | ion-spinner.center { 14 | position: fixed; 15 | display: flex; 16 | transform: translate(-50%, -50%); 17 | left: 50%; 18 | top: 50%; 19 | } 20 | 21 | ion-list-header { 22 | ion-spinner { 23 | width: 20px; 24 | height: 20px; 25 | float: right; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ionic App 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main-wako-like.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppWakoLikeModule } from './app/app.wako-like.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppWakoLikeModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | import './zone-flags'; 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by default for Angular itself. 59 | */ 60 | 61 | import 'zone.js'; // Included with Angular CLI. 62 | 63 | /*************************************************************************************************** 64 | * APPLICATION IMPORTS 65 | */ 66 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; 6 | 7 | declare const require: any; 8 | 9 | // First, initialize the Angular testing environment. 10 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); 11 | // Then we find all the tests. 12 | const context = require.context('./', true, /\.spec\.ts$/); 13 | // And load the modules. 14 | context.keys().map(context); 15 | -------------------------------------------------------------------------------- /src/theme/variables.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --ion-color-primary: #2c7cc0; 3 | --ion-color-primary-rgb: 44, 124, 192; 4 | --ion-color-primary-contrast: #ffffff; 5 | --ion-color-primary-contrast-rgb: 255, 255, 255; 6 | --ion-color-primary-shade: #276da9; 7 | --ion-color-primary-tint: #4189c6; 8 | 9 | --ion-color-secondary: #f15152; 10 | --ion-color-secondary-rgb: 241, 81, 82; 11 | --ion-color-secondary-contrast: #fff; 12 | --ion-color-secondary-contrast-rgb: 0, 0, 0; 13 | --ion-color-secondary-shade: #d44748; 14 | --ion-color-secondary-tint: #f26263; 15 | 16 | --ion-color-tertiary: #f4d8cd; 17 | --ion-color-tertiary-rgb: 244, 216, 205; 18 | --ion-color-tertiary-contrast: #000000; 19 | --ion-color-tertiary-contrast-rgb: 0, 0, 0; 20 | --ion-color-tertiary-shade: #d7beb4; 21 | --ion-color-tertiary-tint: #f5dcd2; 22 | 23 | --ion-color-success: #0f9c1d; 24 | --ion-color-success-rgb: 17, 177, 33; 25 | --ion-color-success-contrast: #ffffff; 26 | --ion-color-success-contrast-rgb: 255, 255, 255; 27 | --ion-color-success-shade: #0f9c1d; 28 | --ion-color-success-tint: #29b937; 29 | 30 | --ion-color-warning: #ffce5a; 31 | --ion-color-warning-rgb: 255, 206, 90; 32 | --ion-color-warning-contrast: #000000; 33 | --ion-color-warning-contrast-rgb: 0, 0, 0; 34 | --ion-color-warning-shade: #e0b54f; 35 | --ion-color-warning-tint: #ffd36b; 36 | 37 | --ion-color-danger: #f53d3d; 38 | --ion-color-danger-rgb: 245, 61, 61; 39 | --ion-color-danger-contrast: #ffffff; 40 | --ion-color-danger-contrast-rgb: 255, 255, 255; 41 | --ion-color-danger-shade: #d83636; 42 | --ion-color-danger-tint: #f65050; 43 | 44 | --ion-color-dark: #1f2d3f; 45 | --ion-color-dark-rgb: 31, 45, 63; 46 | --ion-color-dark-contrast: #ffffff; 47 | --ion-color-dark-contrast-rgb: 255, 255, 255; 48 | --ion-color-dark-shade: #1b2837; 49 | --ion-color-dark-tint: #293646; 50 | 51 | --ion-color-light: #d4d6d9; 52 | --ion-color-light-rgb: 212, 214, 217; 53 | --ion-color-light-contrast: #000000; 54 | --ion-color-light-contrast-rgb: 0, 0, 0; 55 | --ion-color-light-shade: #bbbcbf; 56 | --ion-color-light-tint: #d8dadd; 57 | 58 | --ion-color-medium: #d6d6d6; 59 | --ion-color-medium-rgb: 255, 255, 255; 60 | --ion-color-medium-contrast: #000000; 61 | --ion-color-medium-contrast-rgb: 0, 0, 0; 62 | --ion-color-medium-shade: #e0e0e0; 63 | --ion-color-medium-tint: #d6d6d6; 64 | } 65 | 66 | :root { 67 | --ion-background-color: var(--ion-color-dark); 68 | --ion-text-color: var(--ion-color-light); 69 | --ion-toolbar-color: var(--ion-color-light); 70 | } 71 | 72 | .ion-color-darkLight { 73 | --ion-color-base: #354252; 74 | --ion-color-base-rgb: 53, 66, 82; 75 | --ion-color-contrast: #ffffff; 76 | --ion-color-contrast-rgb: 255, 255, 255; 77 | --ion-color-shade: #2f3a48; 78 | --ion-color-tint: #495563; 79 | } 80 | 81 | .ion-color-white { 82 | --ion-color-base: #ffffff; 83 | --ion-color-base-rgb: 255, 255, 255; 84 | --ion-color-contrast: #000000; 85 | --ion-color-contrast-rgb: 0, 0, 0; 86 | --ion-color-shade: #e0e0e0; 87 | --ion-color-tint: #ffffff; 88 | } 89 | 90 | ion-toolbar { 91 | --background: var(--ion-color-dark); 92 | } 93 | 94 | ion-content { 95 | --color: var(--ion-color-light); 96 | } 97 | 98 | ion-button { 99 | --ripple-color: var(--ion-color-dark); 100 | --background-activated: var(--ion-color-dark); 101 | --background-focused: var(--ion-color-dark); 102 | } 103 | 104 | ion-range { 105 | --knob-background: var(--ion-color-light); 106 | } 107 | 108 | ion-card, 109 | ion-card-header, 110 | ion-card-title, 111 | ion-card-subtitle, 112 | ion-card-content, 113 | ion-card ion-list, 114 | ion-card ion-item { 115 | background-color: var(--ion-color-dark-tint) !important; 116 | color: var(--ion-color-light) !important; 117 | } 118 | 119 | ion-action-sheet { 120 | --color: var(--ion-color-light) !important; 121 | } 122 | 123 | .md ion-segment-button { 124 | color: var(--ion-color-light) !important; 125 | } 126 | 127 | ion-searchbar { 128 | color: var(--ion-color-light) !important; 129 | --background: var(--ion-color-dark-tint) !important; 130 | } 131 | 132 | ion-spinner { 133 | color: var(--ion-color-light) !important; 134 | } 135 | 136 | ion-card-title { 137 | color: var(--ion-color-light); 138 | } 139 | 140 | ion-card ion-list, 141 | ion-card ion-item { 142 | background-color: var(--ion-color-dark-tint) !important; 143 | --background: var(--ion-color-dark-tint) !important; 144 | } 145 | 146 | ion-list-header ion-label { 147 | text-transform: uppercase; 148 | } 149 | 150 | ion-item { 151 | --background: var(--ion-color-dark-tint) !important; 152 | --border-color: #565656 !important; 153 | 154 | ion-note { 155 | --color: var(--ion-color-light-shade) !important; 156 | } 157 | } 158 | 159 | ion-alert .alert-wrapper { 160 | background-color: var(--ion-color-dark-tint) !important; 161 | color: var(--ion-color-light) !important; 162 | } 163 | 164 | ion-alert .alert-radio-label, 165 | .alert-checkbox-label, 166 | ion-alert .alert-message { 167 | color: var(--ion-color-light) !important; 168 | } 169 | 170 | ion-modal { 171 | --width: 100% !important; 172 | --height: 100% !important; 173 | } 174 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | define: (name: string, deps: string[], definitionFn: () => any) => void; 3 | 4 | System: { 5 | import: (url) => Promise; 6 | instantiate: (id, url) => Promise 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/zone-flags.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Prevents Angular change detection from 3 | * running with certain Web Component callbacks 4 | */ 5 | (window as any).__Zone_disable_customElements = true; 6 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": ["src/main.ts", "src/polyfills.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.app.wako-like.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": ["src/main-wako-like.ts", "src/polyfills.ts"], 8 | "include": ["src/**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": false, 9 | "noImplicitOverride": false, 10 | "noPropertyAccessFromIndexSignature": false, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "es2020", 20 | "module": "es2020", 21 | "lib": ["es2020", "dom"] 22 | }, 23 | "angularCompilerOptions": { 24 | "enableI18nLegacyMessageIdFormat": false, 25 | "strictInjectionParameters": true, 26 | "strictInputAccessModifiers": true, 27 | "strictTemplates": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": ["jasmine"] 7 | }, 8 | "files": ["src/test.ts", "src/polyfills.ts"], 9 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 10 | } 11 | --------------------------------------------------------------------------------