├── .codecov.yml ├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── 1_bug_report.yml │ ├── 2_feature_request.yml │ └── config.yml ├── codeql │ └── codeql-config.yml ├── release_template.md └── workflows │ ├── codeql-analysis.yml │ ├── install-wine.sh │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── build ├── resources │ ├── appimage │ │ ├── build-docker.sh │ │ ├── build.sh │ │ └── get-dependencies.sh │ ├── archive │ │ ├── tar-gzip.sh │ │ └── zip.sh │ ├── icons │ │ ├── icon-1024.icns │ │ ├── icon-128.png │ │ ├── icon-16-32-48-256.ico │ │ ├── icon-16.png │ │ ├── icon-256.png │ │ ├── icon-32.png │ │ ├── icon-48.png │ │ ├── icon-64.png │ │ └── icon-scalable.svg │ ├── installer │ │ └── installer.nsi │ └── linux │ │ ├── add-menuitem.sh │ │ ├── remove-menuitem.sh │ │ └── streamlink-twitch-gui.appdata.xml └── tasks │ ├── common │ ├── cdp │ │ ├── connect.js │ │ ├── coverage.js │ │ ├── qunit.js │ │ └── retry.js │ ├── date.js │ ├── deploy │ │ └── github.js │ ├── github-api.js │ ├── hash.js │ ├── platforms.js │ ├── post-config.js │ └── release-changelog.js │ ├── configs │ ├── aliases.yml │ ├── appimage.js │ ├── checksum.js │ ├── clean.js │ ├── compile.js │ ├── compress.js │ ├── copy.js │ ├── coverage.js │ ├── deploy.js │ ├── dist.js │ ├── nwjs.js │ ├── run.js │ ├── runtest.js │ ├── shell.js │ ├── template.js │ └── webpack.js │ ├── custom │ ├── checksum.js │ ├── compile.js │ ├── deploy.js │ ├── dist.js │ ├── nwjs.js │ ├── release.js │ ├── run.js │ └── runtest.js │ └── webpack │ ├── babel.config.js │ ├── configurators │ ├── app.js │ ├── code-quality.js │ ├── dev.js │ ├── ember │ │ ├── app.js │ │ ├── compatibility-helpers.js │ │ ├── data.js │ │ ├── decorators.js │ │ ├── i18n.js │ │ ├── index.js │ │ ├── source.js │ │ └── tests.js │ ├── i18n.js │ ├── index.js │ ├── nwjs.js │ ├── release.js │ ├── resolve.js │ ├── stylesheets-and-assets.js │ └── tests.js │ ├── index.js │ ├── loaders │ ├── ember-app-loader │ │ ├── build.js │ │ ├── check-duplicates.js │ │ ├── get-files.js │ │ ├── get-module-exports.js │ │ ├── imports.js │ │ ├── index.js │ │ ├── module-unification.js │ │ └── parse.js │ ├── flag-icons-loader.js │ ├── hbs-loader.js │ ├── metadata-loader.js │ ├── optimized-json-loader.js │ ├── parse-json-loader.js │ ├── svgo-loader.js │ └── themes-loader.js │ ├── paths.js │ ├── plugins │ ├── babel-plugin-remove-imports.js │ ├── i18n-coverage.js │ └── nwjs.js │ ├── targets │ ├── coverage.js │ ├── debug.js │ ├── dev.js │ ├── i18n.js │ ├── index.js │ ├── prod.js │ ├── test.js │ └── testdev.js │ ├── utils.js │ └── webpack.config.js ├── package.json ├── src ├── app │ ├── app.js │ ├── assets │ │ ├── icons │ │ │ ├── icon-16.png │ │ │ ├── icon-16@2x.png │ │ │ ├── icon-16@3x.png │ │ │ ├── icon-256.png │ │ │ ├── icon-osx-18.png │ │ │ ├── icon-osx-18@2x.png │ │ │ └── icon-osx-18@3x.png │ │ └── images │ │ │ └── Twitch_Logo_Purple.png │ ├── config.js │ ├── data │ │ ├── models │ │ │ ├── -mixins │ │ │ │ ├── adapter.js │ │ │ │ └── polymorphic-fragment-serializer.js │ │ │ ├── application │ │ │ │ └── adapter.js │ │ │ ├── auth │ │ │ │ ├── adapter.js │ │ │ │ ├── model.js │ │ │ │ └── serializer.js │ │ │ ├── channel-settings │ │ │ │ ├── adapter.js │ │ │ │ ├── model.js │ │ │ │ └── serializer.js │ │ │ ├── github │ │ │ │ └── releases │ │ │ │ │ ├── adapter.js │ │ │ │ │ ├── model.js │ │ │ │ │ └── serializer.js │ │ │ ├── search │ │ │ │ ├── adapter.js │ │ │ │ ├── model.js │ │ │ │ └── serializer.js │ │ │ ├── settings │ │ │ │ ├── adapter.js │ │ │ │ ├── chat │ │ │ │ │ ├── fragment.js │ │ │ │ │ ├── provider │ │ │ │ │ │ ├── fragment.js │ │ │ │ │ │ └── serializer.js │ │ │ │ │ └── providers │ │ │ │ │ │ └── fragment.js │ │ │ │ ├── gui │ │ │ │ │ └── fragment.js │ │ │ │ ├── hotkeys │ │ │ │ │ ├── action │ │ │ │ │ │ └── fragment.js │ │ │ │ │ ├── fragment.js │ │ │ │ │ ├── hotkey │ │ │ │ │ │ └── fragment.js │ │ │ │ │ └── namespace │ │ │ │ │ │ ├── fragment.js │ │ │ │ │ │ └── serializer.js │ │ │ │ ├── model.js │ │ │ │ ├── notification │ │ │ │ │ └── fragment.js │ │ │ │ ├── serializer.js │ │ │ │ ├── streaming │ │ │ │ │ ├── fragment.js │ │ │ │ │ ├── player │ │ │ │ │ │ ├── fragment.js │ │ │ │ │ │ └── serializer.js │ │ │ │ │ ├── players │ │ │ │ │ │ └── fragment.js │ │ │ │ │ ├── provider │ │ │ │ │ │ └── fragment.js │ │ │ │ │ ├── providers │ │ │ │ │ │ └── fragment.js │ │ │ │ │ ├── qualities │ │ │ │ │ │ └── fragment.js │ │ │ │ │ └── quality │ │ │ │ │ │ └── fragment.js │ │ │ │ └── streams │ │ │ │ │ ├── fragment.js │ │ │ │ │ └── languages │ │ │ │ │ └── fragment.js │ │ │ ├── stream │ │ │ │ ├── -qualities.js │ │ │ │ ├── adapter.js │ │ │ │ └── model.js │ │ │ ├── twitch │ │ │ │ ├── adapter.js │ │ │ │ ├── channel │ │ │ │ │ ├── adapter.js │ │ │ │ │ ├── model.js │ │ │ │ │ └── serializer.js │ │ │ │ ├── channels-followed │ │ │ │ │ ├── adapter.js │ │ │ │ │ ├── model.js │ │ │ │ │ └── serializer.js │ │ │ │ ├── game-top │ │ │ │ │ ├── model.js │ │ │ │ │ └── serializer.js │ │ │ │ ├── game │ │ │ │ │ ├── adapter.js │ │ │ │ │ ├── model.js │ │ │ │ │ └── serializer.js │ │ │ │ ├── search-channel │ │ │ │ │ ├── model.js │ │ │ │ │ └── serializer.js │ │ │ │ ├── search-game │ │ │ │ │ ├── model.js │ │ │ │ │ └── serializer.js │ │ │ │ ├── serializer.js │ │ │ │ ├── stream-followed │ │ │ │ │ ├── model.js │ │ │ │ │ └── serializer.js │ │ │ │ ├── stream │ │ │ │ │ ├── adapter.js │ │ │ │ │ ├── model.js │ │ │ │ │ └── serializer.js │ │ │ │ ├── team │ │ │ │ │ ├── adapter.js │ │ │ │ │ ├── model.js │ │ │ │ │ └── serializer.js │ │ │ │ └── user │ │ │ │ │ ├── adapter.js │ │ │ │ │ ├── model.js │ │ │ │ │ └── serializer.js │ │ │ ├── versioncheck │ │ │ │ ├── adapter.js │ │ │ │ ├── model.js │ │ │ │ └── serializer.js │ │ │ └── window │ │ │ │ ├── adapter.js │ │ │ │ ├── model.js │ │ │ │ └── serializer.js │ │ └── transforms │ │ │ └── twitch │ │ │ └── image.js │ ├── index.html │ ├── init │ │ ├── initializers │ │ │ ├── ember-data.js │ │ │ ├── env.js │ │ │ ├── keyboard-layout-map.js │ │ │ ├── localstorage │ │ │ │ ├── channelsettings.js │ │ │ │ ├── initializer.js │ │ │ │ ├── localstorage.js │ │ │ │ ├── namespaces.js │ │ │ │ ├── search.js │ │ │ │ ├── settings.js │ │ │ │ └── utils.js │ │ │ ├── model-fragments.js │ │ │ ├── nwjs.js │ │ │ ├── settings-chat-provider.js │ │ │ ├── settings-hotkeys-namespace.js │ │ │ ├── settings-streaming-player.js │ │ │ └── store.js │ │ └── instance-initializers │ │ │ ├── application.js │ │ │ ├── boolean-transform.js │ │ │ ├── intl.js │ │ │ ├── nwjs │ │ │ ├── instance-initializer.js │ │ │ ├── integrations.js │ │ │ ├── parameters.js │ │ │ └── window.js │ │ │ └── routing.js │ ├── locales │ │ ├── de │ │ │ ├── components.yml │ │ │ ├── contextmenu.yml │ │ │ ├── helpers.yml │ │ │ ├── hotkeys.yml │ │ │ ├── languages.yml │ │ │ ├── modal.yml │ │ │ ├── models.yml │ │ │ ├── qualities.yml │ │ │ ├── routes.yml │ │ │ ├── services.yml │ │ │ ├── settings.yml │ │ │ └── themes.yml │ │ ├── en │ │ │ ├── components.yml │ │ │ ├── contextmenu.yml │ │ │ ├── helpers.yml │ │ │ ├── hotkeys.yml │ │ │ ├── languages.yml │ │ │ ├── modal.yml │ │ │ ├── models.yml │ │ │ ├── qualities.yml │ │ │ ├── routes.yml │ │ │ ├── services.yml │ │ │ ├── settings.yml │ │ │ └── themes.yml │ │ ├── es │ │ │ ├── components.yml │ │ │ ├── contextmenu.yml │ │ │ ├── helpers.yml │ │ │ ├── hotkeys.yml │ │ │ ├── languages.yml │ │ │ ├── modal.yml │ │ │ ├── models.yml │ │ │ ├── qualities.yml │ │ │ ├── routes.yml │ │ │ ├── services.yml │ │ │ ├── settings.yml │ │ │ └── themes.yml │ │ ├── fr │ │ │ ├── components.yml │ │ │ ├── contextmenu.yml │ │ │ ├── helpers.yml │ │ │ ├── hotkeys.yml │ │ │ ├── languages.yml │ │ │ ├── modal.yml │ │ │ ├── models.yml │ │ │ ├── qualities.yml │ │ │ ├── routes.yml │ │ │ ├── services.yml │ │ │ ├── settings.yml │ │ │ └── themes.yml │ │ ├── it │ │ │ ├── components.yml │ │ │ ├── contextmenu.yml │ │ │ ├── helpers.yml │ │ │ ├── hotkeys.yml │ │ │ ├── languages.yml │ │ │ ├── modal.yml │ │ │ ├── models.yml │ │ │ ├── qualities.yml │ │ │ ├── routes.yml │ │ │ ├── services.yml │ │ │ ├── settings.yml │ │ │ └── themes.yml │ │ ├── ja │ │ │ ├── components.yml │ │ │ ├── contextmenu.yml │ │ │ ├── helpers.yml │ │ │ ├── hotkeys.yml │ │ │ ├── languages.yml │ │ │ ├── modal.yml │ │ │ ├── models.yml │ │ │ ├── qualities.yml │ │ │ ├── routes.yml │ │ │ ├── services.yml │ │ │ ├── settings.yml │ │ │ └── themes.yml │ │ ├── pt-br │ │ │ ├── components.yml │ │ │ ├── contextmenu.yml │ │ │ ├── helpers.yml │ │ │ ├── hotkeys.yml │ │ │ ├── languages.yml │ │ │ ├── modal.yml │ │ │ ├── models.yml │ │ │ ├── qualities.yml │ │ │ ├── routes.yml │ │ │ ├── services.yml │ │ │ ├── settings.yml │ │ │ └── themes.yml │ │ ├── ru │ │ │ ├── components.yml │ │ │ ├── contextmenu.yml │ │ │ ├── helpers.yml │ │ │ ├── hotkeys.yml │ │ │ ├── languages.yml │ │ │ ├── modal.yml │ │ │ ├── models.yml │ │ │ ├── qualities.yml │ │ │ ├── routes.yml │ │ │ ├── services.yml │ │ │ ├── settings.yml │ │ │ └── themes.yml │ │ ├── zh-cn │ │ │ ├── components.yml │ │ │ ├── contextmenu.yml │ │ │ ├── helpers.yml │ │ │ ├── hotkeys.yml │ │ │ ├── languages.yml │ │ │ ├── modal.yml │ │ │ ├── models.yml │ │ │ ├── qualities.yml │ │ │ ├── routes.yml │ │ │ ├── services.yml │ │ │ ├── settings.yml │ │ │ └── themes.yml │ │ └── zh-tw │ │ │ ├── components.yml │ │ │ ├── contextmenu.yml │ │ │ ├── helpers.yml │ │ │ ├── hotkeys.yml │ │ │ ├── languages.yml │ │ │ ├── modal.yml │ │ │ ├── models.yml │ │ │ ├── qualities.yml │ │ │ ├── routes.yml │ │ │ ├── services.yml │ │ │ ├── settings.yml │ │ │ └── themes.yml │ ├── logger.js │ ├── main.js │ ├── nwjs │ │ ├── App.js │ │ ├── Menu.js │ │ ├── Menubar.js │ │ ├── Screen.js │ │ ├── Tray.js │ │ ├── Window │ │ │ ├── index.js │ │ │ └── reset.js │ │ ├── argv.js │ │ ├── debug.js │ │ ├── nwGui.js │ │ └── process.js │ ├── package.json │ ├── router.js │ ├── services │ │ ├── auth │ │ │ ├── oauth-redirect.html │ │ │ └── service.js │ │ ├── chat │ │ │ ├── launch.js │ │ │ ├── logger.js │ │ │ ├── providers │ │ │ │ ├── -basic.js │ │ │ │ ├── -java.js │ │ │ │ ├── -provider.js │ │ │ │ ├── browser.js │ │ │ │ ├── chatterino.js │ │ │ │ ├── chatty-standalone.js │ │ │ │ ├── chatty.js │ │ │ │ ├── chrome.js │ │ │ │ ├── chromium.js │ │ │ │ ├── custom.js │ │ │ │ └── index.js │ │ │ └── service.js │ │ ├── hotkey.js │ │ ├── intl.js │ │ ├── modal.js │ │ ├── notification │ │ │ ├── badge.js │ │ │ ├── cache │ │ │ │ ├── index.js │ │ │ │ └── item.js │ │ │ ├── data.js │ │ │ ├── dispatch.js │ │ │ ├── icons.js │ │ │ ├── logger.js │ │ │ ├── polling.js │ │ │ ├── provider.js │ │ │ ├── providers │ │ │ │ ├── auto.js │ │ │ │ ├── chrome-notifications.js │ │ │ │ ├── growl.js │ │ │ │ ├── index.js │ │ │ │ ├── native.js │ │ │ │ ├── rich.js │ │ │ │ └── snoretoast.js │ │ │ ├── service.js │ │ │ └── tray.js │ │ ├── nwjs.js │ │ ├── settings.js │ │ ├── streaming │ │ │ ├── cache │ │ │ │ ├── cache.js │ │ │ │ ├── index.js │ │ │ │ └── item.js │ │ │ ├── errors.js │ │ │ ├── exec-obj.js │ │ │ ├── is-aborted.js │ │ │ ├── launch │ │ │ │ ├── index.js │ │ │ │ └── parse-error.js │ │ │ ├── logger.js │ │ │ ├── player │ │ │ │ ├── resolve.js │ │ │ │ └── substitutions.js │ │ │ ├── provider │ │ │ │ ├── find-pythonscript-interpreter.js │ │ │ │ ├── parameters.js │ │ │ │ ├── resolve.js │ │ │ │ └── validate.js │ │ │ ├── service.js │ │ │ └── spawn.js │ │ ├── theme.js │ │ └── versioncheck.js │ ├── ui │ │ ├── components │ │ │ ├── -mixins │ │ │ │ └── hotkey.js │ │ │ ├── button │ │ │ │ ├── channel-button │ │ │ │ │ ├── component.js │ │ │ │ │ └── styles.less │ │ │ │ ├── form-button │ │ │ │ │ ├── component.js │ │ │ │ │ ├── styles.less │ │ │ │ │ └── template.hbs │ │ │ │ ├── open-chat │ │ │ │ │ └── component.js │ │ │ │ ├── share-channel │ │ │ │ │ └── component.js │ │ │ │ └── twitch-emotes │ │ │ │ │ └── component.js │ │ │ ├── flag-icon │ │ │ │ ├── component.js │ │ │ │ └── styles.less │ │ │ ├── form │ │ │ │ ├── -input-btn │ │ │ │ │ ├── component.js │ │ │ │ │ ├── styles.less │ │ │ │ │ └── template.hbs │ │ │ │ ├── -selectable │ │ │ │ │ └── component.js │ │ │ │ ├── check-box │ │ │ │ │ └── component.js │ │ │ │ ├── drop-down-list │ │ │ │ │ ├── component.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── drop-down-selection │ │ │ │ │ ├── component.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── drop-down │ │ │ │ │ ├── component.js │ │ │ │ │ ├── styles.less │ │ │ │ │ └── template.hbs │ │ │ │ ├── file-select │ │ │ │ │ ├── component.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── number-field │ │ │ │ │ ├── component.js │ │ │ │ │ ├── styles.less │ │ │ │ │ └── template.hbs │ │ │ │ ├── radio-buttons-item │ │ │ │ │ └── component.js │ │ │ │ ├── radio-buttons │ │ │ │ │ ├── component.js │ │ │ │ │ ├── styles.less │ │ │ │ │ └── template.hbs │ │ │ │ └── text-field │ │ │ │ │ └── component.js │ │ │ ├── helper │ │ │ │ ├── -from-now.js │ │ │ │ ├── bool-and.js │ │ │ │ ├── bool-not.js │ │ │ │ ├── bool-or.js │ │ │ │ ├── find-by.js │ │ │ │ ├── format-viewers.js │ │ │ │ ├── get-index.js │ │ │ │ ├── get-param.js │ │ │ │ ├── hotkey-title.js │ │ │ │ ├── hours-from-now.js │ │ │ │ ├── is-equal.js │ │ │ │ ├── is-gt.js │ │ │ │ ├── is-gte.js │ │ │ │ ├── is-null.js │ │ │ │ ├── math-add.js │ │ │ │ ├── math-div.js │ │ │ │ ├── math-mul.js │ │ │ │ ├── math-sub.js │ │ │ │ ├── t.js │ │ │ │ └── time-from-now.js │ │ │ ├── link │ │ │ │ ├── documentation-link │ │ │ │ │ ├── component.js │ │ │ │ │ ├── styles.less │ │ │ │ │ └── template.hbs │ │ │ │ ├── embedded-html-links │ │ │ │ │ └── component.js │ │ │ │ ├── embedded-links │ │ │ │ │ ├── component.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── external-link │ │ │ │ │ └── component.js │ │ │ │ └── link-to │ │ │ │ │ └── component.js │ │ │ ├── list │ │ │ │ ├── -list-item │ │ │ │ │ └── component.js │ │ │ │ ├── channel-item │ │ │ │ │ ├── component.js │ │ │ │ │ ├── styles.less │ │ │ │ │ └── template.hbs │ │ │ │ ├── content-list │ │ │ │ │ ├── component.js │ │ │ │ │ ├── styles.less │ │ │ │ │ └── template.hbs │ │ │ │ ├── game-item │ │ │ │ │ ├── component.js │ │ │ │ │ ├── styles.less │ │ │ │ │ └── template.hbs │ │ │ │ ├── headline-totals │ │ │ │ │ ├── component.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── infinite-scroll │ │ │ │ │ ├── component.js │ │ │ │ │ ├── styles.less │ │ │ │ │ └── template.hbs │ │ │ │ ├── settings-channel-item │ │ │ │ │ ├── component.js │ │ │ │ │ ├── styles.less │ │ │ │ │ └── template.hbs │ │ │ │ ├── stream-item │ │ │ │ │ ├── component.js │ │ │ │ │ ├── styles.less │ │ │ │ │ └── template.hbs │ │ │ │ └── team-item │ │ │ │ │ ├── component.js │ │ │ │ │ ├── styles.less │ │ │ │ │ └── template.hbs │ │ │ ├── loading-spinner │ │ │ │ ├── component.js │ │ │ │ ├── styles.less │ │ │ │ └── template.hbs │ │ │ ├── main-menu │ │ │ │ ├── component.js │ │ │ │ ├── styles.less │ │ │ │ └── template.hbs │ │ │ ├── modal │ │ │ │ ├── modal-body │ │ │ │ │ └── component.js │ │ │ │ ├── modal-changelog │ │ │ │ │ ├── component.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── modal-confirm │ │ │ │ │ ├── component.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── modal-debug │ │ │ │ │ ├── component.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── modal-dialog │ │ │ │ │ ├── component.js │ │ │ │ │ ├── styles.less │ │ │ │ │ └── template.hbs │ │ │ │ ├── modal-firstrun │ │ │ │ │ ├── component.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── modal-footer │ │ │ │ │ └── component.js │ │ │ │ ├── modal-header │ │ │ │ │ └── component.js │ │ │ │ ├── modal-log │ │ │ │ │ ├── component.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── modal-newrelease │ │ │ │ │ ├── component.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── modal-quit │ │ │ │ │ ├── component.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── modal-service │ │ │ │ │ ├── component.js │ │ │ │ │ ├── styles.less │ │ │ │ │ └── template.hbs │ │ │ │ └── modal-streaming │ │ │ │ │ ├── component.js │ │ │ │ │ ├── styles.less │ │ │ │ │ └── template.hbs │ │ │ ├── preview-image │ │ │ │ ├── component.js │ │ │ │ ├── styles.less │ │ │ │ └── template.hbs │ │ │ ├── quick │ │ │ │ ├── quick-bar-homepage │ │ │ │ │ └── component.js │ │ │ │ └── quick-bar │ │ │ │ │ ├── component.js │ │ │ │ │ ├── styles.less │ │ │ │ │ └── template.hbs │ │ │ ├── search-bar │ │ │ │ ├── component.js │ │ │ │ ├── styles.less │ │ │ │ └── template.hbs │ │ │ ├── selectable-text │ │ │ │ └── component.js │ │ │ ├── settings-hotkey │ │ │ │ ├── component.js │ │ │ │ ├── styles.less │ │ │ │ └── template.hbs │ │ │ ├── settings-row │ │ │ │ ├── component.js │ │ │ │ ├── styles.less │ │ │ │ └── template.hbs │ │ │ ├── settings-submit │ │ │ │ ├── component.js │ │ │ │ ├── styles.less │ │ │ │ └── template.hbs │ │ │ ├── stream │ │ │ │ ├── stats-row │ │ │ │ │ ├── component.js │ │ │ │ │ ├── styles.less │ │ │ │ │ └── template.hbs │ │ │ │ ├── stream-presentation │ │ │ │ │ ├── component.js │ │ │ │ │ ├── styles.less │ │ │ │ │ └── template.hbs │ │ │ │ └── stream-preview-image │ │ │ │ │ ├── component.js │ │ │ │ │ └── template.hbs │ │ │ ├── sub-menu │ │ │ │ ├── component.js │ │ │ │ ├── styles.less │ │ │ │ └── template.hbs │ │ │ └── title-bar │ │ │ │ ├── component.js │ │ │ │ ├── styles.less │ │ │ │ └── template.hbs │ │ ├── routes │ │ │ ├── -mixins │ │ │ │ ├── controllers │ │ │ │ │ └── retry-transition.js │ │ │ │ └── routes │ │ │ │ │ ├── filter-languages.js │ │ │ │ │ ├── infinite-scroll │ │ │ │ │ ├── common.js │ │ │ │ │ ├── css.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── pagination.js │ │ │ │ │ └── record-array.js │ │ │ │ │ └── refresh.js │ │ │ ├── about │ │ │ │ ├── controller.js │ │ │ │ ├── styles.less │ │ │ │ └── template.hbs │ │ │ ├── application │ │ │ │ ├── controller.js │ │ │ │ ├── route.js │ │ │ │ ├── styles │ │ │ │ │ ├── animations.less │ │ │ │ │ ├── content.less │ │ │ │ │ ├── fixes.less │ │ │ │ │ ├── fontawesome.less │ │ │ │ │ ├── fonts.less │ │ │ │ │ ├── form.less │ │ │ │ │ ├── index.less │ │ │ │ │ ├── main.less │ │ │ │ │ └── scrollbars.less │ │ │ │ └── template.hbs │ │ │ ├── channel │ │ │ │ ├── controller.js │ │ │ │ ├── index │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── loading │ │ │ │ │ └── route.js │ │ │ │ ├── route.js │ │ │ │ ├── settings │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── styles.less │ │ │ │ ├── teams │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ └── template.hbs │ │ │ ├── error │ │ │ │ ├── controller.js │ │ │ │ ├── route.js │ │ │ │ ├── styles.less │ │ │ │ └── template.hbs │ │ │ ├── games │ │ │ │ ├── game │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── index │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ └── loading │ │ │ │ │ └── route.js │ │ │ ├── index │ │ │ │ └── route.js │ │ │ ├── loading │ │ │ │ ├── controller.js │ │ │ │ ├── route.js │ │ │ │ ├── styles.less │ │ │ │ └── template.hbs │ │ │ ├── search │ │ │ │ ├── controller.js │ │ │ │ ├── route.js │ │ │ │ ├── styles.less │ │ │ │ └── template.hbs │ │ │ ├── settings │ │ │ │ ├── -submenu │ │ │ │ │ └── route.js │ │ │ │ ├── channels │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── chat │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── controller.js │ │ │ │ ├── gui │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── hotkeys │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── index │ │ │ │ │ └── route.js │ │ │ │ ├── languages │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── main │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── notifications │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── player │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── route.js │ │ │ │ ├── streaming │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── streams │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── styles.less │ │ │ │ └── template.hbs │ │ │ ├── streams │ │ │ │ ├── route.js │ │ │ │ └── template.hbs │ │ │ ├── team │ │ │ │ ├── controller.js │ │ │ │ ├── index │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── info │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── loading │ │ │ │ │ └── route.js │ │ │ │ ├── members │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── route.js │ │ │ │ ├── styles.less │ │ │ │ └── template.hbs │ │ │ ├── user │ │ │ │ ├── auth │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── controller.js │ │ │ │ ├── followed-channels │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── followed-streams │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── index │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── loading │ │ │ │ │ └── route.js │ │ │ │ └── styles.less │ │ │ └── watching │ │ │ │ ├── controller.js │ │ │ │ ├── route.js │ │ │ │ ├── styles.less │ │ │ │ └── template.hbs │ │ └── styles │ │ │ ├── config.less │ │ │ ├── mixins.less │ │ │ ├── themes.less │ │ │ └── themes │ │ │ ├── dark.less │ │ │ └── light.less │ └── utils │ │ ├── Logger.js │ │ ├── StreamOutputBuffer.js │ │ ├── ember │ │ └── ObjectBuffer.js │ │ ├── getStreamFromUrl.js │ │ ├── is-focused.js │ │ ├── linkparser.js │ │ ├── node │ │ ├── child_process │ │ │ ├── promise.js │ │ │ └── spawn.js │ │ ├── env-path.js │ │ ├── fs │ │ │ ├── clearfolder.js │ │ │ ├── download.js │ │ │ ├── mkdirp.js │ │ │ ├── readLines.js │ │ │ ├── stat.js │ │ │ ├── which.js │ │ │ └── whichFallback.js │ │ ├── http │ │ │ ├── HttpServer.js │ │ │ └── getRedirected.js │ │ ├── onShutdown.js │ │ ├── platform.js │ │ └── resolvePath.js │ │ ├── parameters │ │ ├── Parameter.js │ │ ├── ParameterCustom.js │ │ └── Substitution.js │ │ ├── preload.js │ │ ├── system-locale.js │ │ └── wait.js ├── config │ ├── chat.json │ ├── files.json │ ├── hotkeys.json │ ├── langs.yml │ ├── locales.json │ ├── log.json │ ├── main.json │ ├── notification.json │ ├── players.json │ ├── streaming.json │ ├── themes.json │ ├── twitch.json │ ├── update.json │ └── vars.json ├── test │ ├── dev.css │ ├── fixtures │ │ ├── data │ │ │ └── models │ │ │ │ ├── github │ │ │ │ └── releases.json │ │ │ │ └── twitch │ │ │ │ ├── adapter │ │ │ │ ├── coalesce-stream-multiple.yml │ │ │ │ ├── coalesce-stream-single.yml │ │ │ │ ├── coalesce-various-multiple.yml │ │ │ │ └── coalesce-various-single.yml │ │ │ │ ├── channel.yml │ │ │ │ ├── channels-followed.yml │ │ │ │ ├── game-top.yml │ │ │ │ ├── game.yml │ │ │ │ ├── search-channel.yml │ │ │ │ ├── search-game.yml │ │ │ │ ├── serializer │ │ │ │ ├── metadata.yml │ │ │ │ ├── response.yml │ │ │ │ └── single-response.yml │ │ │ │ ├── stream-followed.yml │ │ │ │ ├── stream.yml │ │ │ │ ├── team.yml │ │ │ │ └── user.yml │ │ ├── services │ │ │ ├── auth │ │ │ │ └── validate-session.yml │ │ │ └── notification │ │ │ │ └── polling.yml │ │ └── ui │ │ │ └── routes │ │ │ ├── games │ │ │ ├── game.yml │ │ │ └── index.yml │ │ │ └── user │ │ │ └── followed-channels.yml │ ├── index.html │ ├── main-coverage.js │ ├── main-dev.js │ ├── main.js │ ├── package.json │ ├── tests │ │ ├── data │ │ │ └── models │ │ │ │ ├── settings │ │ │ │ ├── hotkeys.js │ │ │ │ ├── model.js │ │ │ │ └── streams.js │ │ │ │ └── twitch │ │ │ │ ├── adapter.js │ │ │ │ ├── channel.js │ │ │ │ ├── channels-followed.js │ │ │ │ ├── game-top.js │ │ │ │ ├── game.js │ │ │ │ ├── search-channel.js │ │ │ │ ├── search-game.js │ │ │ │ ├── serializer.js │ │ │ │ ├── stream-followed.js │ │ │ │ ├── stream.js │ │ │ │ ├── team.js │ │ │ │ └── user.js │ │ ├── index.js │ │ ├── init │ │ │ ├── initializers │ │ │ │ ├── keyboard-layout-map.js │ │ │ │ ├── localstorage │ │ │ │ │ ├── channelsettings.js │ │ │ │ │ ├── initializer.js │ │ │ │ │ ├── namespaces.js │ │ │ │ │ ├── search.js │ │ │ │ │ └── settings.js │ │ │ │ └── store.js │ │ │ └── instance-initializers │ │ │ │ ├── application.js │ │ │ │ ├── intl.js │ │ │ │ ├── nwjs │ │ │ │ └── window.js │ │ │ │ └── routing.js │ │ ├── loaders │ │ │ └── ember-app-loader.js │ │ ├── nwjs │ │ │ ├── App.js │ │ │ ├── Window.js │ │ │ └── argv.js │ │ ├── services │ │ │ ├── auth.js │ │ │ ├── chat │ │ │ │ ├── launch.js │ │ │ │ ├── providers │ │ │ │ │ ├── -basic.js │ │ │ │ │ ├── -java.js │ │ │ │ │ ├── browser.js │ │ │ │ │ ├── chatterino.js │ │ │ │ │ ├── chatty-standalone.js │ │ │ │ │ ├── chatty.js │ │ │ │ │ ├── chrome.js │ │ │ │ │ ├── chromium.js │ │ │ │ │ └── custom.js │ │ │ │ └── service.js │ │ │ ├── hotkey.js │ │ │ ├── intl.js │ │ │ ├── modal.js │ │ │ ├── notification │ │ │ │ ├── badge.js │ │ │ │ ├── cache.js │ │ │ │ ├── data.js │ │ │ │ ├── dispatch.js │ │ │ │ ├── icons.js │ │ │ │ ├── polling.js │ │ │ │ ├── provider.js │ │ │ │ ├── providers │ │ │ │ │ ├── auto.js │ │ │ │ │ ├── chrome-notifications.js │ │ │ │ │ ├── growl.js │ │ │ │ │ ├── native.js │ │ │ │ │ ├── rich.js │ │ │ │ │ └── snoretoast.js │ │ │ │ ├── service.js │ │ │ │ └── tray.js │ │ │ ├── nwjs.js │ │ │ ├── settings.js │ │ │ ├── streaming │ │ │ │ ├── cache.js │ │ │ │ ├── launch │ │ │ │ │ ├── index.js │ │ │ │ │ └── parse-error.js │ │ │ │ ├── player │ │ │ │ │ └── resolve.js │ │ │ │ ├── provider │ │ │ │ │ ├── find-pythonscript-interpreter.js │ │ │ │ │ ├── resolve.js │ │ │ │ │ └── validate.js │ │ │ │ └── spawn.js │ │ │ ├── theme.js │ │ │ └── versioncheck.js │ │ ├── ui │ │ │ ├── components │ │ │ │ ├── button │ │ │ │ │ ├── channel-button.js │ │ │ │ │ ├── form-button.js │ │ │ │ │ ├── open-chat.js │ │ │ │ │ ├── share-channel.js │ │ │ │ │ └── twitch-emotes.js │ │ │ │ ├── form │ │ │ │ │ ├── -selectable.js │ │ │ │ │ ├── check-box.js │ │ │ │ │ ├── drop-down-list.js │ │ │ │ │ ├── drop-down.js │ │ │ │ │ ├── number-field.js │ │ │ │ │ └── radio-buttons.js │ │ │ │ ├── helper │ │ │ │ │ ├── bool-.js │ │ │ │ │ ├── find-by.js │ │ │ │ │ ├── format-.js │ │ │ │ │ ├── get-.js │ │ │ │ │ ├── hotkey-title.js │ │ │ │ │ ├── hours-from-now.js │ │ │ │ │ ├── is-.js │ │ │ │ │ ├── math-.js │ │ │ │ │ └── time-from-now.js │ │ │ │ ├── link │ │ │ │ │ ├── embedded-html-links.js │ │ │ │ │ ├── embedded-links.js │ │ │ │ │ ├── external-link.js │ │ │ │ │ └── link-to.js │ │ │ │ ├── list │ │ │ │ │ ├── content-list.js │ │ │ │ │ ├── infinite-scroll.js │ │ │ │ │ └── stream-item.js │ │ │ │ ├── loading-spinner.js │ │ │ │ ├── modals │ │ │ │ │ └── modal-quit.js │ │ │ │ ├── preview-image.js │ │ │ │ ├── search-bar.js │ │ │ │ ├── settings-hotkey.js │ │ │ │ ├── settings-submit.js │ │ │ │ └── title-bar.js │ │ │ └── routes │ │ │ │ ├── -mixins │ │ │ │ └── routes │ │ │ │ │ ├── filter-languages.js │ │ │ │ │ ├── infinite-scroll │ │ │ │ │ ├── css.js │ │ │ │ │ └── index.js │ │ │ │ │ └── refresh.js │ │ │ │ ├── application │ │ │ │ └── route.js │ │ │ │ ├── channel │ │ │ │ └── settings │ │ │ │ │ └── route.js │ │ │ │ ├── games │ │ │ │ ├── game │ │ │ │ │ └── route.js │ │ │ │ └── index │ │ │ │ │ └── route.js │ │ │ │ ├── settings │ │ │ │ └── languages.js │ │ │ │ └── user │ │ │ │ ├── followed-channels │ │ │ │ └── route.js │ │ │ │ └── index │ │ │ │ └── route.js │ │ └── utils │ │ │ ├── Logger.js │ │ │ ├── StreamOutputBuffer.js │ │ │ ├── ember │ │ │ └── ObjectBuffer.js │ │ │ ├── getStreamFromUrl.js │ │ │ ├── linkparser.js │ │ │ ├── node │ │ │ ├── child_process │ │ │ │ ├── promise.js │ │ │ │ └── spawn.js │ │ │ ├── fs │ │ │ │ ├── clearfolder.js │ │ │ │ ├── download.js │ │ │ │ ├── mkdirp.js │ │ │ │ ├── readLines.js │ │ │ │ ├── stat.js │ │ │ │ ├── which.js │ │ │ │ └── whichFallback.js │ │ │ ├── http │ │ │ │ ├── HttpServer.js │ │ │ │ └── getRedirected.js │ │ │ ├── onShutdown.js │ │ │ ├── platform.js │ │ │ └── resolvePath.js │ │ │ ├── parameters │ │ │ ├── Parameter.js │ │ │ ├── ParameterCustom.js │ │ │ └── Substitution.js │ │ │ ├── preload.js │ │ │ └── system-locale.js │ └── web_modules │ │ ├── cartesian-product │ │ ├── index.js │ │ └── test.js │ │ ├── ember-qunit │ │ └── test-loader.js │ │ ├── ember-test.js │ │ ├── event-utils │ │ ├── blur.js │ │ ├── events.js │ │ ├── index.js │ │ ├── test │ │ │ └── events.js │ │ ├── trigger-event.js │ │ └── trigger-key-event.js │ │ ├── htmlbars-inline-precompile.js │ │ ├── intl-utils.js │ │ ├── keyboard-layout-map.js │ │ ├── qunit │ │ ├── assertion-helpers.js │ │ ├── index.js │ │ └── test │ │ │ └── assertion-helpers.js │ │ ├── require.js │ │ ├── store-utils.js │ │ ├── test-utils.js │ │ └── translation-string │ │ └── test.js └── web_modules │ ├── ember-app.js │ ├── ember-data │ └── version.js │ ├── ember-get-config.js │ ├── ember.js │ ├── fetch.js │ ├── metadata.js │ ├── smoothscroll.js │ ├── snoretoast-binaries.js │ ├── translation-key.js │ └── transparent-image.js └── yarn.lock /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: yes 4 | comment: 5 | layout: "reach, diff, flags, files" 6 | after_n_builds: 999999999 7 | coverage: 8 | range: "0..100" 9 | round: down 10 | precision: 2 11 | status: 12 | changes: no 13 | patch: 14 | default: 15 | target: 0 16 | project: 17 | default: no 18 | app: 19 | target: 50 20 | threshold: 1 21 | paths: 22 | - "!src/test/" 23 | tests: 24 | target: 99 25 | threshold: 0 26 | paths: 27 | - "src/test/" 28 | parsers: 29 | javascript: 30 | enable_partials: yes 31 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [ bastimeyer ] 2 | custom: 3 | - "https://www.paypal.com/donate/?hosted_button_id=YUCGRLVALHS8C&item_name=Streamlink%20Twitch%20GUI" 4 | - "https://paypal.me/bastimeyer123" 5 | - "https://www.blockchain.com/btc/address/1EZg8eBz4RdPb8pEzYD9JEzr9Fyitzj8j8" 6 | -------------------------------------------------------------------------------- /.github/codeql/codeql-config.yml: -------------------------------------------------------------------------------- 1 | paths: 2 | - src/ 3 | queries: 4 | - uses: security-and-quality 5 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | schedule: 9 | - cron: '0 0 * * 0' 10 | 11 | permissions: 12 | security-events: write 13 | 14 | jobs: 15 | analyze: 16 | name: Analyze 17 | runs-on: ubuntu-latest 18 | 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | language: ['javascript'] 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v4 27 | 28 | - name: Initialize CodeQL 29 | uses: github/codeql-action/init@v3 30 | with: 31 | languages: ${{ matrix.language }} 32 | config-file: ./.github/codeql/codeql-config.yml 33 | 34 | - name: Perform CodeQL Analysis 35 | uses: github/codeql-action/analyze@v3 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS and editors 2 | .DS_Store 3 | ._* 4 | Thumbs.db 5 | Desktop.ini 6 | *.bak 7 | .cache 8 | .project 9 | .settings 10 | .tmproj 11 | *.esproj 12 | nbproject 13 | *.sublime-project 14 | *.sublime-workspace 15 | .idea 16 | 17 | # gh-pages 18 | /_site/ 19 | 20 | # dependencies 21 | /node_modules/ 22 | 23 | # builds 24 | /build/cache/ 25 | /build/releases/ 26 | /build/tmp/ 27 | /build/travis/data/ 28 | /dist/ 29 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Minimalist Code of Conduct 2 | ==== 3 | 4 | We believe that an elaborate code of conduct invites a lot of fighting over intricate details, much like law, and we do not have the resources to build up the equivalent of a legal system. Therefore we prefer keeping our rules as short as possible and filling the gaps with the mortar of human interaction: empathy. 5 | 6 | All we ask of members and contributors of this project is this: 7 | 8 | - Please treat each other with respect and understanding. 9 | - Please respect our wish to not serve as a stage for disputes about fairness or personal differences. 10 | 11 | If you can agree to these conditions, your contributions are welcome. If you can not, please don't spoil it for the rest of us. 12 | -------------------------------------------------------------------------------- /build/resources/icons/icon-1024.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/f80ad9e92f1b143b9e8900559da212b343772181/build/resources/icons/icon-1024.icns -------------------------------------------------------------------------------- /build/resources/icons/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/f80ad9e92f1b143b9e8900559da212b343772181/build/resources/icons/icon-128.png -------------------------------------------------------------------------------- /build/resources/icons/icon-16-32-48-256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/f80ad9e92f1b143b9e8900559da212b343772181/build/resources/icons/icon-16-32-48-256.ico -------------------------------------------------------------------------------- /build/resources/icons/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/f80ad9e92f1b143b9e8900559da212b343772181/build/resources/icons/icon-16.png -------------------------------------------------------------------------------- /build/resources/icons/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/f80ad9e92f1b143b9e8900559da212b343772181/build/resources/icons/icon-256.png -------------------------------------------------------------------------------- /build/resources/icons/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/f80ad9e92f1b143b9e8900559da212b343772181/build/resources/icons/icon-32.png -------------------------------------------------------------------------------- /build/resources/icons/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/f80ad9e92f1b143b9e8900559da212b343772181/build/resources/icons/icon-48.png -------------------------------------------------------------------------------- /build/resources/icons/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/f80ad9e92f1b143b9e8900559da212b343772181/build/resources/icons/icon-64.png -------------------------------------------------------------------------------- /build/resources/icons/icon-scalable.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /build/resources/linux/add-menuitem.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | APP="streamlink-twitch-gui" 4 | 5 | DIR=$(readlink -f "${0}") 6 | HERE=$(dirname "${DIR}") 7 | 8 | TMP=$(mktemp --directory) 9 | DESKTOP="${TMP}/${APP}.desktop" 10 | 11 | cat << EOF > "${DESKTOP}" 12 | [Desktop Entry] 13 | Type=Application 14 | Name=Streamlink Twitch GUI 15 | GenericName=Twitch.tv browser for Streamlink 16 | Comment=Browse Twitch.tv and watch streams in your videoplayer of choice 17 | Keywords=streamlink;twitch; 18 | Categories=AudioVideo;Network; 19 | StartupWMClass=streamlink-twitch-gui 20 | Exec=${HERE}/${APP} 21 | Icon=${APP} 22 | EOF 23 | 24 | xdg-desktop-menu install "${DESKTOP}" 25 | for SIZE in 16 32 48 64 128 256; do 26 | xdg-icon-resource install --size "${SIZE}" "${HERE}/icons/icon-${SIZE}.png" "${APP}" 27 | done 28 | 29 | rm "${DESKTOP}" 30 | rmdir "${TMP}" 31 | -------------------------------------------------------------------------------- /build/resources/linux/remove-menuitem.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | APP="streamlink-twitch-gui" 4 | 5 | xdg-desktop-menu uninstall "${APP}.desktop" 6 | for SIZE in 16 32 48 64 128 256; do 7 | xdg-icon-resource uninstall --size "${SIZE}" "${APP}" 8 | done 9 | -------------------------------------------------------------------------------- /build/tasks/common/cdp/retry.js: -------------------------------------------------------------------------------- 1 | module.exports = function( n, delay, fn, logger ) { 2 | const retry = i => async err => { 3 | if ( logger && err ) { 4 | logger( err ); 5 | } 6 | await new Promise( resolve => setTimeout( resolve, delay ) ); 7 | return fn( i, n ); 8 | }; 9 | 10 | let promise = Promise.reject(); 11 | for ( let i = 1; i <= n; i++ ) { 12 | promise = promise.catch( retry( i ) ); 13 | } 14 | 15 | return promise; 16 | }; 17 | -------------------------------------------------------------------------------- /build/tasks/common/date.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return process.env.SOURCE_DATE_EPOCH 3 | ? new Date( parseInt( process.env.SOURCE_DATE_EPOCH, 10 ) * 1000 ) 4 | : new Date(); 5 | }; 6 | -------------------------------------------------------------------------------- /build/tasks/common/hash.js: -------------------------------------------------------------------------------- 1 | const { createHash } = require( "crypto" ); 2 | const { createReadStream } = require( "fs" ); 3 | 4 | 5 | module.exports = async function hash( file, algorithm = "sha256", encoding = "hex" ) { 6 | return new Promise( ( resolve, reject ) => { 7 | const hash = createHash( algorithm ); 8 | hash.setEncoding( encoding ); 9 | 10 | const stream = createReadStream( file ); 11 | stream.on( "error", reject ); 12 | stream.on( "end", () => { 13 | hash.end(); 14 | resolve( hash.read() ); 15 | }); 16 | stream.pipe( hash ); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /build/tasks/common/release-changelog.js: -------------------------------------------------------------------------------- 1 | const { promises: { readFile } } = require( "fs" ); 2 | 3 | 4 | /** 5 | * @param {string} file 6 | * @param {string} version 7 | * @returns {Promise} 8 | */ 9 | module.exports = async function getReleaseChangelog( file, version ) { 10 | const reSplit = /\n## /g; 11 | const reFormat = /^\[v?(\d+\.\d+\.\d+(?:-\S+)?)]\(\S+\) \(\d{4}-\d{2}-\d{2}\)\n{2}([\s\S]+)$/; 12 | 13 | const content = ( await readFile( file ) ).toString(); 14 | const release = content 15 | .split( reSplit ) 16 | .slice( 1 ) 17 | .map( release => reFormat.exec( release ) ) 18 | .filter( Boolean ) 19 | .find( release => release[ 1 ] === version ); 20 | 21 | if ( !release || !release[ 2 ] ) { 22 | throw new Error( `Changelog of version '${version}' not found.` ); 23 | } 24 | 25 | return release[ 2 ].trim(); 26 | }; 27 | -------------------------------------------------------------------------------- /build/tasks/configs/aliases.yml: -------------------------------------------------------------------------------- 1 | default: 2 | - "build" 3 | build: 4 | - "build:dev" 5 | build:dev: 6 | - "clean:tmp_dev" 7 | - "webpack:dev" 8 | build:debug: 9 | - "test" 10 | - "clean:tmp_debug" 11 | - "webpack:debug" 12 | build:prod: 13 | - "test" 14 | - "clean:tmp_prod" 15 | - "webpack:prod" 16 | build:prod:coverage: 17 | - "test:coverage" 18 | - "clean:tmp_prod" 19 | - "webpack:prod" 20 | test: 21 | - "clean:tmp_test" 22 | - "webpack:test" 23 | - "runtest" 24 | test:dev: 25 | - "clean:tmp_test" 26 | - "webpack:testdev" 27 | test:coverage: 28 | - "clean:tmp_test" 29 | - "clean:tmp_coverage" 30 | - "webpack:coverage" 31 | - "runtest:coverage" 32 | -------------------------------------------------------------------------------- /build/tasks/configs/checksum.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | options: { 3 | algorithm: "sha256", 4 | encoding: "hex", 5 | dest: "<%= dir.dist %>/<%= package.name %>-<%= version %>-checksums.txt" 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /build/tasks/configs/coverage.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "reporters": [ 3 | { 4 | "name": "html", 5 | "options": { 6 | "subdir": "html-report", 7 | "skipEmpty": false 8 | } 9 | }, 10 | { 11 | "name": "json", 12 | "options": { 13 | "file": "coverage.json" 14 | } 15 | } 16 | ], 17 | "watermarks": { 18 | "lines": [ 80, 95 ], 19 | "functions": [ 80, 95 ], 20 | "branches": [ 80, 95 ], 21 | "statements": [ 80, 95 ] 22 | }, 23 | "i18n": { 24 | "exclude": [ 25 | "hotkeys.codes.*" 26 | ] 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /build/tasks/configs/deploy.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | options: { 3 | changelogFile: "<%= dir.root %>/CHANGELOG.md" 4 | }, 5 | github: { 6 | options: { 7 | apikey: process.env[ "RELEASES_API_KEY" ], 8 | repo: process.env[ "GITHUB_REPOSITORY" ], 9 | tag_name: ( process.env[ "GITHUB_REF" ] || "" ) 10 | .replace( /^(?!refs\/tags\/).+$/, "" ) 11 | .replace( /^refs\/tags\//, "" ), 12 | body: "<%= dir.root %>/.github/release_template.md", 13 | template: { 14 | display_name: "<%= main['display-name'] %>", 15 | version: "<%= package.version %>", 16 | homepage: "<%= package.homepage %>", 17 | donation: "<%= JSON.stringify( main['donation'] ) %>" 18 | } 19 | }, 20 | src: "<%= dir.dist %>/*{.tar.gz,.zip,.exe,.AppImage,-checksums.txt}" 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /build/tasks/configs/run.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | options: Object.assign( {}, require( "./nwjs" ).options, { 3 | flavor: "sdk", 4 | argv: "--remote-debugging-port=8888" 5 | }), 6 | 7 | dev: { 8 | src: "<%= dir.tmp_dev %>/**" 9 | }, 10 | 11 | prod: { 12 | src: "<%= dir.tmp_prod %>/**" 13 | }, 14 | 15 | debug: { 16 | src: "<%= dir.tmp_prod %>/**" 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /build/tasks/configs/runtest.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | options: { 3 | path: "build/tmp/test/**" 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /build/tasks/configs/webpack.js: -------------------------------------------------------------------------------- 1 | module.exports = require( "../webpack" ); 2 | -------------------------------------------------------------------------------- /build/tasks/custom/nwjs.js: -------------------------------------------------------------------------------- 1 | module.exports = function( grunt ) { 2 | const NwBuilder = require( "nw-builder" ); 3 | 4 | function taskNwjs() { 5 | const done = this.async(); 6 | const options = this.options(); 7 | 8 | if ( this.flags.debug ) { 9 | options.flavor = "sdk"; 10 | } 11 | 12 | const nw = new NwBuilder( options ); 13 | 14 | nw.on( "log", grunt.log.debug ); 15 | nw.on( "stdout", grunt.log.debug ); 16 | nw.on( "stderr", grunt.log.debug ); 17 | 18 | nw.build() 19 | .then( () => { 20 | grunt.log.ok( "NW.js application created." ); 21 | done(); 22 | }, grunt.fail.fatal ); 23 | } 24 | 25 | grunt.registerMultiTask( 26 | "nwjs", 27 | "Create an NW.js build of the application", 28 | taskNwjs 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /build/tasks/custom/release.js: -------------------------------------------------------------------------------- 1 | module.exports = function( grunt ) { 2 | const platforms = require( "../common/platforms" ); 3 | 4 | function taskRelease() { 5 | const [ debug, targets ] = platforms.getDebugTargets( this.args ); 6 | 7 | grunt.task.run([ 8 | // build 9 | `build:${debug ? "debug" : "prod"}`, 10 | // compile 11 | ...platforms.getPlatforms( ...targets ) 12 | .map( platform => `compile:${platform}${debug ? ":debug": ""}` ) 13 | ]); 14 | } 15 | 16 | grunt.task.registerTask( 17 | "release", 18 | `Build and compile the application. ${platforms.getList()}`, 19 | taskRelease 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /build/tasks/webpack/babel.config.js: -------------------------------------------------------------------------------- 1 | // global babel{,-loader} config which will be merged by each specific config 2 | module.exports = { 3 | // babel-loader config 4 | // resolves to `node_modules/.cache/babel-loader` 5 | cacheDirectory: true, 6 | 7 | // babel config 8 | parserOpts: {}, 9 | presets: [], 10 | plugins: [] 11 | }; 12 | -------------------------------------------------------------------------------- /build/tasks/webpack/configurators/code-quality.js: -------------------------------------------------------------------------------- 1 | const { pRoot } = require( "../paths" ); 2 | 3 | const ESLintWebpackPlugin = require( "eslint-webpack-plugin" ); 4 | 5 | 6 | /** 7 | * Code quality related configurations 8 | */ 9 | module.exports = { 10 | common( config ) { 11 | config.plugins.push( new ESLintWebpackPlugin({ 12 | context: pRoot, 13 | emitError: true, 14 | failOnError: true 15 | }) ); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /build/tasks/webpack/configurators/ember/app.js: -------------------------------------------------------------------------------- 1 | const { pApp } = require( "../../paths" ); 2 | 3 | 4 | /** 5 | * Custom Ember-Module-Unification loader 6 | * and Ember related loaders for the app modules 7 | */ 8 | module.exports = function( config ) { 9 | config.module.rules.push({ 10 | test: /ember-app\.js$/, 11 | // see `build/tasks/webpack/loaders/ember-app-loader/index.js` 12 | loader: "ember-app-loader", 13 | options: { 14 | context: pApp 15 | } 16 | }); 17 | 18 | config.module.rules.push({ 19 | test: /\.hbs$/, 20 | // see `build/tasks/webpack/loaders/hbs-loader.js` 21 | loader: "hbs-loader" 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /build/tasks/webpack/configurators/ember/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configurations for EmberJS, EmberData, Ember-Addons and everything else related to them. 3 | */ 4 | module.exports = { 5 | common( config, grunt, target ) { 6 | const isProd = target === "prod"; 7 | 8 | require( "./app" )( config, grunt, isProd ); 9 | require( "./source" )( config, grunt, isProd ); 10 | require( "./compatibility-helpers" )( config, grunt, isProd ); 11 | require( "./data" )( config, grunt, isProd ); 12 | require( "./decorators" )( config, grunt, isProd ); 13 | require( "./i18n" )( config, grunt, isProd ); 14 | require( "./tests" )( config, grunt, isProd ); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /build/tasks/webpack/configurators/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require( "./resolve" ), 3 | require( "./code-quality" ), 4 | require( "./nwjs" ), 5 | require( "./ember" ), 6 | require( "./app" ), 7 | require( "./stylesheets-and-assets" ), 8 | require( "./dev" ), 9 | require( "./tests" ), 10 | require( "./i18n" ), 11 | require( "./release" ) 12 | ]; 13 | -------------------------------------------------------------------------------- /build/tasks/webpack/configurators/resolve.js: -------------------------------------------------------------------------------- 1 | const { pApp, pTest } = require( "../paths" ); 2 | const { isTestTarget } = require( "../utils" ); 3 | 4 | 5 | /** 6 | * Resolver configurations for each build target 7 | */ 8 | module.exports = { 9 | common( config, grunt, target ) { 10 | // set the first module resolve path 11 | config.resolve.modules.unshift( 12 | isTestTarget( target ) 13 | ? pTest 14 | : pApp 15 | ); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /build/tasks/webpack/loaders/ember-app-loader/check-duplicates.js: -------------------------------------------------------------------------------- 1 | function sort( { name: a }, { name: b } ) { 2 | return a < b ? -1 : a > b ? 1 : 0; 3 | } 4 | 5 | 6 | module.exports = function ( modules ) { 7 | modules 8 | .slice() 9 | .sort( sort ) 10 | .forEach( ( module, idx, modules ) => { 11 | const next = modules[ idx + 1 ]; 12 | if ( next && next.name === module.name ) { 13 | throw new Error( 14 | `Duplicates found for ${module.name}: ${module.path} and ${next.path}` 15 | ); 16 | } 17 | }); 18 | 19 | return modules; 20 | }; 21 | -------------------------------------------------------------------------------- /build/tasks/webpack/loaders/ember-app-loader/imports.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | dir: "data", 4 | regex: /\.js$/ 5 | }, 6 | { 7 | dir: "init", 8 | regex: /^init\/((?:instance-)?initializer)s\/([^\/]+(\/\1)?)\.js$/ 9 | }, 10 | { 11 | dir: "ui", 12 | regex: /\.(js|hbs)$/ 13 | }, 14 | { 15 | dir: "services", 16 | regex: /^(service)s\/([^\/]+(\/\1)?)\.js$/ 17 | } 18 | ]; 19 | -------------------------------------------------------------------------------- /build/tasks/webpack/loaders/hbs-loader.js: -------------------------------------------------------------------------------- 1 | const TemplateCompiler = require( "ember-source/dist/ember-template-compiler" ); 2 | const { precompile } = TemplateCompiler; 3 | 4 | 5 | module.exports = function( content ) { 6 | const precompiled = precompile( content ).toString(); 7 | 8 | return `export default require('ember').default.HTMLBars.template(${precompiled});`; 9 | }; 10 | -------------------------------------------------------------------------------- /build/tasks/webpack/loaders/metadata-loader.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.cacheable( false ); 3 | 4 | const { version, package: pkg, built } = this.getOptions(); 5 | 6 | return JSON.stringify({ 7 | version, 8 | built, 9 | author: pkg.author, 10 | dependencies: [ "dependencies", "devDependencies" ] 11 | .reduce( ( obj, key ) => Object.assign( obj, pkg[ key ] ), {} ) 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /build/tasks/webpack/loaders/svgo-loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} source 3 | * @this {LoaderContext} 4 | * @return {string} 5 | */ 6 | module.exports = function svgoLoader( source ) { 7 | const { optimize } = require( "svgo" ); 8 | 9 | const { 10 | plugins = [{ name: "preset-default" }], 11 | ...options 12 | } = this.getOptions(); 13 | 14 | const result = optimize( source, { 15 | ...options, 16 | path: this.resourcePath, 17 | plugins 18 | }); 19 | 20 | if ( result.error ) { 21 | throw result.error; 22 | } 23 | 24 | return result.data; 25 | }; 26 | -------------------------------------------------------------------------------- /build/tasks/webpack/loaders/themes-loader.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function( content ) { 3 | const { config, themesVarName, themesPath } = this.query; 4 | 5 | const themes = require( config ); 6 | this.addDependency( config ); 7 | 8 | const imports = themes.themes 9 | .map( theme => `@import "${themesPath}${theme}";` ) 10 | .join( "\n" ); 11 | 12 | const list = themes.themes 13 | .map( theme => `~"${theme}"` ) 14 | .join( ", " ); 15 | 16 | return `${content}\n\n\n${imports}\n@${themesVarName}: ${list};`; 17 | }; 18 | -------------------------------------------------------------------------------- /build/tasks/webpack/paths.js: -------------------------------------------------------------------------------- 1 | const { resolve: r } = require( "path" ); 2 | 3 | 4 | const pProjectRoot = r( __dirname, "..", "..", ".." ); 5 | const pLoaders = r( __dirname, "loaders" ); 6 | const pRoot = r( pProjectRoot, "src" ); 7 | const pApp = r( pRoot, "app" ); 8 | const pAssets = r( pApp, "assets" ); 9 | const pLocales = r( pApp, "locales" ); 10 | const pConfig = r( pRoot, "config" ); 11 | const pTest = r( pRoot, "test" ); 12 | const pTestFixtures = r( pTest, "fixtures" ); 13 | const pDependencies = r( pProjectRoot, "node_modules" ); 14 | 15 | 16 | module.exports = { 17 | pProjectRoot, 18 | pLoaders, 19 | pRoot, 20 | pApp, 21 | pAssets, 22 | pLocales, 23 | pConfig, 24 | pTest, 25 | pTestFixtures, 26 | pDependencies 27 | }; 28 | -------------------------------------------------------------------------------- /build/tasks/webpack/targets/coverage.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | devtool: "inline-source-map", 3 | entry: "main-coverage", 4 | 5 | output: { 6 | path: "<%= dir.tmp_test %>" 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /build/tasks/webpack/targets/debug.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | devtool: "inline-source-map", 3 | 4 | output: { 5 | path: "<%= dir.tmp_prod %>" 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /build/tasks/webpack/targets/dev.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | devtool: "inline-source-map", 3 | 4 | output: { 5 | path: "<%= dir.tmp_dev %>" 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /build/tasks/webpack/targets/i18n.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | devtool: false, 3 | 4 | // this target doesn't output anything, but just in case, set the output path to the test dir 5 | output: { 6 | path: "<%= dir.tmp_test %>" 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /build/tasks/webpack/targets/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dev: require( "./dev" ), 3 | debug: require( "./debug" ), 4 | prod: require( "./prod" ), 5 | test: require( "./test" ), 6 | testdev: require( "./testdev" ), 7 | coverage: require( "./coverage" ), 8 | i18n: require( "./i18n" ) 9 | }; 10 | -------------------------------------------------------------------------------- /build/tasks/webpack/targets/prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: "production", 3 | 4 | output: { 5 | path: "<%= dir.tmp_prod %>" 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /build/tasks/webpack/targets/test.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | output: { 3 | path: "<%= dir.tmp_test %>" 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /build/tasks/webpack/targets/testdev.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | devtool: "inline-source-map", 3 | entry: "main-dev", 4 | 5 | output: { 6 | path: "<%= dir.tmp_test %>" 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /build/tasks/webpack/utils.js: -------------------------------------------------------------------------------- 1 | const { merge } = require( "lodash" ); 2 | const globalBabelConfig = require( "./babel.config" ); 3 | 4 | 5 | function isTestTarget( target ) { 6 | return target === "test" || target === "testdev" || target === "coverage"; 7 | } 8 | 9 | function buildBabelConfig( config ) { 10 | return merge( {}, globalBabelConfig, config ); 11 | } 12 | 13 | 14 | module.exports = { 15 | isTestTarget, 16 | buildBabelConfig 17 | }; 18 | -------------------------------------------------------------------------------- /src/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from "@ember/application"; 2 | import Router from "./router"; 3 | import app from "ember-app"; 4 | 5 | 6 | export default Application.create( app, { 7 | rootElement: "body", 8 | Router, 9 | 10 | toString() { return "App"; } 11 | }); 12 | -------------------------------------------------------------------------------- /src/app/assets/icons/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/f80ad9e92f1b143b9e8900559da212b343772181/src/app/assets/icons/icon-16.png -------------------------------------------------------------------------------- /src/app/assets/icons/icon-16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/f80ad9e92f1b143b9e8900559da212b343772181/src/app/assets/icons/icon-16@2x.png -------------------------------------------------------------------------------- /src/app/assets/icons/icon-16@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/f80ad9e92f1b143b9e8900559da212b343772181/src/app/assets/icons/icon-16@3x.png -------------------------------------------------------------------------------- /src/app/assets/icons/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/f80ad9e92f1b143b9e8900559da212b343772181/src/app/assets/icons/icon-256.png -------------------------------------------------------------------------------- /src/app/assets/icons/icon-osx-18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/f80ad9e92f1b143b9e8900559da212b343772181/src/app/assets/icons/icon-osx-18.png -------------------------------------------------------------------------------- /src/app/assets/icons/icon-osx-18@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/f80ad9e92f1b143b9e8900559da212b343772181/src/app/assets/icons/icon-osx-18@2x.png -------------------------------------------------------------------------------- /src/app/assets/icons/icon-osx-18@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/f80ad9e92f1b143b9e8900559da212b343772181/src/app/assets/icons/icon-osx-18@3x.png -------------------------------------------------------------------------------- /src/app/assets/images/Twitch_Logo_Purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/f80ad9e92f1b143b9e8900559da212b343772181/src/app/assets/images/Twitch_Logo_Purple.png -------------------------------------------------------------------------------- /src/app/data/models/application/adapter.js: -------------------------------------------------------------------------------- 1 | export { default } from "data/models/twitch/adapter"; 2 | -------------------------------------------------------------------------------- /src/app/data/models/auth/adapter.js: -------------------------------------------------------------------------------- 1 | import LocalStorageAdapter from "ember-localstorage-adapter/adapters/ls-adapter"; 2 | 3 | 4 | export default LocalStorageAdapter.extend({ 5 | namespace: "auth" 6 | }); 7 | -------------------------------------------------------------------------------- /src/app/data/models/auth/model.js: -------------------------------------------------------------------------------- 1 | import { computed } from "@ember/object"; 2 | import attr from "ember-data/attr"; 3 | import Model from "ember-data/model"; 4 | 5 | 6 | // noinspection JSValidateTypes 7 | export default Model.extend( /** @class Auth */ { 8 | /** @type {string} */ 9 | access_token: attr( "string" ), 10 | /** @type {string} */ 11 | scope: attr( "string" ), 12 | /** @type {Date} */ 13 | date: attr( "date" ), 14 | 15 | 16 | // state properties 17 | user_id: null, 18 | user_name: null, 19 | 20 | isPending: false, 21 | isLoggedIn: computed( "access_token", "user_id", "isPending", function() { 22 | /** @this {Auth} */ 23 | const { access_token, user_id, isPending } = this; 24 | 25 | return access_token && user_id && !isPending; 26 | }) 27 | 28 | }).reopenClass({ 29 | toString() { return "Auth"; } 30 | }); 31 | -------------------------------------------------------------------------------- /src/app/data/models/auth/serializer.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-localstorage-adapter/serializers/ls-serializer"; 2 | -------------------------------------------------------------------------------- /src/app/data/models/channel-settings/adapter.js: -------------------------------------------------------------------------------- 1 | import LocalStorageAdapter from "ember-localstorage-adapter/adapters/ls-adapter"; 2 | 3 | 4 | export default LocalStorageAdapter.extend({ 5 | namespace: "channelsettings" 6 | }); 7 | -------------------------------------------------------------------------------- /src/app/data/models/channel-settings/serializer.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-localstorage-adapter/serializers/ls-serializer"; 2 | -------------------------------------------------------------------------------- /src/app/data/models/github/releases/adapter.js: -------------------------------------------------------------------------------- 1 | import RESTAdapter from "ember-data/adapters/rest"; 2 | import { update as updateConfig } from "config"; 3 | import AdapterMixin from "data/models/-mixins/adapter"; 4 | 5 | 6 | const { githubreleases: { host, namespace } } = updateConfig; 7 | 8 | 9 | export default RESTAdapter.extend( AdapterMixin, { 10 | host, 11 | namespace, 12 | 13 | queryRecord( store, type, query ) { 14 | const url = this.buildURL( type, null, null, "queryRecord", query ); 15 | 16 | return this.ajax( url, "GET", { data: {} } ); 17 | }, 18 | 19 | urlForQueryRecord( query, modelName ) { 20 | return this._buildURL( modelName, query ); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/data/models/github/releases/model.js: -------------------------------------------------------------------------------- 1 | import { computed } from "@ember/object"; 2 | import attr from "ember-data/attr"; 3 | import Model from "ember-data/model"; 4 | 5 | 6 | export default Model.extend( /** @class GithubReleases */ { 7 | assets: attr(), 8 | assets_url: attr(), 9 | author: attr(), 10 | body: attr(), 11 | created_at: attr(), 12 | draft: attr( "boolean" ), 13 | html_url: attr( "string" ), 14 | name: attr(), 15 | prerelease: attr(), 16 | published_at: attr(), 17 | tag_name: attr( "string" ), 18 | tarball_url: attr(), 19 | target_commitish: attr(), 20 | upload_url: attr(), 21 | url: attr(), 22 | zipball_url: attr(), 23 | 24 | version: computed( "tag_name", function() { 25 | return this.tag_name.replace( /^v/, "" ); 26 | }) 27 | 28 | }).reopenClass({ 29 | toString() { return "releases"; } 30 | }); 31 | -------------------------------------------------------------------------------- /src/app/data/models/github/releases/serializer.js: -------------------------------------------------------------------------------- 1 | import RESTSerializer from "ember-data/serializers/rest"; 2 | 3 | 4 | export default RESTSerializer.extend({ 5 | modelNameFromPayloadKey() { 6 | return "githubReleases"; 7 | }, 8 | 9 | normalizeResponse( store, primaryModelClass, payload, id, requestType ) { 10 | payload = { 11 | githubReleases: payload 12 | }; 13 | 14 | return this._super( store, primaryModelClass, payload, id, requestType ); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/data/models/search/adapter.js: -------------------------------------------------------------------------------- 1 | import LocalStorageAdapter from "ember-localstorage-adapter/adapters/ls-adapter"; 2 | 3 | 4 | export default LocalStorageAdapter.extend({ 5 | namespace: "search" 6 | }); 7 | -------------------------------------------------------------------------------- /src/app/data/models/search/model.js: -------------------------------------------------------------------------------- 1 | import attr from "ember-data/attr"; 2 | import Model from "ember-data/model"; 3 | 4 | 5 | // noinspection JSValidateTypes 6 | export default Model.extend( /** @class Search */ { 7 | /** @type {string} */ 8 | query: attr( "string" ), 9 | /** @type {string} */ 10 | filter: attr( "string" ), 11 | /** @type {Date} */ 12 | date: attr( "date" ) 13 | 14 | }).reopenClass({ 15 | toString() { return "Search"; }, 16 | 17 | filters: [ 18 | { id: "all" }, 19 | { id: "games" }, 20 | { id: "channels" } 21 | ] 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/data/models/search/serializer.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-localstorage-adapter/serializers/ls-serializer"; 2 | -------------------------------------------------------------------------------- /src/app/data/models/settings/adapter.js: -------------------------------------------------------------------------------- 1 | import LocalStorageAdapter from "ember-localstorage-adapter/adapters/ls-adapter"; 2 | 3 | 4 | export default LocalStorageAdapter.extend({ 5 | namespace: "settings" 6 | }); 7 | -------------------------------------------------------------------------------- /src/app/data/models/settings/chat/fragment.js: -------------------------------------------------------------------------------- 1 | import attr from "ember-data/attr"; 2 | import Fragment from "ember-data-model-fragments/fragment"; 3 | import { fragment } from "ember-data-model-fragments/attributes"; 4 | import chatProviders from "services/chat/providers"; 5 | 6 | 7 | const defaultProvider = Object.keys( chatProviders )[0] || "browser"; 8 | 9 | 10 | export default Fragment.extend({ 11 | provider: attr( "string", { defaultValue: defaultProvider } ), 12 | providers: fragment( "settingsChatProviders", { defaultValue: {} } ) 13 | }); 14 | -------------------------------------------------------------------------------- /src/app/data/models/settings/chat/provider/serializer.js: -------------------------------------------------------------------------------- 1 | import PolymorphicFragmentSerializer from "data/models/-mixins/polymorphic-fragment-serializer"; 2 | import { providers, typeKey } from "./fragment"; 3 | 4 | 5 | export default PolymorphicFragmentSerializer.extend({ 6 | models: providers, 7 | modelBaseName: "settings-chat-provider", 8 | typeKey 9 | }); 10 | -------------------------------------------------------------------------------- /src/app/data/models/settings/chat/providers/fragment.js: -------------------------------------------------------------------------------- 1 | import Fragment from "ember-data-model-fragments/fragment"; 2 | import { fragment } from "ember-data-model-fragments/attributes"; 3 | import { typeKey } from "../provider/fragment"; 4 | import chatProviders from "services/chat/providers"; 5 | 6 | 7 | const attributes = {}; 8 | for ( const [ type ] of Object.entries( chatProviders ) ) { 9 | attributes[ type ] = fragment( "settings-chat-provider", { 10 | defaultValue: { 11 | [ typeKey ]: `settings-chat-provider-${type}` 12 | }, 13 | polymorphic: true, 14 | typeKey 15 | }); 16 | } 17 | 18 | 19 | export default Fragment.extend( attributes ); 20 | -------------------------------------------------------------------------------- /src/app/data/models/settings/hotkeys/action/fragment.js: -------------------------------------------------------------------------------- 1 | import Fragment from "ember-data-model-fragments/fragment"; 2 | import { fragment } from "ember-data-model-fragments/attributes"; 3 | 4 | 5 | export default Fragment.extend({ 6 | primary: fragment( "settings-hotkeys-hotkey", { defaultValue: {} } ), 7 | secondary: fragment( "settings-hotkeys-hotkey", { defaultValue: {} } ) 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/data/models/settings/hotkeys/hotkey/fragment.js: -------------------------------------------------------------------------------- 1 | import attr from "ember-data/attr"; 2 | import Fragment from "ember-data-model-fragments/fragment"; 3 | 4 | 5 | export default Fragment.extend({ 6 | disabled: attr( "boolean", { defaultValue: false } ), 7 | code: attr( "string", { defaultValue: null } ), 8 | altKey: attr( "boolean", { defaultValue: false } ), 9 | ctrlKey: attr( "boolean", { defaultValue: false } ), 10 | metaKey: attr( "boolean", { defaultValue: false } ), 11 | shiftKey: attr( "boolean", { defaultValue: false } ) 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/data/models/settings/hotkeys/namespace/serializer.js: -------------------------------------------------------------------------------- 1 | import PolymorphicFragmentSerializer from "data/models/-mixins/polymorphic-fragment-serializer"; 2 | import { namespaces, typeKey } from "./fragment"; 3 | 4 | 5 | export default PolymorphicFragmentSerializer.extend({ 6 | models: namespaces, 7 | modelBaseName: "settings-hotkeys-namespace", 8 | typeKey 9 | }); 10 | -------------------------------------------------------------------------------- /src/app/data/models/settings/serializer.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-localstorage-adapter/serializers/ls-serializer"; 2 | -------------------------------------------------------------------------------- /src/app/data/models/settings/streaming/player/serializer.js: -------------------------------------------------------------------------------- 1 | import PolymorphicFragmentSerializer from "data/models/-mixins/polymorphic-fragment-serializer"; 2 | import { players, typeKey } from "./fragment"; 3 | 4 | 5 | export default PolymorphicFragmentSerializer.extend({ 6 | models: players, 7 | modelBaseName: "settings-streaming-player", 8 | typeKey 9 | }); 10 | -------------------------------------------------------------------------------- /src/app/data/models/settings/streaming/players/fragment.js: -------------------------------------------------------------------------------- 1 | import Fragment from "ember-data-model-fragments/fragment"; 2 | import { fragment } from "ember-data-model-fragments/attributes"; 3 | import { players as playersConfig } from "config"; 4 | import { typeKey } from "../player/fragment"; 5 | 6 | 7 | const attributes = { 8 | "default": fragment( "settings-streaming-player", { defaultValue: {} } ) 9 | }; 10 | for ( const [ type ] of Object.entries( playersConfig ) ) { 11 | attributes[ type ] = fragment( "settings-streaming-player", { 12 | defaultValue: { 13 | [ typeKey ]: `settings-streaming-player-${type}` 14 | }, 15 | polymorphic: true, 16 | typeKey 17 | }); 18 | } 19 | 20 | 21 | export default Fragment.extend( attributes ); 22 | -------------------------------------------------------------------------------- /src/app/data/models/settings/streaming/provider/fragment.js: -------------------------------------------------------------------------------- 1 | import attr from "ember-data/attr"; 2 | import Fragment from "ember-data-model-fragments/fragment"; 3 | 4 | 5 | export default Fragment.extend({ 6 | exec: attr( "string" ), 7 | params: attr( "string" ), 8 | pythonscript: attr( "string" ) 9 | }); 10 | -------------------------------------------------------------------------------- /src/app/data/models/settings/streaming/providers/fragment.js: -------------------------------------------------------------------------------- 1 | import Fragment from "ember-data-model-fragments/fragment"; 2 | import { fragment } from "ember-data-model-fragments/attributes"; 3 | import { streaming as streamingConfig } from "config"; 4 | 5 | 6 | const { providers } = streamingConfig; 7 | 8 | const attributes = {}; 9 | for ( const [ provider ] of Object.entries( providers ) ) { 10 | attributes[ provider ] = fragment( "settingsStreamingProvider", { defaultValue: {} } ); 11 | } 12 | 13 | 14 | export default Fragment.extend( attributes ); 15 | -------------------------------------------------------------------------------- /src/app/data/models/settings/streaming/qualities/fragment.js: -------------------------------------------------------------------------------- 1 | import Fragment from "ember-data-model-fragments/fragment"; 2 | import { fragment } from "ember-data-model-fragments/attributes"; 3 | import { qualities } from "data/models/stream/model"; 4 | 5 | 6 | const attributes = {}; 7 | for ( const { id } of qualities ) { 8 | attributes[ id ] = fragment( "settingsStreamingQuality", { defaultValue: {} } ); 9 | } 10 | 11 | 12 | export default Fragment.extend( attributes ); 13 | -------------------------------------------------------------------------------- /src/app/data/models/settings/streaming/quality/fragment.js: -------------------------------------------------------------------------------- 1 | import attr from "ember-data/attr"; 2 | import Fragment from "ember-data-model-fragments/fragment"; 3 | 4 | 5 | export default Fragment.extend({ 6 | quality: attr( "string" ), 7 | exclude: attr( "string" ) 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/data/models/settings/streams/languages/fragment.js: -------------------------------------------------------------------------------- 1 | import attr from "ember-data/attr"; 2 | import Fragment from "ember-data-model-fragments/fragment"; 3 | import { langs as langsConfig } from "config"; 4 | 5 | 6 | const attributes = {}; 7 | for ( const [ code, { disabled } ] of Object.entries( langsConfig ) ) { 8 | if ( disabled ) { continue; } 9 | attributes[ code ] = attr( "boolean", { defaultValue: false } ); 10 | } 11 | 12 | 13 | export default Fragment.extend( attributes ); 14 | -------------------------------------------------------------------------------- /src/app/data/models/stream/adapter.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-data/adapter"; 2 | -------------------------------------------------------------------------------- /src/app/data/models/twitch/channel/adapter.js: -------------------------------------------------------------------------------- 1 | import TwitchAdapter from "data/models/twitch/adapter"; 2 | 3 | 4 | export default TwitchAdapter.extend({ 5 | coalesceFindRequests: true 6 | }); 7 | -------------------------------------------------------------------------------- /src/app/data/models/twitch/channel/model.js: -------------------------------------------------------------------------------- 1 | import attr from "ember-data/attr"; 2 | import Model from "ember-data/model"; 3 | 4 | 5 | // noinspection JSValidateTypes 6 | export default Model.extend( /** @class TwitchChannel */ { 7 | /** @type {string} */ 8 | broadcaster_language: attr( "string" ), 9 | /** @type {number} */ 10 | delay: attr( "number" ) 11 | 12 | }).reopenClass({ 13 | toString() { return "helix/channels"; } 14 | }); 15 | -------------------------------------------------------------------------------- /src/app/data/models/twitch/channel/serializer.js: -------------------------------------------------------------------------------- 1 | import TwitchSerializer from "data/models/twitch/serializer"; 2 | 3 | 4 | export default TwitchSerializer.extend({ 5 | primaryKey: "broadcaster_id", 6 | 7 | modelNameFromPayloadKey() { 8 | return "twitch-channel"; 9 | }, 10 | 11 | normalize( modelClass, resourceHash, prop ) { 12 | if ( resourceHash[ "broadcaster_language" ] === "id" ) { 13 | resourceHash[ "broadcaster_language" ] = "ID"; 14 | } 15 | 16 | return this._super( modelClass, resourceHash, prop ); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/app/data/models/twitch/channels-followed/model.js: -------------------------------------------------------------------------------- 1 | import attr from "ember-data/attr"; 2 | import Model from "ember-data/model"; 3 | import { belongsTo } from "ember-data/relationships"; 4 | 5 | 6 | export default Model.extend( /** @class TwitchChannelsFollowed */ { 7 | /** @type {TwitchUser} */ 8 | user_id: belongsTo( "twitch-user", { async: true } ), 9 | /** @type {TwitchUser} */ 10 | broadcaster_id: belongsTo( "twitch-user", { async: true } ), 11 | /** @type {Date} */ 12 | followed_at: attr( "date" ) 13 | 14 | }).reopenClass({ 15 | toString() { return "helix/channels/followed"; } 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/data/models/twitch/channels-followed/serializer.js: -------------------------------------------------------------------------------- 1 | import TwitchSerializer from "data/models/twitch/serializer"; 2 | 3 | 4 | export default TwitchSerializer.extend({ 5 | modelNameFromPayloadKey() { 6 | return "twitch-channels-followed"; 7 | }, 8 | 9 | normalize( modelClass, resourceHash, prop ) { 10 | // see adapter for `resourceHash._query.user_id` 11 | resourceHash[ "user_id" ] = resourceHash._query.user_id; 12 | resourceHash[ this.primaryKey ] = `${resourceHash.user_id}-${resourceHash.broadcaster_id}`; 13 | 14 | return this._super( modelClass, resourceHash, prop ); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/data/models/twitch/game-top/model.js: -------------------------------------------------------------------------------- 1 | import Model from "ember-data/model"; 2 | import { belongsTo } from "ember-data/relationships"; 3 | 4 | 5 | export default Model.extend( /** @class TwitchGameTop */ { 6 | /** @type {TwitchGame} */ 7 | game: belongsTo( "twitch-game", { async: false } ) 8 | 9 | }).reopenClass({ 10 | toString() { return "helix/games/top"; } 11 | }); 12 | -------------------------------------------------------------------------------- /src/app/data/models/twitch/game-top/serializer.js: -------------------------------------------------------------------------------- 1 | import TwitchSerializer from "data/models/twitch/serializer"; 2 | 3 | 4 | export default TwitchSerializer.extend({ 5 | modelNameFromPayloadKey() { 6 | return "twitch-game-top"; 7 | }, 8 | 9 | attrs: { 10 | game: { deserialize: "records" } 11 | }, 12 | 13 | normalize( modelClass, resourceHash, prop ) { 14 | const foreignKey = this.store.serializerFor( "twitch-game" ).primaryKey; 15 | resourceHash = { 16 | [ this.primaryKey ]: resourceHash[ foreignKey ], 17 | game: resourceHash 18 | }; 19 | 20 | return this._super( modelClass, resourceHash, prop ); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/data/models/twitch/game/adapter.js: -------------------------------------------------------------------------------- 1 | import TwitchAdapter from "data/models/twitch/adapter"; 2 | 3 | 4 | const returnFalse = () => false; 5 | 6 | 7 | export default TwitchAdapter.extend({ 8 | coalesceFindRequests: true, 9 | 10 | // never reload TwitchGame records 11 | shouldReloadRecord: returnFalse, 12 | shouldBackgroundReloadRecord: returnFalse 13 | }); 14 | -------------------------------------------------------------------------------- /src/app/data/models/twitch/game/model.js: -------------------------------------------------------------------------------- 1 | import attr from "ember-data/attr"; 2 | import Model from "ember-data/model"; 3 | 4 | 5 | // noinspection JSValidateTypes 6 | export default Model.extend( /** @class TwitchGame */ { 7 | /** @type {TwitchImage} */ 8 | box_art_url: attr( "twitch-image", { width: 285, height: 380, expiration: 0 } ), 9 | /** @type {string} */ 10 | name: attr( "string" ) 11 | 12 | }).reopenClass({ 13 | toString() { return "helix/games"; } 14 | }); 15 | -------------------------------------------------------------------------------- /src/app/data/models/twitch/game/serializer.js: -------------------------------------------------------------------------------- 1 | import TwitchSerializer from "data/models/twitch/serializer"; 2 | 3 | 4 | export default TwitchSerializer.extend({ 5 | modelNameFromPayloadKey() { 6 | return "twitch-game"; 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/data/models/twitch/search-channel/serializer.js: -------------------------------------------------------------------------------- 1 | import TwitchSerializer from "data/models/twitch/serializer"; 2 | 3 | 4 | export default TwitchSerializer.extend({ 5 | modelNameFromPayloadKey() { 6 | return "twitch-search-channel"; 7 | }, 8 | 9 | normalize( modelClass, resourceHash, prop ) { 10 | resourceHash.user = resourceHash[ this.primaryKey ]; 11 | resourceHash.game = resourceHash.game_id; 12 | delete resourceHash[ "game_id" ]; 13 | 14 | return this._super( modelClass, resourceHash, prop ); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/data/models/twitch/search-game/model.js: -------------------------------------------------------------------------------- 1 | import Model from "ember-data/model"; 2 | import { belongsTo } from "ember-data/relationships"; 3 | 4 | 5 | export default Model.extend( /** @class TwitchSearchGame */ { 6 | /** @type {TwitchGame} */ 7 | game: belongsTo( "twitch-game", { async: false } ) 8 | 9 | }).reopenClass({ 10 | toString() { return "helix/search/categories"; } 11 | }); 12 | -------------------------------------------------------------------------------- /src/app/data/models/twitch/stream-followed/model.js: -------------------------------------------------------------------------------- 1 | import Model from "ember-data/model"; 2 | import { belongsTo } from "ember-data/relationships"; 3 | 4 | 5 | export default Model.extend( /** @class TwitchStreamFollowed */ { 6 | /** @type {TwitchStream} */ 7 | stream: belongsTo( "twitch-stream", { async: false } ) 8 | 9 | }).reopenClass({ 10 | toString() { return "helix/streams/followed"; } 11 | }); 12 | -------------------------------------------------------------------------------- /src/app/data/models/twitch/stream-followed/serializer.js: -------------------------------------------------------------------------------- 1 | import TwitchSerializer from "data/models/twitch/serializer"; 2 | 3 | 4 | export default TwitchSerializer.extend({ 5 | modelNameFromPayloadKey() { 6 | return "twitch-stream-followed"; 7 | }, 8 | 9 | attrs: { 10 | stream: { deserialize: "records" } 11 | }, 12 | 13 | normalize( modelClass, resourceHash, prop ) { 14 | const foreignKey = this.store.serializerFor( "twitch-stream" ).primaryKey; 15 | resourceHash = { 16 | [ this.primaryKey ]: resourceHash[ foreignKey ], 17 | stream: resourceHash 18 | }; 19 | 20 | return this._super( modelClass, resourceHash, prop ); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/data/models/twitch/stream/adapter.js: -------------------------------------------------------------------------------- 1 | import TwitchAdapter from "data/models/twitch/adapter"; 2 | 3 | 4 | export default TwitchAdapter.extend({ 5 | coalesceFindRequests: true, 6 | 7 | /** 8 | * @param {DS.Store} store 9 | * @param {DS.Model} type 10 | * @param {Object} query 11 | * @return {Promise} 12 | */ 13 | queryRecord( store, type, query ) { 14 | const url = this._buildURL( type ); 15 | 16 | return this.ajax( url, "GET", { data: query } ); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/app/data/models/twitch/team/adapter.js: -------------------------------------------------------------------------------- 1 | import TwitchAdapter from "data/models/twitch/adapter"; 2 | 3 | 4 | export default TwitchAdapter.extend({ 5 | query( store, type, query ) { 6 | const url = this._buildURL( "helix/teams/channel" ); 7 | 8 | return this.ajax( url, "GET", { data: query } ); 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /src/app/data/models/twitch/team/serializer.js: -------------------------------------------------------------------------------- 1 | import TwitchSerializer from "data/models/twitch/serializer"; 2 | 3 | 4 | const { hasOwnProperty } = {}; 5 | 6 | 7 | export default TwitchSerializer.extend({ 8 | modelNameFromPayloadKey() { 9 | return "twitch-team"; 10 | }, 11 | 12 | normalize( modelClass, resourceHash, prop ) { 13 | // we only care about user_id values 14 | resourceHash[ "users" ] = ( resourceHash[ "users" ] /* istanbul ignore next */ || [] ) 15 | .filter( user => user 16 | && typeof user === "object" 17 | && hasOwnProperty.call( user, "user_id" ) 18 | ) 19 | .map( user => user[ "user_id" ] ); 20 | 21 | return this._super( modelClass, resourceHash, prop ); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/data/models/twitch/user/serializer.js: -------------------------------------------------------------------------------- 1 | import TwitchSerializer from "data/models/twitch/serializer"; 2 | 3 | 4 | export default TwitchSerializer.extend({ 5 | modelNameFromPayloadKey() { 6 | return "twitch-user"; 7 | }, 8 | 9 | normalize( modelClass, resourceHash, prop ) { 10 | // add keys for custom channel and stream relationships 11 | resourceHash.channel = resourceHash.stream = resourceHash[ this.primaryKey ]; 12 | 13 | return this._super( modelClass, resourceHash, prop ); 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/data/models/versioncheck/adapter.js: -------------------------------------------------------------------------------- 1 | import LocalStorageAdapter from "ember-localstorage-adapter/adapters/ls-adapter"; 2 | 3 | 4 | export default LocalStorageAdapter.extend({ 5 | namespace: "versioncheck" 6 | }); 7 | -------------------------------------------------------------------------------- /src/app/data/models/versioncheck/model.js: -------------------------------------------------------------------------------- 1 | import attr from "ember-data/attr"; 2 | import Model from "ember-data/model"; 3 | 4 | 5 | export default Model.extend({ 6 | version: attr( "string", { defaultValue: "" } ), 7 | checkagain: attr( "number", { defaultValue: 0 } ), 8 | showdebugmessage: attr( "number", { defaultValue: 0 } ) 9 | 10 | }).reopenClass({ 11 | toString() { return "Versioncheck"; } 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/data/models/versioncheck/serializer.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-localstorage-adapter/serializers/ls-serializer"; 2 | -------------------------------------------------------------------------------- /src/app/data/models/window/adapter.js: -------------------------------------------------------------------------------- 1 | import LocalStorageAdapter from "ember-localstorage-adapter/adapters/ls-adapter"; 2 | 3 | 4 | export default LocalStorageAdapter.extend({ 5 | namespace: "window" 6 | }); 7 | -------------------------------------------------------------------------------- /src/app/data/models/window/model.js: -------------------------------------------------------------------------------- 1 | import attr from "ember-data/attr"; 2 | import Model from "ember-data/model"; 3 | 4 | 5 | export default Model.extend({ 6 | x: attr( "number", { defaultValue: null } ), 7 | y: attr( "number", { defaultValue: null } ), 8 | width: attr( "number", { defaultValue: null } ), 9 | height: attr( "number", { defaultValue: null } ), 10 | maximized: attr( "boolean", { defaultValue: false } ) 11 | 12 | }).reopenClass({ 13 | toString() { return "Window"; } 14 | }); 15 | -------------------------------------------------------------------------------- /src/app/data/models/window/serializer.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-localstorage-adapter/serializers/ls-serializer"; 2 | -------------------------------------------------------------------------------- /src/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Streamlink Twitch GUI 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/app/init/initializers/ember-data.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-data/app/initializers/ember-data"; 2 | -------------------------------------------------------------------------------- /src/app/init/initializers/env.js: -------------------------------------------------------------------------------- 1 | import { locales as localesConfig } from "config"; 2 | 3 | 4 | const { default: defaultLocale } = localesConfig; 5 | 6 | 7 | export default { 8 | name: "env", 9 | 10 | initialize( application ) { 11 | const ENV = { 12 | intl: { 13 | defaultLocale, 14 | defaultFallback: defaultLocale 15 | } 16 | }; 17 | 18 | application.register( "config:environment", ENV ); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/app/init/initializers/keyboard-layout-map.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Map} KeyboardLayoutMap 3 | */ 4 | /** 5 | * @function navigator.keyboard.getLayoutMap 6 | * @returns Promise 7 | */ 8 | 9 | 10 | export default { 11 | name: "keyboard-layout-map", 12 | 13 | /** @param {Ember.Application} application */ 14 | initialize( application ) { 15 | application.deferReadiness(); 16 | 17 | Promise.resolve() 18 | .then( () => navigator.keyboard.getLayoutMap() ) 19 | .catch( () => new Map() ) 20 | .then( /** @param {KeyboardLayoutMap} layoutMap */ layoutMap => { 21 | application.register( "keyboardlayoutmap:main", layoutMap, { instantiate: false } ); 22 | application.advanceReadiness(); 23 | }); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/app/init/initializers/localstorage/channelsettings.js: -------------------------------------------------------------------------------- 1 | import { moveAttributes, qualityIdToName } from "./utils"; 2 | import { qualities } from "data/models/stream/model"; 3 | 4 | 5 | export default function( channelsettings ) { 6 | moveAttributes( channelsettings, { 7 | quality: "streaming_quality", 8 | gui_openchat: "streams_chat_open", 9 | notify_enabled: "notification_enabled" 10 | }); 11 | 12 | qualityIdToName( channelsettings, qualities, "streaming_quality", false ); 13 | } 14 | -------------------------------------------------------------------------------- /src/app/init/initializers/localstorage/localstorage.js: -------------------------------------------------------------------------------- 1 | import nwWindow from "nwjs/Window"; 2 | 3 | 4 | export default nwWindow.window.localStorage; 5 | -------------------------------------------------------------------------------- /src/app/init/initializers/localstorage/namespaces.js: -------------------------------------------------------------------------------- 1 | export default function( LS ) { 2 | const old = LS.getItem( "app" ); 3 | if ( old === null ) { return; } 4 | 5 | try { 6 | for ( const [ ns, obj ] of Object.entries( JSON.parse( old ) ) ) { 7 | const key = ns.toLowerCase(); 8 | const value = JSON.stringify( obj ); 9 | LS.setItem( key, value ); 10 | } 11 | } catch ( e ) { 12 | return; 13 | } 14 | 15 | LS.removeItem( "app" ); 16 | } 17 | -------------------------------------------------------------------------------- /src/app/init/initializers/localstorage/search.js: -------------------------------------------------------------------------------- 1 | /** @typedef {{id: string, query: string, filter: string, date: string}} SearchSerialized */ 2 | 3 | /** @param {Object} records */ 4 | function removeStreamsFilter( records ) { 5 | const channelsQueries = new Set(); 6 | 7 | for ( const record of Object.values( records ) ) { 8 | const { filter } = record; 9 | if ( filter === "channels" ) { 10 | channelsQueries.add( record.query ); 11 | } else if ( filter === "streams" ) { 12 | if ( !channelsQueries.has( record.query ) ) { 13 | record.filter = "channels"; 14 | } else { 15 | delete records[ record.id ]; 16 | } 17 | } 18 | } 19 | } 20 | 21 | 22 | /** @param {Object} records */ 23 | export default function updateSearch( records ) { 24 | removeStreamsFilter( records ); 25 | } 26 | -------------------------------------------------------------------------------- /src/app/init/initializers/nwjs.js: -------------------------------------------------------------------------------- 1 | import Menu from "nwjs/Menu"; 2 | import Menubar from "nwjs/Menubar"; 3 | import Tray from "nwjs/Tray"; 4 | 5 | 6 | export default { 7 | name: "nwjs", 8 | 9 | initialize( application ) { 10 | application.register( "nwjs:menu", Menu, { singleton: false } ); 11 | application.register( "nwjs:menubar", Menubar, { singleton: true } ); 12 | application.register( "nwjs:tray", Tray, { singleton: true } ); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/app/init/initializers/settings-chat-provider.js: -------------------------------------------------------------------------------- 1 | import { providers } from "data/models/settings/chat/provider/fragment"; 2 | import Serializer from "data/models/settings/chat/provider/serializer"; 3 | 4 | 5 | export default { 6 | name: "settings-chat-provider", 7 | 8 | initialize( application ) { 9 | for ( const [ type, model ] of providers ) { 10 | application.register( `model:settings-chat-provider-${type}`, model ); 11 | application.register( `serializer:settings-chat-provider-${type}`, Serializer ); 12 | } 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/app/init/initializers/settings-hotkeys-namespace.js: -------------------------------------------------------------------------------- 1 | import { namespaces } from "data/models/settings/hotkeys/namespace/fragment"; 2 | import Serializer from "data/models/settings/hotkeys/namespace/serializer"; 3 | 4 | 5 | export default { 6 | name: "settings-hotkeys-namespace", 7 | 8 | initialize( application ) { 9 | for ( const [ type, model ] of namespaces ) { 10 | application.register( `model:settings-hotkeys-namespace-${type}`, model ); 11 | application.register( `serializer:settings-hotkeys-namespace-${type}`, Serializer ); 12 | } 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/app/init/initializers/settings-streaming-player.js: -------------------------------------------------------------------------------- 1 | import { players } from "data/models/settings/streaming/player/fragment"; 2 | import Serializer from "data/models/settings/streaming/player/serializer"; 3 | 4 | 5 | export default { 6 | name: "settings-streaming-player", 7 | 8 | initialize( application ) { 9 | for ( const [ type, model ] of players ) { 10 | application.register( `model:settings-streaming-player-${type}`, model ); 11 | application.register( `serializer:settings-streaming-player-${type}`, Serializer ); 12 | } 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/app/init/instance-initializers/boolean-transform.js: -------------------------------------------------------------------------------- 1 | const defaultOptions = { allowNull: true }; 2 | 3 | 4 | export default { 5 | name: "boolean-transform", 6 | 7 | initialize( application ) { 8 | const BooleanTransform = application.lookup( "transform:boolean" ); 9 | 10 | BooleanTransform.reopen({ 11 | deserialize( serialized ) { 12 | return this._super( serialized, defaultOptions ); 13 | }, 14 | 15 | serialize( deserialized ) { 16 | return this._super( deserialized, defaultOptions ); 17 | } 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/app/init/instance-initializers/nwjs/integrations.js: -------------------------------------------------------------------------------- 1 | import { get } from "@ember/object"; 2 | import { setShowInTaskbar } from "nwjs/Window"; 3 | 4 | 5 | export default function( application ) { 6 | const nwjs = application.lookup( "service:nwjs" ); 7 | const settings = application.lookup( "service:settings" ); 8 | const taskbar = get( settings, "gui.isVisibleInTaskbar" ); 9 | const tray = get( settings, "gui.isVisibleInTray" ); 10 | 11 | setShowInTaskbar( taskbar ); 12 | nwjs.setShowInTray( tray ); 13 | } 14 | -------------------------------------------------------------------------------- /src/app/locales/de/contextmenu.yml: -------------------------------------------------------------------------------- 1 | tray: 2 | toggle: "Fenster ein-/ausblenden" 3 | close: Anwendung schließen 4 | macOS: 5 | preferences: Einstellungen 6 | refresh: Aktualisieren 7 | channel-page: Kanalseite 8 | channel-settings: Kanaleinstellungen 9 | change-quality: Qualität ändern 10 | close-stream: Stream schließen 11 | copy: Kopieren 12 | copy-channel-url: URL des Kanals kopieren 13 | copy-link-address: Linkadresse kopieren 14 | copy-selection: Auswahl kopieren 15 | launch-stream: Stream starten 16 | open-chat: Chat öffnen 17 | open-in-browser: Im Browser öffnen 18 | paste: Einfügen 19 | -------------------------------------------------------------------------------- /src/app/locales/de/helpers.yml: -------------------------------------------------------------------------------- 1 | hours-from-now: 2 | now: "gerade eben" 3 | minutes: "{minutes}m" 4 | hours: 5 | simple: "{hours}h" 6 | extended: "{hours}h{minutes}m" 7 | days: 8 | simple: "{days}d" 9 | extended: "{days}d{hours}h" 10 | -------------------------------------------------------------------------------- /src/app/locales/de/hotkeys.yml: -------------------------------------------------------------------------------- 1 | # See the "Hotkeys" section of the "Translating" wiki page on how to properly translate hotkeys: 2 | # https://github.com/streamlink/streamlink-twitch-gui/wiki/Translating#28-hotkeys 3 | modifiers: 4 | altKey: Alt 5 | ctrlKey: Strg 6 | metaKey: Meta 7 | shiftKey: Shift 8 | codes: 9 | ArrowLeft: Links 10 | ArrowRight: Rechts 11 | ArrowUp: Hoch 12 | ArrowDown: Runter 13 | Insert: Einfügen 14 | Delete: Entfernen 15 | Home: Pos1 16 | End: Ende 17 | PageUp: BildAuf 18 | PageDown: BildAb 19 | Space: Leertaste 20 | -------------------------------------------------------------------------------- /src/app/locales/de/models.yml: -------------------------------------------------------------------------------- 1 | twitch: 2 | stream: 3 | started-at: 4 | less-than-24h: "Online seit {started_at, time, medium}" 5 | more-than-24h: "Online seit {started_at, time, long}" 6 | viewer_count: | 7 | {count, plural, 8 | one {Eine Person schaut zu} 9 | other {# Leute schauen zu} 10 | } 11 | search-channel: 12 | started-at: 13 | offline: "Offline" 14 | less-than-24h: "Online seit {started_at, time, medium}" 15 | more-than-24h: "Online seit {started_at, time, long}" 16 | -------------------------------------------------------------------------------- /src/app/locales/de/qualities.yml: -------------------------------------------------------------------------------- 1 | source: Original 2 | high: Hoch 3 | medium: Mittel 4 | low: Niedrig 5 | audio: Nur Ton 6 | -------------------------------------------------------------------------------- /src/app/locales/de/services.yml: -------------------------------------------------------------------------------- 1 | notification: 2 | dispatch: 3 | single: "{name} ist jetzt live" 4 | group: Jetzt live 5 | status: 6 | disabled: Desktop-Benachrichtigungen sind deaktiviert 7 | enabled: Desktop-Benachrichtigungen sind aktiviert 8 | error: Desktop-Benachrichtigungen sind offline 9 | paused: Desktop-Benachrichtigungen sind pausiert 10 | tray: 11 | pause: 12 | label: Benachrichtigungen pausieren 13 | tooltip: "Desktop-Benachrichtigungen temporär ein-/ausschalten" 14 | -------------------------------------------------------------------------------- /src/app/locales/de/themes.yml: -------------------------------------------------------------------------------- 1 | system: Systemeinstellungen 2 | light: Hell 3 | dark: Dunkel 4 | -------------------------------------------------------------------------------- /src/app/locales/en/contextmenu.yml: -------------------------------------------------------------------------------- 1 | tray: 2 | toggle: Toggle window 3 | close: Close application 4 | macOS: 5 | preferences: Preferences 6 | refresh: Refresh 7 | channel-page: Channel page 8 | channel-settings: Channel settings 9 | change-quality: Change quality 10 | close-stream: Close stream 11 | copy: Copy 12 | copy-channel-url: Copy channel URL 13 | copy-link-address: Copy link address 14 | copy-selection: Copy selection 15 | launch-stream: Launch stream 16 | open-chat: Open chat 17 | open-in-browser: Open in browser 18 | paste: Paste 19 | -------------------------------------------------------------------------------- /src/app/locales/en/helpers.yml: -------------------------------------------------------------------------------- 1 | hours-from-now: 2 | now: "just now" 3 | minutes: "{minutes}m" 4 | hours: 5 | simple: "{hours}h" 6 | extended: "{hours}h{minutes}m" 7 | days: 8 | simple: "{days}d" 9 | extended: "{days}d{hours}h" 10 | -------------------------------------------------------------------------------- /src/app/locales/en/hotkeys.yml: -------------------------------------------------------------------------------- 1 | # See the "Hotkeys" section of the "Translating" wiki page on how to properly translate hotkeys: 2 | # https://github.com/streamlink/streamlink-twitch-gui/wiki/Translating#28-hotkeys 3 | modifiers: 4 | altKey: Alt 5 | ctrlKey: Ctrl 6 | metaKey: Meta 7 | shiftKey: Shift 8 | codes: 9 | ArrowLeft: Left 10 | ArrowRight: Right 11 | ArrowUp: Up 12 | ArrowDown: Down 13 | -------------------------------------------------------------------------------- /src/app/locales/en/languages.yml: -------------------------------------------------------------------------------- 1 | ar: Arabic 2 | asl: American sign language 3 | bg: Bulgarian 4 | ca: Catalan 5 | cs: Czech 6 | da: Danish 7 | de: German 8 | el: Greek 9 | en: English 10 | en-gb: British 11 | en-us: American 12 | es: Spanish 13 | es-mx: Mexican 14 | fi: Finnish 15 | fr: French 16 | hi: Hindi 17 | hu: Hungarian 18 | # special uppercase language code 19 | ID: Indonesian 20 | it: Italian 21 | ja: Japanese 22 | ko: Korean 23 | ms: Malay 24 | nl: Dutch 25 | no: Norwegian 26 | other: "\"Other\"" 27 | pl: Polish 28 | pt: Portuguese 29 | pt-br: Brazilian 30 | ro: Romanian 31 | ru: Russian 32 | sk: Slovak 33 | sv: Swedish 34 | tl: Tagalog 35 | th: Thai 36 | tr: Turkish 37 | uk: Ukrainian 38 | vi: Vietnamese 39 | zh: Chinese 40 | zh-cn: Simplified Chinese 41 | zh-hk: Cantonese 42 | zh-tw: Traditional Chinese 43 | -------------------------------------------------------------------------------- /src/app/locales/en/models.yml: -------------------------------------------------------------------------------- 1 | twitch: 2 | stream: 3 | started-at: 4 | less-than-24h: "Online since {started_at, time, medium}" 5 | more-than-24h: "Online since {started_at, time, long}" 6 | viewer_count: | 7 | {count, plural, 8 | one {One person is watching} 9 | other {# people are watching} 10 | } 11 | search-channel: 12 | started-at: 13 | offline: "Offline" 14 | less-than-24h: "Online since {started_at, time, medium}" 15 | more-than-24h: "Online since {started_at, time, long}" 16 | -------------------------------------------------------------------------------- /src/app/locales/en/qualities.yml: -------------------------------------------------------------------------------- 1 | source: Source 2 | high: High 3 | medium: Medium 4 | low: Low 5 | audio: Audio only 6 | -------------------------------------------------------------------------------- /src/app/locales/en/services.yml: -------------------------------------------------------------------------------- 1 | notification: 2 | dispatch: 3 | single: "{name} is now live" 4 | group: Now live 5 | status: 6 | disabled: Desktop notifications are disabled 7 | enabled: Desktop notifications are enabled 8 | error: Desktop notifications are offline 9 | paused: Desktop notifications are paused 10 | tray: 11 | pause: 12 | label: Pause notifications 13 | tooltip: Quickly toggle desktop notifications 14 | -------------------------------------------------------------------------------- /src/app/locales/en/themes.yml: -------------------------------------------------------------------------------- 1 | system: System settings 2 | light: Light 3 | dark: Dark 4 | -------------------------------------------------------------------------------- /src/app/locales/es/contextmenu.yml: -------------------------------------------------------------------------------- 1 | tray: 2 | toggle: Alternar ventana 3 | close: Cerrar aplicación 4 | macOS: 5 | preferences: Preferencias 6 | refresh: Refrescar 7 | channel-page: Página del canal 8 | channel-settings: Configuración del canal 9 | change-quality: Cambiar calidad 10 | close-stream: Cerrar stream 11 | copy: Copiar 12 | copy-channel-url: Copiar URL del canal 13 | copy-link-address: Copiar dirección de enlace 14 | copy-selection: Copiar selección 15 | launch-stream: Iniciar stream 16 | open-chat: Abrir chat 17 | open-in-browser: Abrir en navegador 18 | paste: Pegar 19 | -------------------------------------------------------------------------------- /src/app/locales/es/helpers.yml: -------------------------------------------------------------------------------- 1 | hours-from-now: 2 | now: "justo ahora" 3 | minutes: "{minutes}m" 4 | hours: 5 | simple: "{hours}h" 6 | extended: "{hours}h{minutes}m" 7 | days: 8 | simple: "{days}d" 9 | extended: "{days}d{hours}h" 10 | -------------------------------------------------------------------------------- /src/app/locales/es/hotkeys.yml: -------------------------------------------------------------------------------- 1 | # See the "Hotkeys" section of the "Translating" wiki page on how to properly translate hotkeys: 2 | # https://github.com/streamlink/streamlink-twitch-gui/wiki/Translating#28-hotkeys 3 | modifiers: 4 | altKey: Alt 5 | ctrlKey: Ctrl 6 | metaKey: Meta 7 | shiftKey: Shift 8 | codes: 9 | ArrowLeft: Izquierda 10 | ArrowRight: Derecha 11 | ArrowUp: Arriba 12 | ArrowDown: Abajo 13 | -------------------------------------------------------------------------------- /src/app/locales/es/languages.yml: -------------------------------------------------------------------------------- 1 | ar: Árabe 2 | bg: Búlgaro 3 | cs: Checo 4 | da: Danés 5 | de: Alemán 6 | el: Griego 7 | en: Inglés 8 | en-gb: Británico 9 | en-us: Americano 10 | es: Español 11 | es-mx: Mexicano 12 | fi: Finlandés 13 | fr: Francés 14 | hu: Húngaro 15 | it: Italiano 16 | ja: Japonés 17 | ko: Coreano 18 | nl: Holandés 19 | no: Noruego 20 | pl: Polaco 21 | pt: Portugués 22 | pt-br: Brasileño 23 | ru: Ruso 24 | sk: Eslovaco 25 | sv: Sueco 26 | th: Tailandés 27 | tr: Turco 28 | vi: Vietnamita 29 | zh: Chino 30 | zh-cn: Chino simplificado 31 | zh-tw: Chino tradicional 32 | -------------------------------------------------------------------------------- /src/app/locales/es/models.yml: -------------------------------------------------------------------------------- 1 | twitch: 2 | stream: 3 | started-at: 4 | less-than-24h: "Online desde las {started_at, time, medium}" 5 | more-than-24h: "Online desde las {started_at, time, long}" 6 | viewer_count: | 7 | {count, plural, 8 | one {Una persona está viendo} 9 | other {# personas están viendo} 10 | } 11 | search-channel: 12 | started-at: 13 | offline: "Offline" 14 | less-than-24h: "Online desde las {started_at, time, medium}" 15 | more-than-24h: "Online desde las {started_at, time, long}" 16 | -------------------------------------------------------------------------------- /src/app/locales/es/qualities.yml: -------------------------------------------------------------------------------- 1 | source: Original 2 | high: Alto 3 | medium: Medio 4 | low: Bajo 5 | audio: Solo audio 6 | -------------------------------------------------------------------------------- /src/app/locales/es/services.yml: -------------------------------------------------------------------------------- 1 | notification: 2 | dispatch: 3 | single: "{name} ha empezado streaming" 4 | group: Algunos canales de los que sigues han comenzado a transmitir 5 | status: 6 | disabled: Las notificaciones de escritorio están deshabilitadas 7 | enabled: Las notificaciones de escritorio están habilitadas 8 | error: Las notificaciones de escritorio están offline 9 | paused: Las notificaciones de escritorio están pausadas 10 | tray: 11 | pause: 12 | label: Pausar notificaciones 13 | tooltip: Alternar rápidamente las notificaciones de escritorio 14 | -------------------------------------------------------------------------------- /src/app/locales/es/themes.yml: -------------------------------------------------------------------------------- 1 | system: Ajustes del sistema 2 | light: Claro 3 | dark: Oscuro 4 | -------------------------------------------------------------------------------- /src/app/locales/fr/contextmenu.yml: -------------------------------------------------------------------------------- 1 | tray: 2 | toggle: Basculer la fenêtre 3 | close: Fermer l'application 4 | macOS: 5 | preferences: Préférences 6 | refresh: Rafraîchir 7 | channel-page: Page de la chaîne 8 | channel-settings: Paramètres de la chaîne 9 | change-quality: Changer la qualité 10 | close-stream: Fermer le stream 11 | copy: Copier 12 | copy-channel-url: Copier l'URL de la chaîne 13 | copy-link-address: Copier l'adresse du lien 14 | copy-selection: Copier la sélection 15 | launch-stream: Lancer le stream 16 | open-chat: Ouvrir le chat 17 | open-in-browser: Ouvrir dans le navigateur 18 | paste: Coller 19 | -------------------------------------------------------------------------------- /src/app/locales/fr/helpers.yml: -------------------------------------------------------------------------------- 1 | hours-from-now: 2 | now: "à l'instant" 3 | minutes: "{minutes}m" 4 | hours: 5 | simple: "{hours}h" 6 | extended: "{hours}h{minutes}m" 7 | days: 8 | simple: "{days}d" 9 | extended: "{days}d{hours}h" 10 | -------------------------------------------------------------------------------- /src/app/locales/fr/hotkeys.yml: -------------------------------------------------------------------------------- 1 | # See the "Hotkeys" section of the "Translating" wiki page on how to properly translate hotkeys: 2 | # https://github.com/streamlink/streamlink-twitch-gui/wiki/Translating#28-hotkeys 3 | modifiers: 4 | altKey: Alt 5 | ctrlKey: Ctrl 6 | metaKey: Meta 7 | shiftKey: Shift 8 | codes: 9 | # code: translation 10 | -------------------------------------------------------------------------------- /src/app/locales/fr/languages.yml: -------------------------------------------------------------------------------- 1 | ar: Arabe 2 | asl: Langue des signes américaine 3 | bg: Bulgare 4 | ca: Catalan 5 | cs: Tchèque 6 | da: Danois 7 | de: Allemand 8 | el: Grec 9 | en: Anglais 10 | en-gb: Britannique 11 | en-us: Américain 12 | es: Espagnol 13 | es-mx: Méxicain 14 | fi: Finnois 15 | fr: Français 16 | hi: Hindi 17 | hu: Hongrois 18 | # special uppercase language code 19 | ID: Indonésien 20 | it: Italien 21 | ja: Japonais 22 | ko: Coréen 23 | ms: Malais 24 | nl: Néérlandais 25 | no: Norvégien 26 | other: "\"Autre\"" 27 | pl: Polonais 28 | pt: Portugais 29 | pt-br: Brésilien 30 | ro: Roumain 31 | ru: Russe 32 | sk: Slovaque 33 | sv: Suédois 34 | tl: Tagalog 35 | th: Thaï 36 | tr: Turque 37 | uk: Ukrainien 38 | vi: Vietnamien 39 | zh: Chinois 40 | zh-cn: Chinois Simplifié 41 | zh-hk: Cantonais 42 | zh-tw: Chinois Traditionnel 43 | -------------------------------------------------------------------------------- /src/app/locales/fr/models.yml: -------------------------------------------------------------------------------- 1 | twitch: 2 | stream: 3 | started-at: 4 | less-than-24h: "En ligne depuis le {started_at, time, medium}" 5 | more-than-24h: "En ligne depuis le {started_at, time, long}" 6 | viewer_count: | 7 | {count, plural, 8 | one {Une personne est en train de regarder} 9 | other {# personnes sont en train de regarder} 10 | } 11 | search-channel: 12 | started-at: 13 | offline: "Hors ligne" 14 | less-than-24h: "En ligne depuis le {started_at, time, medium}" 15 | more-than-24h: "En ligne depuis le {started_at, time, long}" 16 | -------------------------------------------------------------------------------- /src/app/locales/fr/qualities.yml: -------------------------------------------------------------------------------- 1 | source: Source 2 | high: Haute 3 | medium: Moyenne 4 | low: Basse 5 | audio: Audio 6 | -------------------------------------------------------------------------------- /src/app/locales/fr/services.yml: -------------------------------------------------------------------------------- 1 | notification: 2 | dispatch: 3 | single: "{name} a commencé un stream" 4 | group: Certaines des chaînes que vous suivez ont commencé à streamer 5 | status: 6 | disabled: Les notifications sur le Bureau sont désactivées 7 | enabled: Les notifications sur le Bureau sont activées 8 | error: Les notifications sur le Bureau sont hors ligne 9 | paused: Les notifications sur le Bureau sont suspendues 10 | tray: 11 | pause: 12 | label: Suspendre les notifications 13 | tooltip: Activer/désactiver rapidement les notifications sur le bureau 14 | -------------------------------------------------------------------------------- /src/app/locales/fr/themes.yml: -------------------------------------------------------------------------------- 1 | system: Réglages du système 2 | light: Clair 3 | dark: Sombre 4 | -------------------------------------------------------------------------------- /src/app/locales/it/contextmenu.yml: -------------------------------------------------------------------------------- 1 | tray: 2 | toggle: Finestra di commutazione 3 | close: Chiudi l'applicazione 4 | macOS: 5 | preferences: Preferenze 6 | refresh: Rircarica 7 | channel-page: Pagina del canale 8 | channel-settings: Impostazioni del canale 9 | change-quality: Cambia qualità 10 | close-stream: Chiudi stream 11 | copy: Copia 12 | copy-channel-url: Copia l'URL del canale 13 | copy-link-address: Copia l'indirizzo del link 14 | copy-selection: Copia selezione 15 | launch-stream: Avvia stream 16 | open-chat: Apri chat 17 | open-in-browser: Apri nel browser 18 | paste: Incolla 19 | -------------------------------------------------------------------------------- /src/app/locales/it/helpers.yml: -------------------------------------------------------------------------------- 1 | hours-from-now: 2 | now: "proprio adesso" 3 | minutes: "{minutes}m" 4 | hours: 5 | simple: "{hours}h" 6 | extended: "{hours}h{minutes}m" 7 | days: 8 | simple: "{days}d" 9 | extended: "{days}d{hours}h" 10 | -------------------------------------------------------------------------------- /src/app/locales/it/hotkeys.yml: -------------------------------------------------------------------------------- 1 | # See the "Hotkeys" section of the "Translating" wiki page on how to properly translate hotkeys: 2 | # https://github.com/streamlink/streamlink-twitch-gui/wiki/Translating#28-hotkeys 3 | modifiers: 4 | altKey: Alt 5 | ctrlKey: Ctrl 6 | metaKey: Meta 7 | shiftKey: Shift 8 | codes: 9 | ArrowLeft: Freccia sinistra 10 | ArrowRight: Freccia destra 11 | ArrowUp: Freccia su 12 | ArrowDown: Freccia giu 13 | -------------------------------------------------------------------------------- /src/app/locales/it/languages.yml: -------------------------------------------------------------------------------- 1 | ar: Arabo 2 | bg: Bulgaro 3 | cs: Ceco 4 | da: Danese 5 | de: Tedesco 6 | el: Greco 7 | en: Inglese 8 | en-gb: Britannico 9 | en-us: Americano 10 | es: Spagna 11 | es-mx: Messicano 12 | fi: Finlandese 13 | fr: Francese 14 | hu: Ungherese 15 | it: Italiano 16 | ja: Giapponese 17 | ko: Coreano 18 | nl: Olandese 19 | no: Norvegese 20 | pl: Polacco 21 | pt: Portoghese 22 | pt-br: Brasiliano 23 | ru: Russo 24 | sk: Sloveno 25 | sv: Svedese 26 | th: Thailandese 27 | tr: Turco 28 | vi: Vietnamita 29 | zh: Cinese 30 | zh-cn: Cinese semplificato 31 | zh-tw: Cinese tradizionale 32 | -------------------------------------------------------------------------------- /src/app/locales/it/models.yml: -------------------------------------------------------------------------------- 1 | twitch: 2 | stream: 3 | started-at: 4 | less-than-24h: "Online da {started_at, time, medium}" 5 | more-than-24h: "Online da {started_at, time, long}" 6 | viewer_count: | 7 | {count, plural, 8 | one {Una persona sta guardando} 9 | other {# persone stanno guardando} 10 | } 11 | search-channel: 12 | started-at: 13 | offline: "Offline" 14 | less-than-24h: "Online da {started_at, time, medium}" 15 | more-than-24h: "Online da {started_at, time, long}" 16 | -------------------------------------------------------------------------------- /src/app/locales/it/qualities.yml: -------------------------------------------------------------------------------- 1 | source: Originale 2 | high: Alta 3 | medium: Media 4 | low: Bassa 5 | audio: Solo audio 6 | -------------------------------------------------------------------------------- /src/app/locales/it/services.yml: -------------------------------------------------------------------------------- 1 | notification: 2 | dispatch: 3 | single: "{name} ha iniziato lo streaming" 4 | group: Alcuni canali seguiti hanno iniziato lo streaming 5 | status: 6 | disabled: Le notifiche desktop sono disabilitate 7 | enabled: Le notifiche desktop sono abilitate 8 | error: Le notifiche desktop sono offline 9 | paused: Le notifiche desktop sono in pausa 10 | tray: 11 | pause: 12 | label: Metti in pausa le notifiche 13 | tooltip: Attiva/disattiva rapidamente le notifiche del desktop 14 | -------------------------------------------------------------------------------- /src/app/locales/it/themes.yml: -------------------------------------------------------------------------------- 1 | system: Impostazioni 2 | light: Chiaro 3 | dark: Scuro 4 | -------------------------------------------------------------------------------- /src/app/locales/ja/contextmenu.yml: -------------------------------------------------------------------------------- 1 | tray: 2 | toggle: ウィンドウの切替 3 | close: アプリを閉じる 4 | macOS: 5 | preferences: 環境設定 6 | refresh: 更新 7 | channel-page: チャンネルページ 8 | channel-settings: チャンネル設定 9 | change-quality: チャンネルの画質 10 | close-stream: ライブを閉じる 11 | copy: コピー 12 | copy-channel-url: チャンネルURLをコピー 13 | copy-link-address: リンクをコピー 14 | copy-selection: 選択部分をコピー 15 | launch-stream: ライブを起動 16 | open-chat: チャットを開く 17 | open-in-browser: ブラウザで開く 18 | paste: ペースト 19 | -------------------------------------------------------------------------------- /src/app/locales/ja/helpers.yml: -------------------------------------------------------------------------------- 1 | hours-from-now: 2 | now: "たった今" 3 | minutes: "{minutes}分" 4 | hours: 5 | simple: "{hours}時間" 6 | extended: "{hours}時間{minutes}分" 7 | days: 8 | simple: "{days}日" 9 | extended: "{days}日{hours}時間" 10 | -------------------------------------------------------------------------------- /src/app/locales/ja/hotkeys.yml: -------------------------------------------------------------------------------- 1 | # See the "Hotkeys" section of the "Translating" wiki page on how to properly translate hotkeys: 2 | # https://github.com/streamlink/streamlink-twitch-gui/wiki/Translating#28-hotkeys 3 | modifiers: 4 | altKey: Alt 5 | ctrlKey: Ctrl 6 | metaKey: Meta 7 | shiftKey: Shift 8 | codes: 9 | ArrowLeft: ← 10 | ArrowRight: → 11 | ArrowUp: ↑ 12 | ArrowDown: ↓ 13 | -------------------------------------------------------------------------------- /src/app/locales/ja/languages.yml: -------------------------------------------------------------------------------- 1 | ar: アラビア語 2 | asl: アメリカ手話 3 | bg: ブルガリア語 4 | ca: カタルーニャ語 5 | cs: チェコ語 6 | da: デンマーク語 7 | de: ドイツ語 8 | el: ギリシア語 9 | en: 英語 10 | en-gb: 英語 (イギリス) 11 | en-us: 英語 (アメリカ) 12 | es: スペイン語 13 | es-mx: スペイン語 (メキシコ) 14 | fi: フィンランド語 15 | fr: フランス語 16 | hi: ヒンディー語 17 | hu: ハンガリー語 18 | # special uppercase language code 19 | ID: インドネシア語 20 | it: イタリア語 21 | ja: 日本語 22 | ko: 朝鮮語 23 | ms: マレー語 24 | nl: オランダ語 25 | no: ノルウェー語 26 | other: "\"その他\"" 27 | pl: ポーランド語 28 | pt: ポルトガル語 29 | pt-br: ポルトガル語 (ブラジル) 30 | ro: ルーマニア語 31 | ru: ロシア語 32 | sk: スロバキア語 33 | sv: スウェーデン語 34 | tl: タガログ語 35 | th: タイ語 36 | tr: トルコ語 37 | uk: ウクライナ語 38 | vi: ベトナム語 39 | zh: 中国語 40 | zh-cn: 中国語 (簡体) 41 | zh-hk: 中国語 (香港) 42 | zh-tw: 中国語 (繁体) 43 | -------------------------------------------------------------------------------- /src/app/locales/ja/models.yml: -------------------------------------------------------------------------------- 1 | twitch: 2 | stream: 3 | started-at: 4 | less-than-24h: "{started_at, time, medium} からオンライン" 5 | more-than-24h: "{started_at, time, long} からオンライン" 6 | viewer_count: | 7 | {count, plural, 8 | other { # 人が視聴中} 9 | } 10 | search-channel: 11 | started-at: 12 | offline: "オフライン" 13 | less-than-24h: "{started_at, time, medium} からオンライン" 14 | more-than-24h: "{started_at, time, long} からオンライン" 15 | -------------------------------------------------------------------------------- /src/app/locales/ja/qualities.yml: -------------------------------------------------------------------------------- 1 | source: ソース 2 | high: 高 3 | medium: 中 4 | low: 低 5 | audio: 音声のみ 6 | -------------------------------------------------------------------------------- /src/app/locales/ja/services.yml: -------------------------------------------------------------------------------- 1 | notification: 2 | dispatch: 3 | single: "{name} がライブを開始しました" 4 | group: 一部のフォロー中のチャンネルがライブを開始しました 5 | status: 6 | disabled: デスクトップ通知をオフにしました 7 | enabled: デスクトップ通知をオンにしました 8 | error: デスクトップ通が停止しました 9 | paused: デスクトップ通知は一時停止中です 10 | tray: 11 | pause: 12 | label: 通知の一時停止 13 | tooltip: デスクトップ通知のオン/オフ 14 | -------------------------------------------------------------------------------- /src/app/locales/ja/themes.yml: -------------------------------------------------------------------------------- 1 | system: システム設定 2 | light: ライト 3 | dark: ダーク 4 | -------------------------------------------------------------------------------- /src/app/locales/pt-br/contextmenu.yml: -------------------------------------------------------------------------------- 1 | tray: 2 | toggle: Alternar janela 3 | close: Fechar aplicação 4 | macOS: 5 | preferences: Preferências 6 | refresh: Atualizar 7 | channel-page: Página do canal 8 | channel-settings: Preferências do canal 9 | change-quality: Mudar qualidade 10 | close-stream: Fechar transmissão 11 | copy: Copiar 12 | copy-channel-url: Copiar URL do canal 13 | copy-link-address: Copiar endereço 14 | copy-selection: Copiar seleção 15 | launch-stream: Abrir transmissão 16 | open-chat: Abrir chat 17 | open-in-browser: Abrir no navegador 18 | paste: Colar 19 | -------------------------------------------------------------------------------- /src/app/locales/pt-br/helpers.yml: -------------------------------------------------------------------------------- 1 | hours-from-now: 2 | now: "Agora" 3 | minutes: "{minutes}m" 4 | hours: 5 | simple: "{hours}h" 6 | extended: "{hours}h{minutes}m" 7 | days: 8 | simple: "{days}d" 9 | extended: "{days}d{hours}h" 10 | -------------------------------------------------------------------------------- /src/app/locales/pt-br/hotkeys.yml: -------------------------------------------------------------------------------- 1 | # See the "Hotkeys" section of the "Translating" wiki page on how to properly translate hotkeys: 2 | # https://github.com/streamlink/streamlink-twitch-gui/wiki/Translating#28-hotkeys 3 | modifiers: 4 | altKey: Alt 5 | ctrlKey: Ctrl 6 | metaKey: Meta 7 | shiftKey: Shift 8 | codes: 9 | ArrowLeft: Esquerda 10 | ArrowRight: Direita 11 | ArrowUp: Cima 12 | ArrowDown: Baixo 13 | -------------------------------------------------------------------------------- /src/app/locales/pt-br/languages.yml: -------------------------------------------------------------------------------- 1 | ar: Árabe 2 | bg: Bulgaria 3 | cs: Tcheco 4 | da: Dinamarquês 5 | de: Alemão 6 | el: Grego 7 | en: Inglês 8 | en-gb: Inglês Britânico 9 | en-us: Inglês Americano 10 | es: Espanhol 11 | es-mx: Mexicano 12 | fi: Finlandês 13 | fr: Francês 14 | hu: Hungariano 15 | it: Italiano 16 | ja: Japonês 17 | ko: Coreano 18 | nl: Holandês 19 | no: Norueguês 20 | pl: Polonês 21 | pt: Português 22 | pt-br: Português (Brasil) 23 | ru: Russo 24 | sk: Eslováco 25 | sv: Suéco 26 | th: Thailandês 27 | tr: Turco 28 | vi: Vietnamita 29 | zh: Chines 30 | zh-cn: Chines (Simplificado) 31 | zh-tw: Chines (Tradicional) 32 | -------------------------------------------------------------------------------- /src/app/locales/pt-br/models.yml: -------------------------------------------------------------------------------- 1 | twitch: 2 | stream: 3 | started-at: 4 | less-than-24h: "Online desde {started_at, time, medium}" 5 | more-than-24h: "Online desde {started_at, time, long}" 6 | viewer_count: | 7 | {count, plural, 8 | one {Uma pessoa está assistindo} 9 | other {# estão assistindo} 10 | } 11 | search-channel: 12 | started-at: 13 | offline: "Offline" 14 | less-than-24h: "Online desde {started_at, time, medium}" 15 | more-than-24h: "Online desde {started_at, time, long}" 16 | -------------------------------------------------------------------------------- /src/app/locales/pt-br/qualities.yml: -------------------------------------------------------------------------------- 1 | source: Original 2 | high: Alta 3 | medium: Média 4 | low: Baixa 5 | audio: Apenas áudio 6 | -------------------------------------------------------------------------------- /src/app/locales/pt-br/services.yml: -------------------------------------------------------------------------------- 1 | notification: 2 | dispatch: 3 | single: "{name} iniciou a transmissão" 4 | group: Alguns canais seguidos iniciaram as transmissões 5 | status: 6 | disabled: Notificação desativada 7 | enabled: Notificação ativada 8 | error: Notificação desligada (offline) 9 | paused: Notificação pausada 10 | tray: 11 | pause: 12 | label: Pausar notificações 13 | tooltip: Ativar ou Desativar as notificações 14 | -------------------------------------------------------------------------------- /src/app/locales/pt-br/themes.yml: -------------------------------------------------------------------------------- 1 | system: Definições do sistema 2 | light: Light 3 | dark: Dark 4 | -------------------------------------------------------------------------------- /src/app/locales/ru/contextmenu.yml: -------------------------------------------------------------------------------- 1 | tray: 2 | toggle: Свернуть/развернуть 3 | close: Закрыть 4 | macOS: 5 | preferences: Настройки 6 | refresh: Обновить 7 | channel-page: Страница канала 8 | channel-settings: Настройки канала 9 | change-quality: Выбрать качество 10 | close-stream: Закрыть стрим 11 | copy: Копировать 12 | copy-channel-url: Копировать ссылку на канал 13 | copy-link-address: Копировать адрес ссылки 14 | copy-selection: Копировать выделенное 15 | launch-stream: Запустить 16 | open-chat: Открыть чат 17 | open-in-browser: Открыть в браузере 18 | paste: Вставить 19 | -------------------------------------------------------------------------------- /src/app/locales/ru/helpers.yml: -------------------------------------------------------------------------------- 1 | hours-from-now: 2 | now: Сейчас 3 | minutes: "{minutes}м" 4 | hours: 5 | simple: "{hours}ч" 6 | extended: "{hours}ч{minutes}м" 7 | days: 8 | simple: "{days}д" 9 | extended: "{days}д{hours}ч" 10 | -------------------------------------------------------------------------------- /src/app/locales/ru/hotkeys.yml: -------------------------------------------------------------------------------- 1 | # See the "Hotkeys" section of the "Translating" wiki page on how to properly translate hotkeys: 2 | # https://github.com/streamlink/streamlink-twitch-gui/wiki/Translating#28-hotkeys 3 | modifiers: 4 | altKey: Alt 5 | ctrlKey: Ctrl 6 | metaKey: Meta 7 | shiftKey: Shift 8 | codes: 9 | ArrowLeft: Влево 10 | ArrowRight: Вправо 11 | ArrowUp: Вверх 12 | ArrowDown: Вниз 13 | -------------------------------------------------------------------------------- /src/app/locales/ru/languages.yml: -------------------------------------------------------------------------------- 1 | ar: арабский 2 | bg: болгарский 3 | cs: чешский 4 | da: датский 5 | de: немецкий 6 | el: греческий 7 | en: английский 8 | en-gb: британский 9 | en-us: американский 10 | es: испанский 11 | es-mx: мексиканский 12 | fi: финский 13 | fr: французский 14 | hu: венгерский 15 | it: итальянский 16 | ja: японский 17 | ko: корейский 18 | nl: голландский 19 | no: норвежский 20 | pl: польский 21 | pt: португальский 22 | pt-br: бразильский 23 | ru: русский 24 | sk: словацкий 25 | sv: шведский 26 | th: тайский 27 | tr: турецкий 28 | vi: вьетнамский 29 | zh: китайский 30 | zh-cn: упрощенный китайский 31 | zh-tw: традиционный китайский 32 | -------------------------------------------------------------------------------- /src/app/locales/ru/models.yml: -------------------------------------------------------------------------------- 1 | twitch: 2 | stream: 3 | started-at: 4 | less-than-24h: "Запущен в {started_at, time, medium}" 5 | more-than-24h: "Запущен в {started_at, time, long}" 6 | viewer_count: | 7 | {count, plural, 8 | one {# зритель} 9 | few {# зрителя} 10 | many {# зрителей} 11 | other {Зрителей: #} 12 | } 13 | search-channel: 14 | started-at: 15 | offline: "не в сети" 16 | less-than-24h: "Запущен в {started_at, time, medium}" 17 | more-than-24h: "Запущен в {started_at, time, long}" 18 | -------------------------------------------------------------------------------- /src/app/locales/ru/qualities.yml: -------------------------------------------------------------------------------- 1 | source: Источник 2 | high: Высокое 3 | medium: Среднее 4 | low: Низкое 5 | audio: Только звук 6 | -------------------------------------------------------------------------------- /src/app/locales/ru/services.yml: -------------------------------------------------------------------------------- 1 | notification: 2 | dispatch: 3 | single: "{name} запустил стрим" 4 | group: "Некоторые отслеживаемые каналы начали трансляцию" 5 | status: 6 | disabled: "Уведомления на рабочем столе отключены" 7 | enabled: "Уведомления на рабочем столе включены" 8 | error: "Уведомления на рабочем столе отключены (offline)" 9 | paused: "Уведомления на рабочем столе приостановлены" 10 | tray: 11 | pause: 12 | label: "Приостановить нотификации" 13 | tooltip: "Быстрое переключение уведомлений на рабочем столе" 14 | -------------------------------------------------------------------------------- /src/app/locales/ru/themes.yml: -------------------------------------------------------------------------------- 1 | system: Настройки системы 2 | light: Светлая 3 | dark: Темная 4 | -------------------------------------------------------------------------------- /src/app/locales/zh-cn/contextmenu.yml: -------------------------------------------------------------------------------- 1 | tray: 2 | toggle: 显示/隐藏窗口 3 | close: 关闭应用 4 | macOS: 5 | preferences: 偏好设置 6 | refresh: 刷新 7 | channel-page: 频道页面 8 | channel-settings: 频道设置 9 | change-quality: 更改画质 10 | close-stream: 关闭直播 11 | copy: 复制 12 | copy-channel-url: 复制频道地址 13 | copy-link-address: 复制链接地址 14 | copy-selection: 复制选中部分 15 | launch-stream: 打开直播 16 | open-chat: 打开聊天室 17 | open-in-browser: 在浏览器打开 18 | paste: 粘贴 19 | -------------------------------------------------------------------------------- /src/app/locales/zh-cn/helpers.yml: -------------------------------------------------------------------------------- 1 | hours-from-now: 2 | now: "刚刚" 3 | minutes: "{minutes}分" 4 | hours: 5 | simple: "{hours}时" 6 | extended: "{hours}时{minutes}分" 7 | days: 8 | simple: "{days}天" 9 | extended: "{days}天{hours}时" 10 | -------------------------------------------------------------------------------- /src/app/locales/zh-cn/hotkeys.yml: -------------------------------------------------------------------------------- 1 | # See the "Hotkeys" section of the "Translating" wiki page on how to properly translate hotkeys: 2 | # https://github.com/streamlink/streamlink-twitch-gui/wiki/Translating#28-hotkeys 3 | modifiers: 4 | altKey: Alt 5 | ctrlKey: Ctrl 6 | metaKey: Meta 7 | shiftKey: Shift 8 | codes: 9 | ArrowLeft: 左 10 | ArrowRight: 右 11 | ArrowUp: 上 12 | ArrowDown: 下 13 | -------------------------------------------------------------------------------- /src/app/locales/zh-cn/languages.yml: -------------------------------------------------------------------------------- 1 | ar: 阿拉伯语 2 | asl: 美国手语 3 | bg: 保加利亚语 4 | ca: 加泰罗尼亚语 5 | cs: 捷克语 6 | da: 丹麦语 7 | de: 德语 8 | el: 希腊语 9 | en: 英语 10 | en-gb: 英式英语 11 | en-us: 美式英语 12 | es: 西班牙语 13 | es-mx: 墨西哥语 14 | fi: 芬兰语 15 | fr: 法语 16 | hi: 印地语 17 | hu: 匈牙利语 18 | # special uppercase language code 19 | ID: 印尼语 20 | it: 意大利语 21 | ja: 日语 22 | ko: 韩语 23 | ms: 马来语 24 | nl: 荷兰语 25 | no: 挪威语 26 | other: "\"其他\"" 27 | pl: 波兰语 28 | pt: 葡萄牙语 29 | pt-br: 葡萄牙语 (巴西) 30 | ro: 罗马尼亚语 31 | ru: 俄语 32 | sk: 斯洛伐克语 33 | sv: 瑞典语 34 | tl: 他加禄语 35 | th: 泰语 36 | tr: 土耳其语 37 | uk: 乌克兰语 38 | vi: 越南语 39 | zh: 中文 40 | zh-cn: 中文 (简体) 41 | zh-hk: 中文 (香港) 42 | zh-tw: 中文 (繁体) 43 | -------------------------------------------------------------------------------- /src/app/locales/zh-cn/models.yml: -------------------------------------------------------------------------------- 1 | twitch: 2 | stream: 3 | started-at: 4 | less-than-24h: "于 {started_at, time, medium} 开始" 5 | more-than-24h: "于 {started_at, time, long} 开始" 6 | viewer_count: | 7 | {count, plural, 8 | other { # 人正在观看} 9 | } 10 | search-channel: 11 | started-at: 12 | offline: "离线" 13 | less-than-24h: "从 {started_at, time, medium} 开始" 14 | more-than-24h: "从 {started_at, time, long} 开始" 15 | -------------------------------------------------------------------------------- /src/app/locales/zh-cn/qualities.yml: -------------------------------------------------------------------------------- 1 | source: 来源 2 | high: 高 3 | medium: 中 4 | low: 低 5 | audio: 仅音频 6 | -------------------------------------------------------------------------------- /src/app/locales/zh-cn/services.yml: -------------------------------------------------------------------------------- 1 | notification: 2 | dispatch: 3 | single: "{name} 已开始直播" 4 | group: 关注的频道已开始直播 5 | status: 6 | disabled: 已关闭桌面通知 7 | enabled: 已开启桌面通知 8 | error: 桌面通知已离线 9 | paused: 已暂停桌面通知 10 | tray: 11 | pause: 12 | label: 暂停通知 13 | tooltip: 开启/关闭桌面通知 14 | -------------------------------------------------------------------------------- /src/app/locales/zh-cn/themes.yml: -------------------------------------------------------------------------------- 1 | system: 系统设置 2 | light: 浅色 3 | dark: 深色 4 | -------------------------------------------------------------------------------- /src/app/locales/zh-tw/contextmenu.yml: -------------------------------------------------------------------------------- 1 | tray: 2 | toggle: 顯示/隱藏視窗 3 | close: 關閉 4 | macOS: 5 | preferences: 偏好設定 6 | refresh: 重新整理 7 | channel-page: 頻道頁面 8 | channel-settings: 頻道設定 9 | change-quality: 品質 10 | close-stream: 關閉實況 11 | copy: 複製 12 | copy-channel-url: 複製頻道網址 13 | copy-link-address: 複製連結網址 14 | copy-selection: 複製 15 | launch-stream: 開啟實況 16 | open-chat: 開啟聊天室 17 | open-in-browser: 在瀏覽器中打開 18 | paste: 貼上 19 | -------------------------------------------------------------------------------- /src/app/locales/zh-tw/helpers.yml: -------------------------------------------------------------------------------- 1 | hours-from-now: 2 | now: "剛剛" 3 | minutes: "{minutes}m" 4 | hours: 5 | simple: "{hours}h" 6 | extended: "{hours}h{minutes}m" 7 | days: 8 | simple: "{days}d" 9 | extended: "{days}d{hours}h" 10 | -------------------------------------------------------------------------------- /src/app/locales/zh-tw/hotkeys.yml: -------------------------------------------------------------------------------- 1 | # See the "Hotkeys" section of the "Translating" wiki page on how to properly translate hotkeys: 2 | # https://github.com/streamlink/streamlink-twitch-gui/wiki/Translating#28-hotkeys 3 | modifiers: 4 | altKey: Alt 5 | ctrlKey: Ctrl 6 | metaKey: Meta 7 | shiftKey: Shift 8 | codes: 9 | ArrowLeft: 左 10 | ArrowRight: 右 11 | ArrowUp: 上 12 | ArrowDown: 下 13 | Numpad0: 數字鍵盤0 14 | Numpad1: 數字鍵盤1 15 | Numpad2: 數字鍵盤2 16 | Numpad3: 數字鍵盤3 17 | Numpad4: 數字鍵盤4 18 | Numpad5: 數字鍵盤5 19 | Numpad6: 數字鍵盤6 20 | Numpad7: 數字鍵盤7 21 | Numpad8: 數字鍵盤8 22 | Numpad9: 數字鍵盤9 23 | NumpadDecimal: 數字鍵盤. 24 | NumpadAdd: 數字鍵盤+ 25 | NumpadSubtract: 數字鍵盤- 26 | NumpadMultiply: 數字鍵盤* 27 | NumpadEnter: 數字鍵盤Enter 28 | NumpadDivide: 數字鍵盤/ 29 | -------------------------------------------------------------------------------- /src/app/locales/zh-tw/models.yml: -------------------------------------------------------------------------------- 1 | twitch: 2 | stream: 3 | started-at: 4 | less-than-24h: "從 {started_at, time, medium} 開始" 5 | more-than-24h: "從 {started_at, time, long} 開始" 6 | viewer_count: | 7 | {count, plural, 8 | one {目前有 1 位觀眾} 9 | other {目前有 # 位觀眾} 10 | } 11 | search-channel: 12 | started-at: 13 | offline: "離線" 14 | less-than-24h: "從 {started_at, time, medium} 開始" 15 | more-than-24h: "從 {started_at, time, long} 開始" 16 | -------------------------------------------------------------------------------- /src/app/locales/zh-tw/qualities.yml: -------------------------------------------------------------------------------- 1 | source: 來源 2 | high: 高 3 | medium: 中 4 | low: 低 5 | audio: 僅有語音 6 | -------------------------------------------------------------------------------- /src/app/locales/zh-tw/services.yml: -------------------------------------------------------------------------------- 1 | notification: 2 | dispatch: 3 | single: "{name} 已開始實況" 4 | group: 開始實況 5 | status: 6 | disabled: 已停用桌面通知 7 | enabled: 已啟用桌面通知 8 | error: 桌面通知已離線 9 | paused: 已暫停桌面通知 10 | tray: 11 | pause: 12 | label: 暫停通知 13 | tooltip: 快速停/啟用桌面通知 14 | -------------------------------------------------------------------------------- /src/app/locales/zh-tw/themes.yml: -------------------------------------------------------------------------------- 1 | system: 系統設置 2 | light: 浅色 3 | dark: 深色 4 | -------------------------------------------------------------------------------- /src/app/nwjs/App.js: -------------------------------------------------------------------------------- 1 | import { App } from "./nwGui"; 2 | import Process from "./process"; 3 | 4 | 5 | App.removeAllListeners( "open" ); 6 | 7 | 8 | export default App; 9 | 10 | export const argv = App.argv; 11 | export const filteredArgv = App.filteredArgv; 12 | export const manifest = App.manifest; 13 | export const dataPath = App.dataPath; 14 | 15 | 16 | export function quit() { 17 | try { 18 | // manually emit the process's exit event 19 | Process.emit( "exit" ); 20 | } catch ( e ) {} 21 | App.quit(); 22 | } 23 | -------------------------------------------------------------------------------- /src/app/nwjs/Screen.js: -------------------------------------------------------------------------------- 1 | import { Screen } from "nwjs/nwGui"; 2 | 3 | 4 | const screen = Screen.Init(); 5 | screen.removeAllListeners(); 6 | 7 | 8 | export default screen; 9 | -------------------------------------------------------------------------------- /src/app/nwjs/debug.js: -------------------------------------------------------------------------------- 1 | /* globals DEBUG, DEVELOPMENT */ 2 | export const isDebug = DEBUG || DEVELOPMENT; 3 | export const isDevelopment = DEVELOPMENT; 4 | -------------------------------------------------------------------------------- /src/app/nwjs/nwGui.js: -------------------------------------------------------------------------------- 1 | import nwGui from "nw.gui"; 2 | 3 | 4 | export default nwGui; 5 | 6 | export const App = nwGui.App; 7 | export const Clipboard = nwGui.Clipboard; 8 | export const Menu = nwGui.Menu; 9 | export const MenuItem = nwGui.MenuItem; 10 | export const Screen = nwGui.Screen; 11 | export const Shell = nwGui.Shell; 12 | export const Shortcut = nwGui.Shortcut; 13 | export const Tray = nwGui.Tray; 14 | export const Window = nwGui.Window; 15 | -------------------------------------------------------------------------------- /src/app/nwjs/process.js: -------------------------------------------------------------------------------- 1 | export default global.process; 2 | -------------------------------------------------------------------------------- /src/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= package.name %>", 3 | "version": "<%= package.version %>", 4 | "product_string": "<%= main['display-name'] %>", 5 | 6 | "main": "index.html", 7 | 8 | "window": { 9 | "title": "<%= main['display-name'] %>", 10 | "frame": false, 11 | "resizable": true, 12 | "show": false, 13 | "position": "center", 14 | "width": 960, 15 | "height": 540, 16 | "min_width": 960, 17 | "min_height": 540, 18 | "icon": "~assets/icons/icon-256.png", 19 | "show_in_taskbar": false 20 | }, 21 | "window.icon": "~assets/icons/icon-256.png", 22 | "icons": { 23 | "256": "~assets/icons/icon-256.png" 24 | }, 25 | 26 | "chromium-args": "--disable-devtools --enable-features=NativeNotifications --disable-smooth-scrolling --disable-features=nw2" 27 | } 28 | -------------------------------------------------------------------------------- /src/app/services/chat/launch.js: -------------------------------------------------------------------------------- 1 | import { spawn } from "child_process"; 2 | import { nextTick } from "process"; 3 | 4 | 5 | export default async function( exec, params ) { 6 | let child; 7 | try { 8 | await new Promise( ( resolve, reject ) => { 9 | child = spawn( exec, params, { 10 | detached: true, 11 | stdio: "ignore" 12 | }); 13 | child.unref(); 14 | child.once( "error", reject ); 15 | nextTick( resolve ); 16 | }); 17 | } finally { 18 | child = null; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/services/chat/logger.js: -------------------------------------------------------------------------------- 1 | import Logger from "utils/Logger"; 2 | 3 | 4 | export const { logDebug, logError } = new Logger( "ChatService" ); 5 | -------------------------------------------------------------------------------- /src/app/services/chat/providers/browser.js: -------------------------------------------------------------------------------- 1 | import ChatProvider from "./-provider"; 2 | import { Shell } from "nwjs/nwGui"; 3 | 4 | 5 | const { openExternal } = Shell; 6 | 7 | 8 | /** 9 | * @class ChatProviderBrowser 10 | * @implements ChatProvider 11 | */ 12 | export default class ChatProviderBrowser extends ChatProvider { 13 | // noinspection JSCheckFunctionSignatures 14 | async setup( config, userConfig = {} ) { 15 | this.context = await this._getContext( config, userConfig ); 16 | } 17 | 18 | // noinspection JSCheckFunctionSignatures 19 | async launch( channel ) { 20 | const url = this._getUrl( channel ); 21 | openExternal( url ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/services/chat/providers/chatterino.js: -------------------------------------------------------------------------------- 1 | import ChatProviderBasic from "./-basic"; 2 | import Parameter from "utils/parameters/Parameter"; 3 | 4 | 5 | /** 6 | * @class ChatProviderChatterino 7 | * @implements ChatProviderBasic 8 | */ 9 | export default class ChatProviderChatterino extends ChatProviderBasic { 10 | // noinspection JSCheckFunctionSignatures 11 | async _getParameters() { 12 | return [ 13 | new Parameter( "--channels", null, "channel" ) 14 | ]; 15 | } 16 | 17 | // noinspection JSCheckFunctionSignatures 18 | _getRuntimeContext( channel ) { 19 | return Object.assign( {}, this.context, { 20 | channel: `t:${channel}` 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/services/chat/providers/chatty-standalone.js: -------------------------------------------------------------------------------- 1 | import ChatProviderBasic from "./-basic"; 2 | import { chattyParameters } from "./chatty"; 3 | 4 | 5 | /** 6 | * @class ChatProviderChattyStandalone 7 | * @implements ChatProviderBasic 8 | */ 9 | export default class ChatProviderChattyStandalone extends ChatProviderBasic { 10 | async _getParameters() { 11 | const parameters = await super._getParameters( ...arguments ); 12 | parameters.unshift( ...chattyParameters ); 13 | 14 | return parameters; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/services/chat/providers/chatty.js: -------------------------------------------------------------------------------- 1 | import ChatProviderJava from "./-java"; 2 | import Parameter from "utils/parameters/Parameter"; 3 | 4 | 5 | export const chattyParameters = [ 6 | new Parameter( "-single", "instance" ), 7 | new Parameter( "-connect" ), 8 | new Parameter( "-channel", null, "channel" ), 9 | new Parameter( "-user", [ "isLoggedIn", "auth" ], "user" ), 10 | new Parameter( "-token", [ "isLoggedIn", "auth" ], "token" ) 11 | ]; 12 | 13 | 14 | /** 15 | * @class ChatProviderChatty 16 | * @implements ChatProviderJava 17 | */ 18 | export default class ChatProviderChatty extends ChatProviderJava { 19 | async _getParameters() { 20 | const parameters = await super._getParameters( ...arguments ); 21 | parameters.splice( 1, 0, ...chattyParameters ); 22 | 23 | return parameters; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/services/chat/providers/chrome.js: -------------------------------------------------------------------------------- 1 | export { default } from "./chromium"; 2 | -------------------------------------------------------------------------------- /src/app/services/chat/providers/custom.js: -------------------------------------------------------------------------------- 1 | export { default } from "./-basic"; 2 | -------------------------------------------------------------------------------- /src/app/services/chat/providers/index.js: -------------------------------------------------------------------------------- 1 | import ChatProviderBrowser from "./browser"; 2 | import ChatProviderChromium from "./chromium"; 3 | import ChatProviderChrome from "./chrome"; 4 | import ChatProviderChatterino from "./chatterino"; 5 | import ChatProviderChatty from "./chatty"; 6 | import ChatProviderChattyStandalone from "./chatty-standalone"; 7 | import ChatProviderCustom from "./custom"; 8 | 9 | 10 | export default { 11 | "browser": ChatProviderBrowser, 12 | "chromium": ChatProviderChromium, 13 | "chrome": ChatProviderChrome, 14 | "chatterino": ChatProviderChatterino, 15 | "chatty": ChatProviderChatty, 16 | "chatty-standalone": ChatProviderChattyStandalone, 17 | "custom": ChatProviderCustom 18 | }; 19 | -------------------------------------------------------------------------------- /src/app/services/notification/cache/item.js: -------------------------------------------------------------------------------- 1 | export default class NotificationStreamCacheItem { 2 | /** 3 | * @param {TwitchStream} stream 4 | */ 5 | constructor( stream ) { 6 | this.id = stream.id; 7 | this.since = stream.started_at; 8 | this.fails = 0; 9 | } 10 | 11 | /** 12 | * @param {TwitchStream[]} streams 13 | * @returns {Number} 14 | */ 15 | findStreamIndex( streams ) { 16 | for ( let id = this.id, i = 0, l = streams.length; i < l; i++ ) { 17 | if ( streams[ i ].id === id ) { 18 | return i; 19 | } 20 | } 21 | return -1; 22 | } 23 | 24 | /** 25 | * @param {TwitchStream} stream 26 | * @returns {Boolean} 27 | */ 28 | isNotNewer( stream ) { 29 | return this.since >= stream.started_at; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/services/notification/logger.js: -------------------------------------------------------------------------------- 1 | import Logger from "utils/Logger"; 2 | 3 | 4 | export const { logDebug, logError } = new Logger( "NotificationService" ); 5 | -------------------------------------------------------------------------------- /src/app/services/notification/providers/auto.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class NotificationProviderAuto 3 | * @implements NotificationProvider 4 | */ 5 | export default class NotificationProviderAuto { 6 | static isSupported() { 7 | return false; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/services/notification/providers/native.js: -------------------------------------------------------------------------------- 1 | import NotificationProviderChromeNotifications from "./chrome-notifications"; 2 | import { isDarwin, isLinux } from "utils/node/platform"; 3 | 4 | 5 | /** 6 | * Uses the system's native notification system 7 | * 8 | * @class NotificationProviderNative 9 | * @implements NotificationProviderChromeNotifications 10 | * @implements NotificationProvider 11 | */ 12 | export default class NotificationProviderNative extends NotificationProviderChromeNotifications { 13 | static isSupported() { 14 | return isDarwin || isLinux; 15 | } 16 | 17 | static supportsListNotifications() { 18 | return false; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/services/streaming/cache/index.js: -------------------------------------------------------------------------------- 1 | import Cache from "./cache"; 2 | 3 | 4 | /** @type {Cache} */ 5 | export const providerCache = new Cache( "exec" ); 6 | 7 | /** @type {Cache} */ 8 | export const playerCache = new Cache( "exec" ); 9 | 10 | export function clearCache() { 11 | providerCache.clear(); 12 | playerCache.clear(); 13 | } 14 | -------------------------------------------------------------------------------- /src/app/services/streaming/cache/item.js: -------------------------------------------------------------------------------- 1 | import { watch } from "fs"; 2 | 3 | 4 | export default class CacheItem { 5 | constructor( data ) { 6 | this.data = data; 7 | } 8 | 9 | watch( listener ) { 10 | this.unwatch(); 11 | this.watcher = watch( this.data, listener ); 12 | } 13 | 14 | unwatch() { 15 | if ( this.watcher ) { 16 | this.watcher.close(); 17 | this.watcher = null; 18 | } 19 | } 20 | 21 | toValue() { 22 | return this.data; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/services/streaming/exec-obj.js: -------------------------------------------------------------------------------- 1 | export default class ExecObj { 2 | /** 3 | * @param {String?} exec 4 | * @param {String[]?} params 5 | * @param {Object?} env 6 | */ 7 | constructor( exec = null, params = null, env = null ) { 8 | this.exec = exec; 9 | this.params = params; 10 | this.env = env; 11 | } 12 | 13 | /** 14 | * @param {ExecObj} execObj 15 | */ 16 | merge( execObj ) { 17 | if ( execObj.exec ) { 18 | this.exec = execObj.exec; 19 | } 20 | if ( execObj.params ) { 21 | this.params = execObj.params; 22 | } 23 | if ( execObj.env ) { 24 | this.env = execObj.env; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/services/streaming/is-aborted.js: -------------------------------------------------------------------------------- 1 | import { get } from "@ember/object"; 2 | import { Aborted } from "./errors"; 3 | 4 | 5 | export default function( stream ) { 6 | if ( get( stream, "isAborted" ) ) { 7 | // remove the record from the store 8 | if ( !get( stream, "isDeleted" ) ) { 9 | stream.destroyRecord(); 10 | } 11 | 12 | throw new Aborted(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/services/streaming/launch/parse-error.js: -------------------------------------------------------------------------------- 1 | import { 2 | PlayerError, 3 | UnableToOpenError, 4 | NoStreamsFoundError, 5 | TimeoutError, 6 | Warning 7 | } from "../errors"; 8 | 9 | 10 | const errors = [ 11 | PlayerError, 12 | UnableToOpenError, 13 | NoStreamsFoundError, 14 | TimeoutError, 15 | Warning 16 | ]; 17 | 18 | 19 | /** 20 | * @param {String} data 21 | * @returns {(Error|null)} 22 | */ 23 | export default function( data ) { 24 | for ( let ErrorClass of errors ) { 25 | for ( let regex of ErrorClass.regex ) { 26 | const match = regex.exec( data ); 27 | if ( match ) { 28 | return new ErrorClass( ...match ); 29 | } 30 | } 31 | } 32 | return null; 33 | } 34 | -------------------------------------------------------------------------------- /src/app/services/streaming/logger.js: -------------------------------------------------------------------------------- 1 | import Logger from "utils/Logger"; 2 | 3 | 4 | const { logDebug, logError } = new Logger( "StreamingService" ); 5 | 6 | 7 | export { 8 | logDebug, 9 | logError 10 | }; 11 | -------------------------------------------------------------------------------- /src/app/services/streaming/spawn.js: -------------------------------------------------------------------------------- 1 | import { logDebug } from "./logger"; 2 | import spawn from "utils/node/child_process/spawn"; 3 | 4 | 5 | const { assign } = Object; 6 | 7 | 8 | /** 9 | * @param {ExecObj} execObj 10 | * @param {String[]} additonalParams 11 | * @param {Object} options 12 | * @returns {ChildProcess} 13 | */ 14 | export default function( execObj, additonalParams = [], options = {} ) { 15 | let { exec, params, env } = execObj; 16 | 17 | params = [ 18 | ...( params || [] ), 19 | ...( additonalParams || [] ) 20 | ]; 21 | 22 | if ( env ) { 23 | options.env = assign( {}, options.env, env ); 24 | } 25 | 26 | logDebug( "Spawning process", { 27 | exec, 28 | params, 29 | env 30 | }); 31 | 32 | return spawn( exec, params, options ); 33 | } 34 | -------------------------------------------------------------------------------- /src/app/ui/components/button/channel-button/styles.less: -------------------------------------------------------------------------------- 1 | .channel-button-component > i { 2 | transform: translateY(1px); 3 | } 4 | -------------------------------------------------------------------------------- /src/app/ui/components/button/form-button/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if hasBlock}}{{_setHasBlock}}{{/if}} 2 | {{#if icon}} 3 | {{#if isLoading}} 4 | {{loading-spinner}} 5 | {{else}} 6 | 7 | {{/if}} 8 | {{/if}} 9 | {{#if hasBlock}}{{yield}}{{/if}} -------------------------------------------------------------------------------- /src/app/ui/components/form/-input-btn/template.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{#if hasBlock}} 3 | {{_setHasBlock}} 4 |
5 |
{{yield}}
6 | {{#if (hasBlock "inverse")}} 7 |
{{yield to="inverse"}}
8 | {{/if}} 9 |
10 | {{else if label}} 11 |
12 |
{{label}}
13 | {{#if description}} 14 |
{{description}}
15 | {{/if}} 16 |
17 | {{/if}} -------------------------------------------------------------------------------- /src/app/ui/components/form/check-box/component.js: -------------------------------------------------------------------------------- 1 | import { get } from "@ember/object"; 2 | import InputBtnComponent from "../-input-btn/component"; 3 | 4 | 5 | export default InputBtnComponent.extend({ 6 | classNames: [ "check-box-component" ], 7 | 8 | click() { 9 | if ( get( this, "disabled" ) ) { return; } 10 | this.toggleProperty( "checked" ); 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/ui/components/form/drop-down-list/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if hasBlock}} 2 | {{#each content as |item|}} 3 |
  • 4 | {{yield (hash 5 | isSelected=(is-equal item selection) 6 | label=(get item optionLabelPath) 7 | value=(get item optionValuePath) 8 | )}} 9 |
  • 10 | {{/each}} 11 | {{else}} 12 | {{#each content as |item|}} 13 |
  • 14 | {{get item optionLabelPath}} 15 |
  • 16 | {{/each}} 17 | {{/if}} -------------------------------------------------------------------------------- /src/app/ui/components/form/drop-down-selection/component.js: -------------------------------------------------------------------------------- 1 | import Component from "@ember/component"; 2 | import { get } from "@ember/object"; 3 | import { inject as service } from "@ember/service"; 4 | import { t } from "ember-intl"; 5 | import layout from "./template.hbs"; 6 | 7 | 8 | export default Component.extend({ 9 | /** @type {IntlService} */ 10 | intl: service(), 11 | 12 | layout, 13 | 14 | tagName: "div", 15 | classNames: [ "drop-down-selection-component" ], 16 | classNameBindings: [ "class" ], 17 | 18 | placeholder: t( "components.drop-down-selection.placeholder" ), 19 | 20 | click() { 21 | get( this, "action" )(); 22 | return false; 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/ui/components/form/drop-down-selection/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if hasBlock}} 2 | {{yield (hash 3 | hasSelection=(if selection true false) 4 | label=(get selection optionLabelPath) 5 | value=(get selection optionValuePath) 6 | placeholder=placeholder 7 | )}} 8 | {{else}} 9 | {{if selection 10 | (get selection optionLabelPath) 11 | placeholder 12 | }} 13 | {{/if}} -------------------------------------------------------------------------------- /src/app/ui/components/form/file-select/template.hbs: -------------------------------------------------------------------------------- 1 | {{input type="text" value=value class=inputClass placeholder=placeholder disabled=disabled}} 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/ui/components/form/number-field/template.hbs: -------------------------------------------------------------------------------- 1 | 9 |
    10 | 11 | 12 |
    -------------------------------------------------------------------------------- /src/app/ui/components/form/radio-buttons-item/component.js: -------------------------------------------------------------------------------- 1 | import { get } from "@ember/object"; 2 | import InputBtnComponent from "../-input-btn/component"; 3 | 4 | 5 | export default InputBtnComponent.extend({ 6 | classNames: [ "radio-buttons-item-component" ], 7 | 8 | click() { 9 | if ( get( this, "disabled" ) ) { return; } 10 | get( this, "action" )(); 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/ui/components/form/radio-buttons/component.js: -------------------------------------------------------------------------------- 1 | import { set } from "@ember/object"; 2 | import Selectable from "../-selectable/component"; 3 | import layout from "./template.hbs"; 4 | import "./styles.less"; 5 | 6 | 7 | export default Selectable.extend({ 8 | layout, 9 | 10 | tagName: "div", 11 | classNames: [ "radio-buttons-component" ], 12 | 13 | actions: { 14 | change( item ) { 15 | set( this, "selection", item ); 16 | } 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/app/ui/components/form/radio-buttons/template.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{#if hasBlock}} 3 | {{#each content as |item|}} 4 | {{yield (hash 5 | button=(component "radio-buttons-item" 6 | label=(get item optionLabelPath) 7 | checked=(is-equal item selection) 8 | disabled=item.disabled 9 | action=(action "change" item bubbles=false) 10 | ) 11 | label=(get item optionLabelPath) 12 | value=(get item optionValuePath) 13 | checked=(is-equal item selection) 14 | disabled=item.disabled 15 | )}} 16 | {{/each}} 17 | {{else}} 18 | {{#each content as |item|}} 19 | {{#radio-buttons-item 20 | checked=(is-equal item selection) 21 | disabled=item.disabled 22 | action=(action "change" item bubbles=false) 23 | }} 24 | {{get item optionLabelPath}} 25 | {{/radio-buttons-item}} 26 | {{/each}} 27 | {{/if}} -------------------------------------------------------------------------------- /src/app/ui/components/helper/-from-now.js: -------------------------------------------------------------------------------- 1 | import Helper from "@ember/component/helper"; 2 | import { run } from "@ember/runloop"; 3 | 4 | 5 | export const helper = Helper.extend({ 6 | compute( params, hash ) { 7 | if ( hash.interval ) { 8 | this._interval = setTimeout( () => run( () => this.recompute() ), hash.interval ); 9 | } 10 | 11 | return this._compute( ...arguments ); 12 | }, 13 | 14 | destroy() { 15 | if ( this._interval ) { 16 | clearTimeout( this._interval ); 17 | this._interval = null; 18 | } 19 | 20 | this._super( ...arguments ); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/ui/components/helper/bool-and.js: -------------------------------------------------------------------------------- 1 | import { helper as h } from "@ember/component/helper"; 2 | 3 | 4 | export const helper = h( params => params.every( value => value ) ); 5 | -------------------------------------------------------------------------------- /src/app/ui/components/helper/bool-not.js: -------------------------------------------------------------------------------- 1 | import { helper as h } from "@ember/component/helper"; 2 | 3 | 4 | export const helper = h( params => params.every( value => !value ) ); 5 | -------------------------------------------------------------------------------- /src/app/ui/components/helper/bool-or.js: -------------------------------------------------------------------------------- 1 | import { helper as h } from "@ember/component/helper"; 2 | 3 | 4 | export const helper = h( params => params.some( value => value ) ); 5 | -------------------------------------------------------------------------------- /src/app/ui/components/helper/find-by.js: -------------------------------------------------------------------------------- 1 | import { helper as h } from "@ember/component/helper"; 2 | 3 | 4 | export const helper = h( ([ arr, key, value ]) => arr.findBy( key, value ) ); 5 | -------------------------------------------------------------------------------- /src/app/ui/components/helper/format-viewers.js: -------------------------------------------------------------------------------- 1 | import { helper as h } from "@ember/component/helper"; 2 | 3 | 4 | export const helper = h( params => { 5 | const viewers = Number( params[ 0 ] ); 6 | 7 | return isNaN( viewers ) 8 | ? "0" 9 | : viewers >= 1000000 10 | ? `${( Math.floor( viewers / 10000 ) / 100 ).toFixed( 2 )}m` 11 | : viewers >= 100000 12 | ? `${( Math.floor( viewers / 1000 ) ).toFixed( 0 )}k` 13 | : viewers >= 10000 14 | ? `${( Math.floor( viewers / 100 ) / 10 ).toFixed( 1 )}k` 15 | : viewers.toFixed( 0 ); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/ui/components/helper/get-index.js: -------------------------------------------------------------------------------- 1 | import { helper as h } from "@ember/component/helper"; 2 | 3 | 4 | export const helper = h( ([ arr, key ]) => arr[ key ] ); 5 | -------------------------------------------------------------------------------- /src/app/ui/components/helper/get-param.js: -------------------------------------------------------------------------------- 1 | import { helper as h } from "@ember/component/helper"; 2 | 3 | 4 | export const helper = h( ( params, hash ) => params[ hash.index ] ); 5 | -------------------------------------------------------------------------------- /src/app/ui/components/helper/is-equal.js: -------------------------------------------------------------------------------- 1 | import { helper as h } from "@ember/component/helper"; 2 | 3 | 4 | export const helper = h( params => 5 | params.every( ( currentValue, index, arr ) => currentValue === arr[ 0 ] ) 6 | ); 7 | -------------------------------------------------------------------------------- /src/app/ui/components/helper/is-gt.js: -------------------------------------------------------------------------------- 1 | import { helper as h } from "@ember/component/helper"; 2 | 3 | 4 | export const helper = h( params => params[0] > params[1] ); 5 | -------------------------------------------------------------------------------- /src/app/ui/components/helper/is-gte.js: -------------------------------------------------------------------------------- 1 | import { helper as h } from "@ember/component/helper"; 2 | 3 | 4 | export const helper = h( params => params[0] >= params[1] ); 5 | -------------------------------------------------------------------------------- /src/app/ui/components/helper/is-null.js: -------------------------------------------------------------------------------- 1 | import { helper as h } from "@ember/component/helper"; 2 | 3 | 4 | export const helper = h( params => params.every( value => value === null ) ); 5 | -------------------------------------------------------------------------------- /src/app/ui/components/helper/math-add.js: -------------------------------------------------------------------------------- 1 | import { helper as h } from "@ember/component/helper"; 2 | 3 | 4 | export const helper = h( params => params.reduce( ( a, b ) => a + b ) ); 5 | -------------------------------------------------------------------------------- /src/app/ui/components/helper/math-div.js: -------------------------------------------------------------------------------- 1 | import { helper as h } from "@ember/component/helper"; 2 | 3 | 4 | export const helper = h( params => params.reduce( ( a, b ) => a / b ) ); 5 | -------------------------------------------------------------------------------- /src/app/ui/components/helper/math-mul.js: -------------------------------------------------------------------------------- 1 | import { helper as h } from "@ember/component/helper"; 2 | 3 | 4 | export const helper = h( params => params.reduce( ( a, b ) => a * b ) ); 5 | -------------------------------------------------------------------------------- /src/app/ui/components/helper/math-sub.js: -------------------------------------------------------------------------------- 1 | import { helper as h } from "@ember/component/helper"; 2 | 3 | 4 | export const helper = h( params => params.reduce( ( a, b ) => a - b ) ); 5 | -------------------------------------------------------------------------------- /src/app/ui/components/helper/t.js: -------------------------------------------------------------------------------- 1 | export { default as helper } from "ember-intl/helpers/t"; 2 | -------------------------------------------------------------------------------- /src/app/ui/components/link/documentation-link/styles.less: -------------------------------------------------------------------------------- 1 | 2 | .documentation-link-component { 3 | font-style: italic; 4 | 5 | &.with-url { 6 | cursor: pointer; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/app/ui/components/link/documentation-link/template.hbs: -------------------------------------------------------------------------------- 1 | {{item}} -------------------------------------------------------------------------------- /src/app/ui/components/link/embedded-links/component.js: -------------------------------------------------------------------------------- 1 | import Component from "@ember/component"; 2 | import { get, computed } from "@ember/object"; 3 | import { parseString } from "utils/linkparser"; 4 | import layout from "./template.hbs"; 5 | 6 | 7 | export default Component.extend({ 8 | layout, 9 | 10 | classNames: [ "embedded-links-component" ], 11 | 12 | content: computed( "text", function() { 13 | const text = get( this, "text" ); 14 | const parsed = parseString( text ); 15 | const links = parsed.links; 16 | 17 | // merge texts and links 18 | return parsed.texts.reduce(function( output, textItem, index ) { 19 | if ( textItem.length ) { 20 | output.push({ text: textItem }); 21 | } 22 | if ( links[ index ] ) { 23 | output.push( links[ index ] ); 24 | } 25 | return output; 26 | }, [] ); 27 | }) 28 | }); 29 | -------------------------------------------------------------------------------- /src/app/ui/components/link/embedded-links/template.hbs: -------------------------------------------------------------------------------- 1 | {{~#each content as |item|~}} 2 | {{~#if item.url~}} 3 | {{~#external-link url=item.url~}} 4 | {{~item.text~}} 5 | {{~/external-link~}} 6 | {{~else~}} 7 | {{~item.text~}} 8 | {{~/if~}} 9 | {{~/each~}} -------------------------------------------------------------------------------- /src/app/ui/components/list/-list-item/component.js: -------------------------------------------------------------------------------- 1 | import Component from "@ember/component"; 2 | import { inject as service } from "@ember/service"; 3 | 4 | 5 | export default Component.extend({ 6 | settings: service(), 7 | 8 | tagName: "li", 9 | classNameBindings: [ 10 | "isNewItem:newItem", 11 | "isDuplicateItem:duplicateItem" 12 | ], 13 | 14 | isNewItem: false, 15 | isDuplicateItem: false 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/ui/components/list/channel-item/component.js: -------------------------------------------------------------------------------- 1 | import ListItemComponent from "../-list-item/component"; 2 | import { alias } from "@ember/object/computed"; 3 | import layout from "./template.hbs"; 4 | import "./styles.less"; 5 | 6 | 7 | export default ListItemComponent.extend({ 8 | layout, 9 | 10 | classNames: [ "channel-item-component" ], 11 | 12 | user: alias( "content.user" ), 13 | followed_at: alias( "content.followed_at" ) 14 | }); 15 | -------------------------------------------------------------------------------- /src/app/ui/components/list/content-list/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if content.length}} 2 |
      3 | {{#each content as |item index|}} 4 | {{yield 5 | item 6 | (is-gte index lengthInitial) 7 | (get-index duplicates index) 8 | }} 9 | {{/each}} 10 |
    11 | {{#if infiniteScroll}} 12 | {{infinite-scroll 13 | content=content 14 | isFetching=isFetching 15 | hasFetchedAll=hasFetchedAll 16 | fetchError=fetchError 17 | action=(action "willFetchContent") 18 | }} 19 | {{/if}} 20 | {{else if (hasBlock "inverse")}} 21 |
    22 | {{yield to="inverse"}} 23 |
    24 | {{/if}} -------------------------------------------------------------------------------- /src/app/ui/components/list/game-item/component.js: -------------------------------------------------------------------------------- 1 | import { inject as service } from "@ember/service"; 2 | import ListItemComponent from "../-list-item/component"; 3 | import layout from "./template.hbs"; 4 | import "./styles.less"; 5 | 6 | 7 | export default ListItemComponent.extend({ 8 | /** @type {RouterService} */ 9 | router: service(), 10 | 11 | layout, 12 | 13 | classNames: [ "game-item-component" ], 14 | 15 | click() { 16 | this.router.transitionTo( "games.game", this.content.id ); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/app/ui/components/list/game-item/template.hbs: -------------------------------------------------------------------------------- 1 | {{preview-image src=content.box_art_url.latest}} 2 |
    {{content.name}}
    -------------------------------------------------------------------------------- /src/app/ui/components/list/headline-totals/component.js: -------------------------------------------------------------------------------- 1 | import Component from "@ember/component"; 2 | import { gte } from "@ember/object/computed"; 3 | import layout from "./template.hbs"; 4 | 5 | 6 | export default Component.extend({ 7 | layout, 8 | 9 | tagName: "div", 10 | classNames: [ "total" ], 11 | 12 | total: null, 13 | 14 | isVisible: gte( "total", 0 ) 15 | 16 | }).reopenClass({ 17 | positionalParams: [ "total" ] 18 | }); 19 | -------------------------------------------------------------------------------- /src/app/ui/components/list/headline-totals/template.hbs: -------------------------------------------------------------------------------- 1 | ({{total}}) -------------------------------------------------------------------------------- /src/app/ui/components/list/infinite-scroll/styles.less: -------------------------------------------------------------------------------- 1 | 2 | .form-button-component.infinite-scroll-component { 3 | display: block; 4 | min-width: 8em; 5 | height: auto; 6 | margin: 2em auto; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/ui/components/list/infinite-scroll/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if fetchError}} 2 | {{t "components.infinite-scroll.error"}} 3 | {{else if isLocked}} 4 | {{loading-spinner}}{{t "components.infinite-scroll.loading"}} 5 | {{else}} 6 | {{t "components.infinite-scroll.fetch"}} 7 | {{/if}} -------------------------------------------------------------------------------- /src/app/ui/components/list/settings-channel-item/component.js: -------------------------------------------------------------------------------- 1 | import { get, set } from "@ember/object"; 2 | import ListItemComponent from "../-list-item/component"; 3 | import layout from "./template.hbs"; 4 | import "./styles.less"; 5 | 6 | 7 | export default ListItemComponent.extend({ 8 | layout, 9 | 10 | classNames: [ "settings-channel-item-component" ], 11 | 12 | dialog: false, 13 | 14 | actions: { 15 | eraseDialog() { 16 | set( this, "dialog", true ); 17 | }, 18 | 19 | confirm() { 20 | get( this, "erase" )(); 21 | }, 22 | 23 | decline() { 24 | set( this, "dialog", false ); 25 | } 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/ui/components/list/team-item/component.js: -------------------------------------------------------------------------------- 1 | import ListItemComponent from "../-list-item/component"; 2 | import layout from "./template.hbs"; 3 | import "./styles.less"; 4 | 5 | 6 | export default ListItemComponent.extend({ 7 | layout, 8 | 9 | classNames: [ "team-item-component" ] 10 | }); 11 | -------------------------------------------------------------------------------- /src/app/ui/components/list/team-item/styles.less: -------------------------------------------------------------------------------- 1 | @import (reference) "ui/styles/config"; 2 | @import (reference) "ui/styles/mixins"; 3 | 4 | 5 | .team-item-component { 6 | @logo: 6.5rem; 7 | 8 | min-height: 91px; 9 | margin-bottom: 1.5rem; 10 | .flexbox(); 11 | .dynamic-elems-per-row( 2, @content-width, 4%, @additional-width ); 12 | 13 | > .logo { 14 | display: block; 15 | position: relative; 16 | width: @logo; 17 | height: @logo; 18 | cursor: pointer; 19 | } 20 | 21 | > .info { 22 | flex-grow: 1; 23 | width: 0; 24 | margin: 0 .5rem; 25 | 26 | > header { 27 | margin: 0 0 .5rem; 28 | .text-overflow(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/ui/components/list/team-item/template.hbs: -------------------------------------------------------------------------------- 1 | {{#link-to "team" content.id tagName="div" class="logo"}} 2 | {{preview-image src=content.thumbnail_url title=content.title}} 3 | {{/link-to}} 4 |
    5 |
    {{#link-to "team" content.id}}{{content.title}}{{/link-to}}
    6 |
    {{t "components.team-item.created-at.format" created_at=content.created_at}}
    7 |
    {{t "components.team-item.updated-at.format" updated_at=content.updated_at}}
    8 |
    -------------------------------------------------------------------------------- /src/app/ui/components/loading-spinner/component.js: -------------------------------------------------------------------------------- 1 | import Component from "@ember/component"; 2 | import { on } from "@ember/object/evented"; 3 | import layout from "./template.hbs"; 4 | import "./styles.less"; 5 | 6 | 7 | export default Component.extend({ 8 | layout, 9 | 10 | tagName: "svg", 11 | attributeBindings: [ "viewBox" ], 12 | classNames: [ "loading-spinner-component" ], 13 | 14 | viewBox: "0 0 1 1", 15 | 16 | _setRadiusAttribute: on( "didInsertElement", function() { 17 | let circle = this.element.querySelector( "circle" ); 18 | let strokeWidth = window.getComputedStyle( circle ).strokeWidth; 19 | let radius = 50 - parseFloat( strokeWidth ); 20 | circle.setAttribute( "r", `${radius}%` ); 21 | }) 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/ui/components/loading-spinner/template.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/ui/components/modal/modal-body/component.js: -------------------------------------------------------------------------------- 1 | import Component from "@ember/component"; 2 | 3 | 4 | export default Component.extend({ 5 | tagName: "section", 6 | classNames: [ "modal-body-component" ] 7 | }); 8 | -------------------------------------------------------------------------------- /src/app/ui/components/modal/modal-changelog/template.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{#modal-header}} 3 | {{t "modal.changelog.header" version=modalContext.version}} 4 | {{/modal-header}} 5 | {{#modal-body}} 6 | {{t "modal.changelog.body"}} 7 | {{/modal-body}} 8 | {{#modal-footer classNames="button-list-horizontal"}} 9 | {{#form-button 10 | action=(action "close") 11 | classNames="btn-danger" 12 | icon="fa-times" 13 | }} 14 | {{t "modal.dialog.action.close"}} 15 | {{/form-button}} 16 | {{#form-button 17 | action=(action "showChangelog") 18 | classNames="btn-success" 19 | icon="fa-list-alt" 20 | iconanim=true 21 | }} 22 | {{t "modal.changelog.action.show"}} 23 | {{/form-button}} 24 | {{/modal-footer}} 25 |
    -------------------------------------------------------------------------------- /src/app/ui/components/modal/modal-confirm/component.js: -------------------------------------------------------------------------------- 1 | import ModalDialogComponent from "../modal-dialog/component"; 2 | import layout from "./template.hbs"; 3 | 4 | 5 | function actionFactory( action ) { 6 | return function( success, failure ) { 7 | this.modalContext.send( action, success, failure ); 8 | }; 9 | } 10 | 11 | 12 | export default ModalDialogComponent.extend({ 13 | layout, 14 | 15 | "class": "modal-confirm", 16 | 17 | hotkeysNamespace: "modalconfirm", 18 | hotkeys: { 19 | confirm: "apply" 20 | }, 21 | 22 | 23 | actions: { 24 | "apply" : actionFactory( "apply" ), 25 | "discard": actionFactory( "discard" ), 26 | "cancel" : actionFactory( "cancel" ) 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /src/app/ui/components/modal/modal-debug/component.js: -------------------------------------------------------------------------------- 1 | import ModalDialogComponent from "../modal-dialog/component"; 2 | import layout from "./template.hbs"; 3 | 4 | 5 | export default ModalDialogComponent.extend({ 6 | layout, 7 | classNames: [ "modal-debug-component" ] 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/ui/components/modal/modal-debug/template.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{#modal-header}} 3 | {{t "modal.debug.header" version=modalContext.buildVersion}} 4 | {{/modal-header}} 5 | {{#modal-body}} 6 | {{t "modal.debug.body" name=modalContext.displayName htmlSafe=true}} 7 | {{/modal-body}} 8 | {{#modal-footer classNames="button-list-horizontal"}} 9 | {{#form-button 10 | action=(action "close") 11 | classNames="btn-primary" 12 | icon="fa-arrow-left" 13 | }} 14 | {{t "modal.debug.action.close"}} 15 | {{/form-button}} 16 | {{/modal-footer}} 17 |
    -------------------------------------------------------------------------------- /src/app/ui/components/modal/modal-dialog/template.hbs: -------------------------------------------------------------------------------- 1 |
    {{yield}}
    -------------------------------------------------------------------------------- /src/app/ui/components/modal/modal-firstrun/component.js: -------------------------------------------------------------------------------- 1 | import { inject as service } from "@ember/service"; 2 | import { main as mainConfig } from "config"; 3 | import ModalDialogComponent from "../modal-dialog/component"; 4 | import layout from "./template.hbs"; 5 | 6 | 7 | export default ModalDialogComponent.extend( /** @class ModalFirstrunComponent */ { 8 | /** @type {RouterService} */ 9 | router: service(), 10 | 11 | layout, 12 | classNames: [ "modal-firstrun-component" ], 13 | 14 | name: mainConfig[ "display-name" ], 15 | 16 | 17 | actions: { 18 | /** @this {ModalFirstrunComponent} */ 19 | settings() { 20 | this.router.transitionTo( "settings" ); 21 | this.send( "close" ); 22 | } 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/ui/components/modal/modal-firstrun/template.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{#modal-header}} 3 | {{name}} 4 | {{/modal-header}} 5 | {{#modal-body}} 6 | {{t "modal.firstrun.body" name=name htmlSafe=true}} 7 | {{/modal-body}} 8 | {{#modal-footer classNames="button-list-horizontal"}} 9 | {{#form-button 10 | action=(action "close") 11 | classNames="btn-primary" 12 | icon="fa-arrow-left" 13 | }} 14 | {{t "modal.firstrun.action.start"}} 15 | {{/form-button}} 16 | {{#form-button 17 | action=(action "settings") 18 | classNames="btn-success" 19 | icon="fa-cog" 20 | }} 21 | {{t "modal.firstrun.action.settings"}} 22 | {{/form-button}} 23 | {{/modal-footer}} 24 |
    -------------------------------------------------------------------------------- /src/app/ui/components/modal/modal-footer/component.js: -------------------------------------------------------------------------------- 1 | import Component from "@ember/component"; 2 | 3 | 4 | export default Component.extend({ 5 | tagName: "footer", 6 | classNames: [ "modal-footer-component" ] 7 | }); 8 | -------------------------------------------------------------------------------- /src/app/ui/components/modal/modal-header/component.js: -------------------------------------------------------------------------------- 1 | import Component from "@ember/component"; 2 | 3 | 4 | export default Component.extend({ 5 | tagName: "header", 6 | classNames: [ "modal-header-component" ] 7 | }); 8 | -------------------------------------------------------------------------------- /src/app/ui/components/modal/modal-log/component.js: -------------------------------------------------------------------------------- 1 | import Component from "@ember/component"; 2 | import { computed, observer } from "@ember/object"; 3 | import { on } from "@ember/object/evented"; 4 | import { scheduleOnce } from "@ember/runloop"; 5 | import layout from "./template.hbs"; 6 | 7 | 8 | export default Component.extend({ 9 | layout, 10 | 11 | tagName: "section", 12 | classNames: [ "modal-log-component" ], 13 | 14 | log: computed(function() { 15 | return []; 16 | }), 17 | 18 | _logObserver: observer( "log.[]", function() { 19 | scheduleOnce( "afterRender", () => this.scrollToBottom() ); 20 | }), 21 | 22 | scrollToBottom: on( "didInsertElement", function() { 23 | const elem = this.element; 24 | if ( !elem ) { return; } 25 | elem.scrollTop = Math.max( 0, elem.scrollHeight - elem.clientHeight ); 26 | }) 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/ui/components/modal/modal-log/template.hbs: -------------------------------------------------------------------------------- 1 | {{#selectable-text tagName="ul" class="list-unstyled"}} 2 | {{#each log as |line|}} 3 |
  • {{line.line}}
  • 4 | {{/each}} 5 | {{/selectable-text}} -------------------------------------------------------------------------------- /src/app/ui/components/modal/modal-newrelease/template.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{#modal-header}} 3 | {{t "modal.newrelease.header" version=modalContext.version}} 4 | {{/modal-header}} 5 | {{#modal-body}} 6 | {{t "modal.newrelease.body" version=modalContext.release.version}} 7 | {{/modal-body}} 8 | {{#modal-footer classNames="button-list-horizontal"}} 9 | {{#form-button 10 | action=(action "download") 11 | classNames="btn-success" 12 | icon="fa-download" 13 | iconanim=true 14 | }} 15 | {{t "modal.newrelease.action.download"}} 16 | {{/form-button}} 17 | {{#form-button 18 | action=(action "ignore") 19 | classNames="btn-danger" 20 | icon="fa-times" 21 | }} 22 | {{t "modal.newrelease.action.ignore"}} 23 | {{/form-button}} 24 | {{/modal-footer}} 25 |
    -------------------------------------------------------------------------------- /src/app/ui/components/modal/modal-quit/component.js: -------------------------------------------------------------------------------- 1 | import { readOnly } from "@ember/object/computed"; 2 | import { inject as service } from "@ember/service"; 3 | import ModalDialogComponent from "../modal-dialog/component"; 4 | import layout from "./template.hbs"; 5 | 6 | 7 | export default ModalDialogComponent.extend({ 8 | nwjs: service(), 9 | streaming: service(), 10 | 11 | layout, 12 | 13 | classNames: [ "modal-quit-component" ], 14 | 15 | hasStreams: readOnly( "streaming.hasStreams" ), 16 | 17 | hotkeysNamespace: "modalquit", 18 | hotkeys: { 19 | shutdown: "shutdown" 20 | }, 21 | 22 | actions: { 23 | shutdown() { 24 | this.streaming.killAll(); 25 | this.send( "quit" ); 26 | }, 27 | 28 | quit() { 29 | this.nwjs.quit(); 30 | } 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /src/app/ui/components/modal/modal-service/component.js: -------------------------------------------------------------------------------- 1 | import Component from "@ember/component"; 2 | import { inject as service } from "@ember/service"; 3 | import layout from "./template.hbs"; 4 | import "./styles.less"; 5 | 6 | 7 | export default Component.extend({ 8 | /** @type {ModalService} */ 9 | modal: service(), 10 | 11 | layout, 12 | classNames: "modal-service-component", 13 | classNameBindings: [ "modal.isModalOpened:active" ] 14 | }); 15 | -------------------------------------------------------------------------------- /src/app/ui/components/modal/modal-service/styles.less: -------------------------------------------------------------------------------- 1 | .modal-service-component { 2 | @duration: .333s; 3 | 4 | display: flex; 5 | align-content: center; 6 | justify-content: center; 7 | align-items: center; 8 | position: fixed; 9 | left: 0; 10 | right: 0; 11 | top: 0; 12 | bottom: 0; 13 | z-index: 10000; 14 | background: fade( #000, 50% ); 15 | 16 | opacity: 0; 17 | visibility: hidden; 18 | // active -> inactive: toggle visibility after the opacity transition has finished 19 | transition: opacity @duration ease-out 0s, visibility 0s linear @duration; 20 | 21 | &.active { 22 | opacity: 1; 23 | visibility: visible; 24 | // inactive -> active: toggle visibility immediately 25 | transition: opacity @duration ease-out 0s; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/ui/components/modal/modal-service/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if modal.isModalOpened}} 2 | {{#with modal.modals.lastObject as |modal|}} 3 | {{component 4 | (concat "modal-" modal.name) 5 | modalName=modal.name 6 | modalContext=modal.context 7 | }} 8 | {{/with}} 9 | {{/if}} -------------------------------------------------------------------------------- /src/app/ui/components/preview-image/styles.less: -------------------------------------------------------------------------------- 1 | @import (reference) "ui/styles/config"; 2 | @import (reference) "font-awesome/less/variables.less"; 3 | 4 | 5 | .previewImage, 6 | .previewError { 7 | display: block; 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | width: 100%; 12 | height: 100%; 13 | margin: 0; 14 | } 15 | 16 | .previewError { 17 | background: @color-twitch-purple; 18 | 19 | &::before { 20 | content: @fa-var-twitch; 21 | display: block; 22 | position: absolute; 23 | top: 50%; 24 | left: 0; 25 | right: 0; 26 | font: 4.5em/.1 FontAwesome; 27 | color: #fff !important; 28 | text-align: center; 29 | opacity: 1; 30 | transition: opacity .333s ease-out; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/ui/components/preview-image/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if error}} 2 |
    3 | {{else}} 4 | 5 | {{/if}} -------------------------------------------------------------------------------- /src/app/ui/components/quick/quick-bar/template.hbs: -------------------------------------------------------------------------------- 1 | {{form-button action=(action "menuClick") class="btn-open" icon=(if isLocked "fa-ellipsis-v" "fa-ellipsis-h")}} 2 | -------------------------------------------------------------------------------- /src/app/ui/components/settings-hotkey/styles.less: -------------------------------------------------------------------------------- 1 | .settings-hotkey-component { 2 | display: flex; 3 | 4 | > * { 5 | @height: 2.25rem; 6 | 7 | flex: 0 0 @height; 8 | height: @height !important; 9 | } 10 | > input { 11 | flex-grow: 1; 12 | 13 | &.is-empty { 14 | font-style: italic; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/ui/components/settings-submit/styles.less: -------------------------------------------------------------------------------- 1 | @import (reference) "ui/styles/config"; 2 | 3 | 4 | .settings-submit-component { 5 | display: flex; 6 | justify-content: center; 7 | padding-bottom: 2rem; 8 | overflow: hidden; 9 | 10 | > .form-button-component { 11 | margin: 0 .5rem; 12 | transition: opacity @anim-content-duration ease-out, transform @anim-content-duration ease-out; 13 | } 14 | 15 | &.faded > .form-button-component { 16 | opacity: 0; 17 | transform: translateY( 3rem ); 18 | -webkit-animation: settings-submit-component-buttons @anim-content-duration 1 ease-out alternate both; 19 | } 20 | } 21 | 22 | @-webkit-keyframes settings-submit-component-buttons { 23 | 0%, 99.9999% { 24 | visibility: visible; 25 | } 26 | 100% { 27 | visibility: hidden; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/ui/components/settings-submit/template.hbs: -------------------------------------------------------------------------------- 1 | {{#form-button 2 | action=apply 3 | classNames="btn-success" 4 | icon="fa-check" 5 | iconanim=true 6 | }} 7 | {{t "components.settings-submit.apply"}} 8 | {{/form-button}} 9 | {{#form-button 10 | action=discard 11 | classNames="btn-danger" 12 | icon="fa-trash-o" 13 | iconanim=true 14 | }} 15 | {{t "components.settings-submit.discard"}} 16 | {{/form-button}} -------------------------------------------------------------------------------- /src/app/ui/components/stream/stats-row/component.js: -------------------------------------------------------------------------------- 1 | import Component from "@ember/component"; 2 | import layout from "./template.hbs"; 3 | import "./styles.less"; 4 | 5 | 6 | export default Component.extend({ 7 | layout, 8 | 9 | tagName: "div", 10 | classNameBindings: [ ":stats-row-component", "class" ], 11 | 12 | /** @type {TwitchStream} */ 13 | stream: null, 14 | /** @type {boolean} */ 15 | withFlag: true 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/ui/components/stream/stats-row/styles.less: -------------------------------------------------------------------------------- 1 | 2 | .stats-row-component { 3 | display: flex; 4 | justify-content: flex-start; 5 | 6 | span:not(.stats-row-item-flag) { 7 | width: 6rem; 8 | overflow: hidden; 9 | white-space: nowrap; 10 | } 11 | 12 | span.stats-row-item-flag { 13 | width: 2rem; 14 | } 15 | 16 | i { 17 | margin-right: .25em; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/ui/components/stream/stats-row/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if withFlag}} 2 | {{#if stream.hasBroadcasterLanguage}} 3 | {{flag-icon type="broadcaster" lang=stream.channel.broadcaster_language}} 4 | {{/if}} 5 | {{/if}} 6 | {{hours-from-now stream.started_at interval=60000}} 7 | {{format-viewers stream.viewer_count}} -------------------------------------------------------------------------------- /src/app/ui/components/stream/stream-presentation/component.js: -------------------------------------------------------------------------------- 1 | import Component from "@ember/component"; 2 | import layout from "./template.hbs"; 3 | import "./styles.less"; 4 | 5 | 6 | export default Component.extend({ 7 | layout, 8 | 9 | tagName: "section", 10 | classNameBindings: [ ":stream-presentation-component", "class" ], 11 | "class": "", 12 | 13 | clickablePreview: true 14 | }); 15 | -------------------------------------------------------------------------------- /src/app/ui/components/stream/stream-presentation/styles.less: -------------------------------------------------------------------------------- 1 | 2 | .stream-presentation-component { 3 | display: flex; 4 | flex-flow: row nowrap; 5 | justify-content: space-between; 6 | 7 | > .image { 8 | position: relative; 9 | width: 59%; 10 | } 11 | 12 | > .info { 13 | width: 38.5%; 14 | 15 | > .title { 16 | margin-top: 0; 17 | overflow: hidden; 18 | text-overflow: ellipsis; 19 | white-space: nowrap; 20 | 21 | > .flag-icon-component { 22 | margin-right: .25em; 23 | } 24 | 25 | > a { 26 | text-decoration: none; 27 | } 28 | } 29 | 30 | > .game { 31 | > i.fa-gamepad { 32 | margin-right: .333em; 33 | } 34 | } 35 | 36 | > .stats-row-component { 37 | max-width: 20em; 38 | margin-top: .5em; 39 | } 40 | 41 | > .status { 42 | margin: .5em 0; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/ui/components/stream/stream-presentation/template.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{stream-preview-image 3 | stream=stream 4 | user=stream.user 5 | clickable=clickablePreview 6 | src=stream.thumbnail_url.latest 7 | noContextmenu=(bool-not clickablePreview) 8 | }} 9 |
    10 |
    11 |

    {{#if stream.hasLanguage}}{{flag-icon type="channel" lang=stream.language}}{{/if}}{{#link-to "channel" stream.id}}{{stream.user.detailedName}}{{/link-to}}

    12 | {{#if stream.game_id}}
    {{#link-to "games.game" stream.game_id}}{{stream.game_name}}{{/link-to}}
    {{/if}} 13 | {{stats-row stream=stream}} 14 | {{embedded-links class="status" text=stream.title}} 15 | {{#if hasBlock}} 16 |
    17 | {{yield}} 18 |
    19 | {{/if}} 20 |
    -------------------------------------------------------------------------------- /src/app/ui/components/stream/stream-preview-image/template.hbs: -------------------------------------------------------------------------------- 1 | {{preview-image src=src}} 2 | {{#if hasBlock}}{{yield}}{{/if}} -------------------------------------------------------------------------------- /src/app/ui/components/sub-menu/component.js: -------------------------------------------------------------------------------- 1 | import Component from "@ember/component"; 2 | import layout from "./template.hbs"; 3 | import "./styles.less"; 4 | 5 | 6 | export default Component.extend({ 7 | layout, 8 | 9 | tagName: "ul", 10 | classNames: [ 11 | "sub-menu-component" 12 | ], 13 | 14 | baseroute: null, 15 | menus: null 16 | 17 | }).reopenClass({ 18 | positionalParams: [ 19 | "baseroute", 20 | "menus" 21 | ] 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/ui/components/sub-menu/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if hasBlock}} 2 | {{#each-in menus as |subroute name|}} 3 | {{yield subroute name}} 4 | {{/each-in}} 5 | {{else}} 6 | {{#each-in menus as |subroute name|}} 7 | {{#link-to (concat baseroute "." subroute) tagName="li"}}{{name}}{{/link-to}} 8 | {{/each-in}} 9 | {{/if}} -------------------------------------------------------------------------------- /src/app/ui/routes/-mixins/controllers/retry-transition.js: -------------------------------------------------------------------------------- 1 | import { get, set } from "@ember/object"; 2 | import Mixin from "@ember/object/mixin"; 3 | 4 | 5 | export default Mixin.create({ 6 | /** 7 | * Retry a previously stored transition 8 | * @param {string?} route 9 | * @returns {Promise} 10 | */ 11 | retryTransition( route ) { 12 | const transition = get( this, "previousTransition" ); 13 | 14 | if ( !transition ) { 15 | return route 16 | ? this.transitionToRoute( route ) 17 | : Promise.resolve(); 18 | } 19 | 20 | set( this, "previousTransition", null ); 21 | return transition.retry(); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/ui/routes/-mixins/routes/infinite-scroll/record-array.js: -------------------------------------------------------------------------------- 1 | import { get, set } from "@ember/object"; 2 | 3 | 4 | function factory( fn ) { 5 | /** 6 | * @param {(DS.RecordArray|Ember.MutableArray)} enumerable 7 | * @param {...*} args 8 | * @returns {Promise} 9 | */ 10 | return async function( arr, ...args ) { 11 | const meta = get( arr, "meta" ); 12 | 13 | arr = await Promise.all( arr[ fn ]( ...args ) ); 14 | arr = arr.filter( Boolean ); 15 | 16 | /* istanbul ignore else */ 17 | if ( meta ) { 18 | set( arr, "meta", meta ); 19 | } 20 | 21 | return arr; 22 | }; 23 | } 24 | 25 | 26 | export const toArray = factory( "toArray" ); 27 | export const map = factory( "map" ); 28 | -------------------------------------------------------------------------------- /src/app/ui/routes/about/controller.js: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import { main as mainConfig, locales as localesConfig } from "config"; 3 | import metadata from "metadata"; 4 | import { manifest } from "nwjs/App"; 5 | import { platform, release, arch } from "utils/node/platform"; 6 | import "./styles.less"; 7 | import process from "process"; 8 | 9 | 10 | export default Controller.extend({ 11 | mainConfig, 12 | localesConfig, 13 | metadata, 14 | nwjsVersion: process.versions[ "nw" ], 15 | platform, 16 | release, 17 | arch, 18 | releaseUrl: mainConfig.urls.release.replace( "{version}", manifest.version ) 19 | }); 20 | -------------------------------------------------------------------------------- /src/app/ui/routes/about/styles.less: -------------------------------------------------------------------------------- 1 | @import (reference) "ui/styles/mixins"; 2 | 3 | 4 | .content-about { 5 | @width: 15rem; 6 | @margin-data: ( @width + 1 ); 7 | 8 | article { 9 | .clearfix(); 10 | } 11 | 12 | h3 > i.fa { 13 | margin-right: .333em; 14 | } 15 | 16 | .dl-horizontal { 17 | > dt { 18 | width: @width; 19 | } 20 | > dd { 21 | margin-left: @margin-data; 22 | } 23 | } 24 | 25 | .text-donation { 26 | width: 24rem; 27 | margin-left: @margin-data; 28 | } 29 | 30 | .coinaddress { 31 | display: inline-block; 32 | margin-left: 1em; 33 | font-size: smaller; 34 | font-style: italic; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/ui/routes/application/controller.js: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import "bootstrap/dist/css/bootstrap.css"; 3 | import "./styles/index.less"; 4 | 5 | 6 | export default Controller; 7 | -------------------------------------------------------------------------------- /src/app/ui/routes/application/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | import { inject as service } from "@ember/service"; 3 | 4 | 5 | export default Route.extend({ 6 | /** @type {AuthService} */ 7 | auth: service(), 8 | /** @type {VersioncheckService} */ 9 | versioncheck: service(), 10 | 11 | init() { 12 | this._super( ...arguments ); 13 | this.auth.autoLogin(); 14 | this.versioncheck.check(); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/ui/routes/application/styles/content.less: -------------------------------------------------------------------------------- 1 | 2 | main.content { 3 | flex: 1; 4 | padding-right: ( @app-padding - @scrollbar-size ); 5 | overflow-y: scroll; 6 | 7 | > h2, 8 | > header > h2 { 9 | height: 1.5em; 10 | margin: 0; 11 | overflow: hidden; 12 | font-weight: lighter; 13 | white-space: nowrap; 14 | text-overflow: ellipsis; 15 | } 16 | 17 | > header { 18 | display: flex; 19 | justify-content: space-between; 20 | 21 | > div.total { 22 | flex-shrink: 0; 23 | flex-grow: 1; 24 | margin-left: .5em; 25 | line-height: 2.5; 26 | 27 | .theme({ 28 | .theme-mix-color( @theme-headline-sub-color, @theme-background ); 29 | }); 30 | } 31 | } 32 | 33 | &:not( .content-loading ) { 34 | animation: animFadeInRight @anim-content-duration ease-out; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/ui/routes/application/styles/fixes.less: -------------------------------------------------------------------------------- 1 | 2 | button { 3 | border: 0; 4 | border-radius: 0; 5 | background: transparent; 6 | } 7 | 8 | 9 | .input-group .form-control:first-child { 10 | border-right-width: 0 !important; 11 | } 12 | 13 | 14 | .input-group-btn { 15 | font-size: inherit; 16 | } 17 | 18 | 19 | // fix the weird size of the gamepad icon 20 | i.fa.fa-gamepad { 21 | font-size: 1.2em; 22 | } 23 | 24 | // fix alignment of film icon 25 | i.fa.fa-film { 26 | position: relative; 27 | top: -.05em; 28 | } 29 | 30 | // fix size of credit-card icon in buttons 31 | .form-button-component.icon > i.fa.fa-credit-card { 32 | transform: scaleX(.9) translateX(-.025em); 33 | } 34 | -------------------------------------------------------------------------------- /src/app/ui/routes/application/styles/fontawesome.less: -------------------------------------------------------------------------------- 1 | @import "font-awesome/less/variables.less"; 2 | @import "font-awesome/less/mixins.less"; 3 | @import "font-awesome/less/core.less"; 4 | @import "font-awesome/less/larger.less"; 5 | @import "font-awesome/less/fixed-width.less"; 6 | @import "font-awesome/less/list.less"; 7 | //@import "font-awesome/less/bordered-pulled.less"; 8 | //@import "font-awesome/less/animated.less"; 9 | //@import "font-awesome/less/rotated-flipped.less"; 10 | //@import "font-awesome/less/stacked.less"; 11 | @import "font-awesome/less/icons.less"; 12 | 13 | 14 | @font-face { 15 | font-family: "FontAwesome"; 16 | src: url( "font-awesome/fonts/fontawesome-webfont.woff2" ) format( "woff2" ); 17 | font-weight: normal; 18 | font-style: normal; 19 | } 20 | -------------------------------------------------------------------------------- /src/app/ui/routes/application/styles/index.less: -------------------------------------------------------------------------------- 1 | @import "ui/styles/config"; 2 | @import "ui/styles/mixins"; 3 | @import "ui/styles/themes"; 4 | 5 | @import "fonts"; 6 | @import "fontawesome"; 7 | @import "animations"; 8 | @import "scrollbars"; 9 | @import "main"; 10 | @import "content"; 11 | @import "form"; 12 | @import "fixes"; 13 | -------------------------------------------------------------------------------- /src/app/ui/routes/application/styles/scrollbars.less: -------------------------------------------------------------------------------- 1 | 2 | ::-webkit-scrollbar { 3 | width: @scrollbar-size; 4 | height: @scrollbar-size; 5 | } 6 | 7 | ::-webkit-scrollbar-track { 8 | background: transparent; 9 | } 10 | 11 | ::-webkit-scrollbar-thumb { 12 | border: ( ( @scrollbar-size - @scrollbar-thumb-size ) / 2 ) solid transparent; 13 | background-clip: content-box !important; 14 | 15 | .theme({ 16 | background: @theme-scrollbar-background; 17 | 18 | &:hover, 19 | &:active { 20 | background: @theme-scrollbar-background-hover; 21 | } 22 | }); 23 | } 24 | 25 | main.content::-webkit-scrollbar-thumb:vertical { 26 | border-top-width: 0; 27 | } 28 | -------------------------------------------------------------------------------- /src/app/ui/routes/application/template.hbs: -------------------------------------------------------------------------------- 1 | {{title-bar}} 2 |
    3 | {{main-menu}} 4 | {{outlet}} 5 |
    6 | {{modal-service}} -------------------------------------------------------------------------------- /src/app/ui/routes/channel/controller.js: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import { alias } from "@ember/object/computed"; 3 | import "./styles.less"; 4 | 5 | 6 | export default Controller.extend({ 7 | /** @type {TwitchStream} */ 8 | stream : alias( "model.stream" ), 9 | /** @type {TwitchUser} */ 10 | user : alias( "model.user" ), 11 | /** @type {TwitchChannel} */ 12 | channel: alias( "model.channel" ) 13 | }); 14 | -------------------------------------------------------------------------------- /src/app/ui/routes/channel/index/route.js: -------------------------------------------------------------------------------- 1 | import { getOwner } from "@ember/application"; 2 | import UserIndexRoute from "ui/routes/user/index/route"; 3 | 4 | 5 | export default UserIndexRoute.extend({ 6 | async model() { 7 | return this.modelFor( "channel" ); 8 | }, 9 | 10 | refresh() { 11 | return getOwner( this ).lookup( "route:channel" ).refresh(); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /src/app/ui/routes/channel/loading/route.js: -------------------------------------------------------------------------------- 1 | export { default } from "ui/routes/loading/route"; 2 | -------------------------------------------------------------------------------- /src/app/ui/routes/channel/teams/route.js: -------------------------------------------------------------------------------- 1 | import UserIndexRoute from "ui/routes/user/index/route"; 2 | import PaginationMixin from "ui/routes/-mixins/routes/infinite-scroll/pagination"; 3 | 4 | 5 | export default UserIndexRoute.extend( PaginationMixin, { 6 | itemSelector: ".team-item-component", 7 | modelName: "twitch-team", 8 | modelPreload: "thumbnail_url", 9 | 10 | model() { 11 | const { user: { id: broadcaster_id } } = this.modelFor( "channel" ); 12 | 13 | return this._super({ broadcaster_id }); 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/ui/routes/channel/teams/template.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{#content-list model as |item isNewItem isDuplicateItem|}} 3 | {{team-item 4 | content=item 5 | isNewItem=isNewItem 6 | isDuplicateItem=isDuplicateItem 7 | }} 8 | {{else}} 9 |

    {{t "routes.channel.teams.empty"}}

    10 | {{/content-list}} 11 |
    -------------------------------------------------------------------------------- /src/app/ui/routes/error/controller.js: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import "./styles.less"; 3 | 4 | 5 | export default Controller; 6 | -------------------------------------------------------------------------------- /src/app/ui/routes/error/styles.less: -------------------------------------------------------------------------------- 1 | 2 | .content-error { 3 | @width: 8em; 4 | @margin: .25em; 5 | 6 | h4 { 7 | font-size: 1.2em; 8 | } 9 | 10 | dt { 11 | width: @width; 12 | margin-bottom: @margin; 13 | text-align: left; 14 | } 15 | 16 | dd { 17 | margin: 0 0 @margin @width; 18 | } 19 | 20 | .stack { 21 | font-family: monospace; 22 | white-space: pre-wrap; 23 | 24 | &::before, 25 | &::after { 26 | display: none; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/ui/routes/error/template.hbs: -------------------------------------------------------------------------------- 1 | {{#selectable-text tagName="main" class="content content-error"}} 2 |

    {{t "routes.error.header"}}

    3 |

    {{t "routes.error.text"}}

    4 |
    5 | {{#each model as |row|}} 6 |
    {{row.key}}
    7 |
    {{row.value}}
    8 | {{/each}} 9 |
    10 | {{/selectable-text}} -------------------------------------------------------------------------------- /src/app/ui/routes/games/game/template.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    {{game.name}}

    4 | {{#quick-bar}} 5 | {{quick-bar-homepage}} 6 | {{/quick-bar}} 7 |
    8 | {{#content-list model as |item isNewItem isDuplicateItem|}} 9 | {{stream-item 10 | content=item 11 | isNewItem=isNewItem 12 | isDuplicateItem=isDuplicateItem 13 | showGame=false 14 | }} 15 | {{else}} 16 |

    {{t "routes.game.empty"}}

    17 | {{/content-list}} 18 |
    -------------------------------------------------------------------------------- /src/app/ui/routes/games/index/route.js: -------------------------------------------------------------------------------- 1 | import UserIndexRoute from "ui/routes/user/index/route"; 2 | import PaginationMixin from "ui/routes/-mixins/routes/infinite-scroll/pagination"; 3 | import RefreshRouteMixin from "ui/routes/-mixins/routes/refresh"; 4 | 5 | 6 | export default UserIndexRoute.extend( PaginationMixin, RefreshRouteMixin, { 7 | itemSelector: ".game-item-component", 8 | modelName: "twitch-game-top", 9 | modelMapBy: "game", 10 | modelPreload: "box_art_url.latest" 11 | }); 12 | -------------------------------------------------------------------------------- /src/app/ui/routes/games/index/template.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    {{t "routes.games.header"}}

    4 | {{#quick-bar}} 5 | {{quick-bar-homepage}} 6 | {{/quick-bar}} 7 |
    8 | {{#content-list model as |item isNewItem isDuplicateItem|}} 9 | {{game-item 10 | content=item 11 | isNewItem=isNewItem 12 | isDuplicateItem=isDuplicateItem 13 | }} 14 | {{else}} 15 |

    {{t "routes.games.empty"}}

    16 | {{/content-list}} 17 |
    -------------------------------------------------------------------------------- /src/app/ui/routes/games/loading/route.js: -------------------------------------------------------------------------------- 1 | export { default } from "ui/routes/loading/route"; 2 | -------------------------------------------------------------------------------- /src/app/ui/routes/index/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | 4 | export default Route.extend({ 5 | beforeModel( transition ) { 6 | // access to this route is restricted 7 | // but don't block the initial transition 8 | if ( transition.sequence > 0 ) { 9 | transition.abort(); 10 | } 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/ui/routes/loading/controller.js: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import "./styles.less"; 3 | 4 | 5 | export default Controller; 6 | -------------------------------------------------------------------------------- /src/app/ui/routes/loading/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | 4 | export default Route.extend({ 5 | // override automatically generated templateName for all routes which import LoadingRoute 6 | templateName: "loading" 7 | }); 8 | -------------------------------------------------------------------------------- /src/app/ui/routes/loading/styles.less: -------------------------------------------------------------------------------- 1 | @import (reference) "ui/styles/config"; 2 | 3 | 4 | main.content.content-loading { 5 | @size: 6rem; 6 | 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | padding: 0; 11 | overflow: hidden; 12 | 13 | > .loading-spinner-component { 14 | position: relative; 15 | width: @size; 16 | height: @size; 17 | 18 | > circle { 19 | stroke: @color-primary; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/ui/routes/loading/template.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{loading-spinner}} 3 |
    -------------------------------------------------------------------------------- /src/app/ui/routes/search/controller.js: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import { computed } from "@ember/object"; 3 | import { alias, empty, equal } from "@ember/object/computed"; 4 | import "./styles.less"; 5 | 6 | 7 | export default Controller.extend({ 8 | queryParams: [ "filter", "query" ], 9 | 10 | games : alias( "model.games" ), 11 | channels: alias( "model.channels" ), 12 | 13 | notFiltered: equal( "filter", "all" ), 14 | 15 | emptyGames : empty( "games" ), 16 | emptyChannels: empty( "channels" ), 17 | 18 | noResults: computed( "emptyGames", "emptyChannels", function() { 19 | return this.emptyGames && this.emptyChannels; 20 | }) 21 | }); 22 | -------------------------------------------------------------------------------- /src/app/ui/routes/search/styles.less: -------------------------------------------------------------------------------- 1 | 2 | .content-search { 3 | h2 > span { 4 | padding-left: .25em; 5 | } 6 | 7 | h3 { 8 | margin-top: 0; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/ui/routes/settings/-submenu/route.js: -------------------------------------------------------------------------------- 1 | import { set } from "@ember/object"; 2 | import Route from "@ember/routing/route"; 3 | 4 | 5 | export default Route.extend({ 6 | model() { 7 | return this.modelFor( "settings" ); 8 | }, 9 | 10 | activate() { 11 | const settingsController = this.controllerFor( "settings" ); 12 | set( settingsController, "currentSubmenu", this.routeName ); 13 | }, 14 | 15 | deactivate() { 16 | const settingsController = this.controllerFor( "settings" ); 17 | set( settingsController, "isAnimated", true ); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /src/app/ui/routes/settings/channels/template.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{#if model.length}} 3 | 4 | {{/if}} 5 | {{#content-list modelFiltered as |item|}} 6 | {{settings-channel-item 7 | content=item.user 8 | erase=(action "erase" item) 9 | }} 10 | {{else if filter}} 11 |

    {{t "settings.channels.none"}}

    12 | {{else}} 13 |

    {{t "settings.channels.empty"}}

    14 | {{/content-list}} 15 |
    -------------------------------------------------------------------------------- /src/app/ui/routes/settings/chat/route.js: -------------------------------------------------------------------------------- 1 | export { default } from "../-submenu/route"; 2 | -------------------------------------------------------------------------------- /src/app/ui/routes/settings/gui/route.js: -------------------------------------------------------------------------------- 1 | export { default } from "../-submenu/route"; 2 | -------------------------------------------------------------------------------- /src/app/ui/routes/settings/hotkeys/route.js: -------------------------------------------------------------------------------- 1 | export { default } from "../-submenu/route"; 2 | -------------------------------------------------------------------------------- /src/app/ui/routes/settings/index/route.js: -------------------------------------------------------------------------------- 1 | import { get, set } from "@ember/object"; 2 | import Route from "@ember/routing/route"; 3 | 4 | 5 | export default Route.extend({ 6 | actions: { 7 | didTransition() { 8 | const settingsController = this.controllerFor( "settings" ); 9 | let goto = get( settingsController, "currentSubmenu" ); 10 | if ( !goto ) { 11 | goto = "settings.main"; 12 | } 13 | 14 | set( settingsController, "isAnimated", false ); 15 | 16 | this.replaceWith( goto ); 17 | }, 18 | 19 | willTransition() { 20 | return false; 21 | } 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/ui/routes/settings/main/route.js: -------------------------------------------------------------------------------- 1 | export { default } from "../-submenu/route"; 2 | -------------------------------------------------------------------------------- /src/app/ui/routes/settings/notifications/route.js: -------------------------------------------------------------------------------- 1 | export { default } from "../-submenu/route"; 2 | -------------------------------------------------------------------------------- /src/app/ui/routes/settings/player/route.js: -------------------------------------------------------------------------------- 1 | export { default } from "../-submenu/route"; 2 | -------------------------------------------------------------------------------- /src/app/ui/routes/settings/streaming/route.js: -------------------------------------------------------------------------------- 1 | export { default } from "../-submenu/route"; 2 | -------------------------------------------------------------------------------- /src/app/ui/routes/settings/streams/controller.js: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import { qualities as contentStreamingQuality } from "data/models/stream/model"; 3 | import { 4 | default as SettingsStreams, 5 | DEFAULT_VODCAST_REGEXP 6 | } from "data/models/settings/streams/fragment"; 7 | import { isDarwin } from "utils/node/platform"; 8 | 9 | 10 | const { 11 | contentName: contentStreamsName, 12 | info: contentStreamsInfo, 13 | click: contentStreamsClick 14 | } = SettingsStreams; 15 | 16 | 17 | export default Controller.extend({ 18 | contentStreamingQuality, 19 | contentStreamsName, 20 | contentStreamsInfo, 21 | contentStreamsClick, 22 | 23 | DEFAULT_VODCAST_REGEXP, 24 | 25 | isDarwin 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/ui/routes/settings/streams/route.js: -------------------------------------------------------------------------------- 1 | export { default } from "../-submenu/route"; 2 | -------------------------------------------------------------------------------- /src/app/ui/routes/settings/template.hbs: -------------------------------------------------------------------------------- 1 |
    2 |

    {{t "settings.menu.header"}}

    3 |
    4 | {{sub-menu "settings" (hash 5 | main=(t "settings.menu.main") 6 | gui=(t "settings.menu.gui") 7 | streaming=(t "settings.menu.streaming") 8 | player=(t "settings.menu.player") 9 | streams=(t "settings.menu.streams") 10 | chat=(t "settings.menu.chat") 11 | languages=(t "settings.menu.languages") 12 | hotkeys=(t "settings.menu.hotkeys") 13 | notifications=(t "settings.menu.notifications") 14 | channels=(t "settings.menu.channels") 15 | )}} 16 |
    17 | {{outlet}} 18 | {{settings-submit 19 | apply=(action "apply") 20 | discard=(action "discard") 21 | isDirty=model.isDirty 22 | disabled=(is-equal currentSubmenu "settings.channels") 23 | }} 24 |
    -------------------------------------------------------------------------------- /src/app/ui/routes/streams/route.js: -------------------------------------------------------------------------------- 1 | import UserIndexRoute from "ui/routes/user/index/route"; 2 | import PaginationMixin from "ui/routes/-mixins/routes/infinite-scroll/pagination"; 3 | import FilterLanguagesMixin from "ui/routes/-mixins/routes/filter-languages"; 4 | import RefreshRouteMixin from "ui/routes/-mixins/routes/refresh"; 5 | 6 | 7 | export default UserIndexRoute.extend( PaginationMixin, FilterLanguagesMixin, RefreshRouteMixin, { 8 | itemSelector: ".stream-item-component", 9 | modelName: "twitch-stream", 10 | modelPreload: "thumbnail_url.latest", 11 | 12 | /** 13 | * @param {TwitchStream} twitchStream 14 | * @return {Promise} 15 | */ 16 | async modelItemLoader( twitchStream ) { 17 | await Promise.all([ 18 | twitchStream.user.promise, 19 | twitchStream.channel.promise 20 | ]); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/ui/routes/streams/template.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    {{t "routes.streams.header"}}

    4 | {{#quick-bar}} 5 | {{quick-bar-homepage}} 6 | {{/quick-bar}} 7 |
    8 | {{#content-list model as |item isNewItem isDuplicateItem|}} 9 | {{stream-item 10 | content=item 11 | isNewItem=isNewItem 12 | isDuplicateItem=isDuplicateItem 13 | }} 14 | {{else}} 15 |

    {{t "routes.streams.empty"}}

    16 | {{/content-list}} 17 |
    -------------------------------------------------------------------------------- /src/app/ui/routes/team/controller.js: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import "./styles.less"; 3 | 4 | 5 | export default Controller; 6 | -------------------------------------------------------------------------------- /src/app/ui/routes/team/index/template.hbs: -------------------------------------------------------------------------------- 1 | {{#content-list model as |item isNewItem|}} 2 | {{stream-item 3 | content=item 4 | isNewItem=isNewItem 5 | }} 6 | {{else}} 7 |

    {{t "routes.team.live.empty"}}

    8 | {{/content-list}} -------------------------------------------------------------------------------- /src/app/ui/routes/team/info/route.js: -------------------------------------------------------------------------------- 1 | import { getOwner } from "@ember/application"; 2 | import UserIndexRoute from "ui/routes/user/index/route"; 3 | 4 | 5 | export default UserIndexRoute.extend({ 6 | model() { 7 | return this.modelFor( "team" ); 8 | }, 9 | 10 | refresh() { 11 | return getOwner( this ).lookup( "route:team" ).refresh(); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /src/app/ui/routes/team/info/template.hbs: -------------------------------------------------------------------------------- 1 | {{#selectable-text tagName="section" classNames="content-team-info"}} 2 |

    {{t "routes.team.info.header"}}

    3 | {{#embedded-html-links classNames="user-content"}}{{{model.info}}}{{/embedded-html-links}} 4 | {{/selectable-text}} -------------------------------------------------------------------------------- /src/app/ui/routes/team/loading/route.js: -------------------------------------------------------------------------------- 1 | export { default } from "ui/routes/loading/route"; 2 | -------------------------------------------------------------------------------- /src/app/ui/routes/team/members/template.hbs: -------------------------------------------------------------------------------- 1 | {{#content-list model as |item isNewItem|}} 2 | {{channel-item 3 | content=(hash 4 | user=item 5 | ) 6 | isNewItem=isNewItem 7 | }} 8 | {{else}} 9 |

    {{t "routes.team.all.empty"}}

    10 | {{/content-list}} -------------------------------------------------------------------------------- /src/app/ui/routes/team/route.js: -------------------------------------------------------------------------------- 1 | import UserIndexRoute from "ui/routes/user/index/route"; 2 | import RefreshRouteMixin from "ui/routes/-mixins/routes/refresh"; 3 | import preload from "utils/preload"; 4 | 5 | 6 | export default UserIndexRoute.extend( RefreshRouteMixin, { 7 | async model({ team_id }) { 8 | /** @type {TwitchTeam} */ 9 | const record = await this.store.findRecord( "twitch-team", team_id, { reload: true } ); 10 | 11 | return await preload( record, "thumbnail_url" ); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /src/app/ui/routes/user/auth/route.js: -------------------------------------------------------------------------------- 1 | import { get } from "@ember/object"; 2 | import Route from "@ember/routing/route"; 3 | import { inject as service } from "@ember/service"; 4 | 5 | 6 | export default Route.extend({ 7 | auth: service(), 8 | 9 | beforeModel( transition ) { 10 | // check if user is successfully logged in 11 | if ( get( this, "auth.session.isLoggedIn" ) ) { 12 | transition.abort(); 13 | this.transitionTo( "user.index" ); 14 | } 15 | }, 16 | 17 | actions: { 18 | willTransition() { 19 | this.controller.send( "abort" ); 20 | this.controller.resetProperties(); 21 | } 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/ui/routes/user/controller.js: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import "./styles.less"; 3 | 4 | 5 | export default Controller; 6 | -------------------------------------------------------------------------------- /src/app/ui/routes/user/followed-channels/template.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    {{t "routes.user.followedChannels.header"}}

    4 | {{headline-totals model.meta.total}} 5 | {{#quick-bar}} 6 | {{quick-bar-homepage}} 7 | {{/quick-bar}} 8 |
    9 | {{#content-list model as |item isNewItem isDuplicateItem|}} 10 | {{channel-item 11 | content=(hash 12 | user=item.broadcaster_id 13 | followed_at=item.followed_at 14 | ) 15 | isNewItem=isNewItem 16 | isDuplicateItem=isDuplicateItem 17 | }} 18 | {{else}} 19 |

    {{t "routes.user.followedChannels.empty"}}

    20 | {{/content-list}} 21 |
    -------------------------------------------------------------------------------- /src/app/ui/routes/user/followed-streams/template.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    {{t "routes.user.followedStreams.header"}}

    4 | {{#quick-bar}} 5 | {{quick-bar-homepage}} 6 | {{/quick-bar}} 7 |
    8 | {{#content-list model as |item isNewItem isDuplicateItem|}} 9 | {{stream-item 10 | content=item 11 | isNewItem=isNewItem 12 | isDuplicateItem=isDuplicateItem 13 | faded=false 14 | }} 15 | {{else}} 16 |

    {{t "routes.user.followedStreams.empty.text"}}

    17 |
      18 |
    • {{#link-to "streams"}}{{t "routes.user.followedStreams.empty.streams"}}{{/link-to}}
    • 19 |
    20 | {{/content-list}} 21 |
    -------------------------------------------------------------------------------- /src/app/ui/routes/user/loading/route.js: -------------------------------------------------------------------------------- 1 | export { default } from "ui/routes/loading/route"; 2 | -------------------------------------------------------------------------------- /src/app/ui/routes/watching/controller.js: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import { sort } from "@ember/object/computed"; 3 | import { inject as service } from "@ember/service"; 4 | import { qualities } from "data/models/stream/model"; 5 | import "./styles.less"; 6 | 7 | 8 | export default Controller.extend({ 9 | /** @type {AuthService} */ 10 | auth: service(), 11 | /** @type {StreamingService} */ 12 | streaming: service(), 13 | 14 | sortedModel: sort( "model", "sortBy" ), 15 | sortBy: [ "started:desc" ], 16 | 17 | qualities, 18 | 19 | actions: { 20 | openDialog( stream ) { 21 | this.streaming.startStream( stream ); 22 | }, 23 | 24 | closeStream( stream ) { 25 | this.streaming.closeStream( stream ); 26 | } 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /src/app/ui/routes/watching/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | import { inject as service } from "@ember/service"; 3 | import RefreshRouteMixin from "ui/routes/-mixins/routes/refresh"; 4 | import preload from "utils/preload"; 5 | 6 | 7 | export default Route.extend( RefreshRouteMixin, { 8 | /** @type {StreamingService} */ 9 | streaming: service(), 10 | 11 | /** 12 | * @return {Promise>} 13 | */ 14 | async model() { 15 | await preload( 16 | this.streaming.model.map( stream => stream.stream ), 17 | "thumbnail_url.latest" 18 | ); 19 | 20 | return this.streaming.model; 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/utils/getStreamFromUrl.js: -------------------------------------------------------------------------------- 1 | const reUrl = /^(?:https?:\/\/)?(?:(?:www|secure|go)\.)?twitch\.tv\/(\w+)(?:\/profile)?$/; 2 | const blacklist = [ "directory", "login", "signup", "logout", "settings" ]; 3 | 4 | 5 | /** 6 | * @param {String} url 7 | * @returns {(Boolean|String)} 8 | */ 9 | function getStreamFromUrl( url ) { 10 | const match = reUrl.exec( String( url ) ); 11 | 12 | if ( !match ) { 13 | return false; 14 | } 15 | 16 | return blacklist.indexOf( match[ 1 ] ) === -1 17 | ? match[ 1 ] 18 | : false; 19 | } 20 | 21 | 22 | export default getStreamFromUrl; 23 | -------------------------------------------------------------------------------- /src/app/utils/is-focused.js: -------------------------------------------------------------------------------- 1 | export default function isFocused( element ) { 2 | return element.ownerDocument.activeElement === element; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/utils/node/child_process/spawn.js: -------------------------------------------------------------------------------- 1 | import Process from "nwjs/process"; 2 | import { spawn } from "child_process"; 3 | 4 | 5 | const { assign } = Object; 6 | 7 | 8 | export default function( command, params, options = {} ) { 9 | const opt = assign( {}, options ); 10 | 11 | if ( opt.env ) { 12 | opt.env = assign( {}, Process.env, opt.env ); 13 | } 14 | 15 | return spawn( command, params, opt ); 16 | } 17 | -------------------------------------------------------------------------------- /src/app/utils/node/env-path.js: -------------------------------------------------------------------------------- 1 | import { delimiter } from "path"; 2 | 3 | export const paths = ( process.env.PATH || process.env.path || "." ).split( delimiter ); 4 | -------------------------------------------------------------------------------- /src/app/utils/node/fs/mkdirp.js: -------------------------------------------------------------------------------- 1 | import { promises as fsPromises } from "fs"; 2 | 3 | 4 | const { mkdir } = fsPromises; 5 | 6 | 7 | /** 8 | * @param {string} path 9 | * @param {Object?} options 10 | * @param {(string|number)?} options.mode 11 | * @returns {Promise} 12 | */ 13 | export default async function mkdirp( path, options = {} ) { 14 | return mkdir( path, Object.assign( {}, options, { recursive: true } ) ); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/utils/node/resolvePath.js: -------------------------------------------------------------------------------- 1 | import { isWin } from "utils/node/platform"; 2 | import { resolve } from "path"; 3 | 4 | 5 | const reVarWindows = /%([^%]+)%/g; 6 | const reVarUnix = /\$([A-Z_]+)/g; 7 | 8 | 9 | function fnVarReplace( _, v ) { 10 | return process.env[ v ] || ""; 11 | } 12 | 13 | function resolvePathFactory( pattern ) { 14 | return function resolvePath( ...args ) { 15 | if ( !args.length ) { 16 | return ""; 17 | } 18 | 19 | args[0] = String( args[0] || "" ).replace( pattern, fnVarReplace ); 20 | 21 | return resolve( ...args ); 22 | }; 23 | } 24 | 25 | 26 | export default isWin 27 | ? resolvePathFactory( reVarWindows ) 28 | : resolvePathFactory( reVarUnix ); 29 | -------------------------------------------------------------------------------- /src/app/utils/system-locale.js: -------------------------------------------------------------------------------- 1 | import { locales as localesConfig } from "config"; 2 | import { window } from "nwjs/Window"; 3 | 4 | 5 | const kLocales = Object.keys( localesConfig.locales ); 6 | // test whether one of the system locales is supported 7 | const locale = window.navigator.languages 8 | .map( tag => tag.toLowerCase() ) 9 | .find( tag => kLocales.includes( tag ) ) 10 | // choose the first supported system locale, or use the defined default one instead 11 | || localesConfig.default; 12 | 13 | 14 | export default locale; 15 | -------------------------------------------------------------------------------- /src/app/utils/wait.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {number} time 3 | * @param {boolean?} fail 4 | * @returns {Function} 5 | */ 6 | function wait( time, fail ) { 7 | return function waitPromise( data ) { 8 | return new Promise(function( resolve, reject ) { 9 | setTimeout(function() { 10 | ( fail ? reject : resolve )( data ); 11 | }, time ); 12 | }); 13 | }; 14 | } 15 | 16 | 17 | export default wait; 18 | -------------------------------------------------------------------------------- /src/config/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "icons": { 3 | "big": "~assets/icons/icon-256.png", 4 | "tray": { 5 | "win32": { 6 | "@1x": "~assets/icons/icon-16.png", 7 | "@2x": "~assets/icons/icon-16@2x.png", 8 | "@3x": "~assets/icons/icon-16@3x.png" 9 | }, 10 | "darwin": { 11 | "@1x": "~assets/icons/icon-osx-18.png", 12 | "@2x": "~assets/icons/icon-osx-18@2x.png", 13 | "@3x": "~assets/icons/icon-osx-18@3x.png" 14 | }, 15 | "linux": { 16 | "@1x": "~assets/icons/icon-16@3x.png", 17 | "@2x": "~assets/icons/icon-16@3x.png", 18 | "@3x": "~assets/icons/icon-16@3x.png" 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/config/log.json: -------------------------------------------------------------------------------- 1 | { 2 | "maxAgeDays": 7 3 | } 4 | -------------------------------------------------------------------------------- /src/config/notification.json: -------------------------------------------------------------------------------- 1 | { 2 | "cache": { 3 | "dir": "icons", 4 | "time": 604800000 5 | }, 6 | "fails": { 7 | "requests": 3, 8 | "channels": 1 9 | }, 10 | "interval": { 11 | "request": 60000, 12 | "retry": 1000, 13 | "error": 120000 14 | }, 15 | "query": { 16 | "first": 100, 17 | "maxQueries": 5 18 | }, 19 | "provider": { 20 | "snoretoast": { 21 | "timeoutSetup": 3000, 22 | "timeoutNotify": 60000 23 | }, 24 | "freedesktop": { 25 | "expire": 43200 26 | }, 27 | "growl": { 28 | "host": "localhost", 29 | "ports": [ 30 | 23053, 31 | 23052 32 | ], 33 | "timeout": 100 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/config/themes.json: -------------------------------------------------------------------------------- 1 | { 2 | "prefix": "theme-", 3 | "system": { 4 | "no-preference": "light", 5 | "light": "light", 6 | "dark": "dark" 7 | }, 8 | "themes": [ 9 | "light", 10 | "dark" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/config/update.json: -------------------------------------------------------------------------------- 1 | { 2 | "check-again": 604800000, 3 | "show-debug-message": 86400000, 4 | "githubreleases": { 5 | "host": "https://api.github.com", 6 | "namespace": "repos/streamlink/streamlink-twitch-gui" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/config/vars.json: -------------------------------------------------------------------------------- 1 | { 2 | "time-window-event-debounce": 1000, 3 | "time-window-event-ignore": 2000, 4 | 5 | "search-history-size": 50, 6 | 7 | "stream-reload-interval": 60000, 8 | "image-expiration-time": 60000 9 | } 10 | -------------------------------------------------------------------------------- /src/test/dev.css: -------------------------------------------------------------------------------- 1 | body { 2 | overflow-y: scroll; 3 | } 4 | 5 | #qunit { 6 | position: unset !important; 7 | } 8 | 9 | #qunit-tests { 10 | overflow: auto !important; 11 | } 12 | 13 | /* fix line height in the test module dropdown */ 14 | #qunit-modulefilter-dropdown-list .clickable { 15 | display: inline-block; 16 | width: 100%; 17 | } 18 | 19 | /* use the same rules as #qunit-fixture */ 20 | #ember-testing { 21 | position: absolute; 22 | top: -10000px; 23 | left: -10000px; 24 | width: 1000px; 25 | height: 1000px; 26 | } 27 | 28 | /* specific component fixes for testing */ 29 | .modal-service-component { 30 | top: unset !important; 31 | right: unset !important; 32 | bottom: unset !important; 33 | left: unset !important; 34 | } 35 | -------------------------------------------------------------------------------- /src/test/fixtures/data/models/github/releases.json: -------------------------------------------------------------------------------- 1 | { 2 | "latest-older": { 3 | "request": { 4 | "url": "https://api.github.com/repos/streamlink/streamlink-twitch-gui/releases/latest", 5 | "method": "GET", 6 | "query": {} 7 | }, 8 | "response": { 9 | "id": "123456789", 10 | "tag_name": "v1.0.0", 11 | "html_url": "https://github.com/streamlink/streamlink-twitch-gui/releases/v1.0.0" 12 | } 13 | }, 14 | "latest-newer": { 15 | "request": { 16 | "url": "https://api.github.com/repos/streamlink/streamlink-twitch-gui/releases/latest", 17 | "method": "GET", 18 | "query": {} 19 | }, 20 | "response": { 21 | "id": "987654321", 22 | "tag_name": "v1337.0.0", 23 | "html_url": "https://github.com/streamlink/streamlink-twitch-gui/releases/v1337.0.0" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/fixtures/data/models/twitch/adapter/coalesce-stream-single.yml: -------------------------------------------------------------------------------- 1 | request: 2 | method: "GET" 3 | url: "https://api.twitch.tv/helix/streams" 4 | query: 5 | - name: "user_id" 6 | value: "1" 7 | - name: "user_id" 8 | value: "2" 9 | response: 10 | data: 11 | - id: "1" 12 | user_id: "1" 13 | game_id: "1" 14 | - id: "2" 15 | user_id: "2" 16 | game_id: "2" 17 | pagination: {} 18 | -------------------------------------------------------------------------------- /src/test/fixtures/data/models/twitch/channel.yml: -------------------------------------------------------------------------------- 1 | request: 2 | method: "GET" 3 | url: "https://api.twitch.tv/helix/channels" 4 | query: 5 | - name: "broadcaster_id" 6 | value: "11" 7 | - name: "broadcaster_id" 8 | value: "21" 9 | response: 10 | data: 11 | - broadcaster_id: "11" 12 | broadcaster_name: "foo" 13 | game_id: "12" 14 | game_name: "some game" 15 | broadcaster_language: "en" 16 | title: "some title" 17 | delay: 90 18 | - broadcaster_id: "21" 19 | broadcaster_name: "bar" 20 | game_id: "22" 21 | game_name: "another game" 22 | broadcaster_language: "id" 23 | title: "another title" 24 | delay: 0 25 | -------------------------------------------------------------------------------- /src/test/fixtures/data/models/twitch/game-top.yml: -------------------------------------------------------------------------------- 1 | request: 2 | method: "GET" 3 | url: "https://api.twitch.tv/helix/games/top" 4 | query: {} 5 | response: 6 | data: 7 | - id: "1" 8 | - id: "2" 9 | pagination: {} 10 | -------------------------------------------------------------------------------- /src/test/fixtures/data/models/twitch/game.yml: -------------------------------------------------------------------------------- 1 | request: 2 | method: "GET" 3 | url: "https://api.twitch.tv/helix/games" 4 | query: 5 | - name: "id" 6 | value: "1" 7 | - name: "id" 8 | value: "2" 9 | response: 10 | data: 11 | - id: "1" 12 | name: "some game" 13 | box_art_url: "https://mock/twitch-game/1/box_art-{width}x{height}.png" 14 | - id: "2" 15 | name: "another game" 16 | box_art_url: "https://mock/twitch-game/2/box_art-{width}x{height}.png" 17 | -------------------------------------------------------------------------------- /src/test/fixtures/data/models/twitch/search-game.yml: -------------------------------------------------------------------------------- 1 | request: 2 | method: "GET" 3 | url: "https://api.twitch.tv/helix/search/categories" 4 | query: 5 | query: "foo" 6 | response: 7 | data: 8 | - id: "1" 9 | - id: "2" 10 | -------------------------------------------------------------------------------- /src/test/fixtures/data/models/twitch/serializer/metadata.yml: -------------------------------------------------------------------------------- 1 | invalid: 2 | request: 3 | method: "GET" 4 | url: "https://api.twitch.tv/foo" 5 | query: {} 6 | response: 7 | data: 8 | - id: "1" 9 | pagination: {} 10 | nul: null 11 | obj: 12 | key: "val" 13 | arr: 14 | - "item1" 15 | - "item2" 16 | valid: 17 | request: 18 | method: "GET" 19 | url: "https://api.twitch.tv/foo" 20 | query: {} 21 | response: 22 | data: 23 | - id: "1" 24 | pagination: 25 | cursor: "cursor-value" 26 | str: "foo" 27 | num: 123 28 | -------------------------------------------------------------------------------- /src/test/fixtures/data/models/twitch/serializer/response.yml: -------------------------------------------------------------------------------- 1 | invalid: 2 | request: 3 | method: "GET" 4 | url: "https://api.twitch.tv/foo" 5 | query: {} 6 | response: {} 7 | valid: 8 | request: 9 | method: "GET" 10 | url: "https://api.twitch.tv/foo" 11 | query: {} 12 | response: 13 | data: 14 | - id: "1" 15 | foo: "bar" 16 | - id: "2" 17 | foo: "baz" 18 | -------------------------------------------------------------------------------- /src/test/fixtures/data/models/twitch/serializer/single-response.yml: -------------------------------------------------------------------------------- 1 | valid: 2 | request: 3 | method: "GET" 4 | url: "https://api.twitch.tv/foo" 5 | query: {} 6 | response: 7 | data: 8 | - id: "1" 9 | foo: "bar" 10 | invalid: 11 | request: 12 | method: "GET" 13 | url: "https://api.twitch.tv/foo" 14 | query: {} 15 | response: 16 | data: 17 | -------------------------------------------------------------------------------- /src/test/fixtures/data/models/twitch/stream-followed.yml: -------------------------------------------------------------------------------- 1 | request: 2 | method: "GET" 3 | url: "https://api.twitch.tv/helix/streams/followed" 4 | query: 5 | user_id: "1" 6 | response: 7 | data: 8 | - user_id: "1" 9 | - user_id: "2" 10 | -------------------------------------------------------------------------------- /src/test/fixtures/services/auth/validate-session.yml: -------------------------------------------------------------------------------- 1 | request: 2 | method: "GET" 3 | url: "https://api.twitch.tv/helix/users" 4 | query: {} 5 | response: 6 | data: 7 | - id: "1337" 8 | login: "user" 9 | display_name: "User" 10 | -------------------------------------------------------------------------------- /src/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | QUnit 6 | 7 | 8 |
    9 |
    10 |
    11 |
    12 |
    13 | 14 | -------------------------------------------------------------------------------- /src/test/main-coverage.js: -------------------------------------------------------------------------------- 1 | import "./main"; 2 | 3 | 4 | function importAll( r ) { 5 | r.keys().forEach( r ); 6 | } 7 | 8 | importAll( require.context( "root/app/", true, /^\.\/(?!(main|app|logger)\.js$).+\.js$/ ) ); 9 | -------------------------------------------------------------------------------- /src/test/main-dev.js: -------------------------------------------------------------------------------- 1 | global._noQUnitBridge = true; 2 | 3 | import "./dev.css"; 4 | import "./main"; 5 | import nwGui from "nw.gui"; 6 | 7 | 8 | const nwWindow = nwGui.Window.get(); 9 | 10 | 11 | nwWindow.show(); 12 | nwWindow.showDevTools(); 13 | -------------------------------------------------------------------------------- /src/test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= package.name %>-test", 3 | "version": "<%= package.version %>", 4 | 5 | "main": "index.html", 6 | 7 | "window": { 8 | "title": "<%= main['display-name'] %> Test", 9 | "frame": true, 10 | "resizable": true, 11 | "show": false, 12 | "position": "center", 13 | "width": 960, 14 | "height": 540, 15 | "min_width": 960, 16 | "min_height": 540 17 | }, 18 | 19 | "chromium-args": "--enable-features=NativeNotifications" 20 | } 21 | -------------------------------------------------------------------------------- /src/test/tests/index.js: -------------------------------------------------------------------------------- 1 | function importAll( r ) { 2 | r.keys().forEach( r ); 3 | } 4 | 5 | // import tests in a certain order instead of importing them alphabetically 6 | importAll( require.context( "../web_modules/", true, /test\.js$|test\/[^\/]+\.js$/ ) ); 7 | importAll( require.context( "./loaders/", true, /\.js$/ ) ); 8 | importAll( require.context( "./nwjs/", true, /\.js$/ ) ); 9 | importAll( require.context( "./utils/", true, /\.js$/ ) ); 10 | importAll( require.context( "./data/", true, /\.js$/ ) ); 11 | importAll( require.context( "./init/", true, /\.js$/ ) ); 12 | importAll( require.context( "./ui/", true, /\.js$/ ) ); 13 | importAll( require.context( "./services/", true, /\.js$/ ) ); 14 | -------------------------------------------------------------------------------- /src/test/tests/init/initializers/localstorage/search.js: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | 3 | import updateSearch from "init/initializers/localstorage/search"; 4 | 5 | 6 | module( "init/initializers/localstorage/search", function() { 7 | test( "Removes 'streams' filter", function( assert ) { 8 | const data = { 9 | "1": { id: "1", filter: "channels", query: "foo" }, 10 | "2": { id: "2", filter: "streams", query: "foo" }, 11 | "3": { id: "3", filter: "streams", query: "bar" }, 12 | "4": { id: "4", filter: "all", query: "foo" } 13 | }; 14 | 15 | updateSearch( data ); 16 | 17 | assert.propEqual( data, { 18 | "1": { id: "1", filter: "channels", query: "foo" }, 19 | "3": { id: "3", filter: "channels", query: "bar" }, 20 | "4": { id: "4", filter: "all", query: "foo" } 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/test/tests/services/chat/providers/chrome.js: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | 3 | import chatProviderChromeInjector from "inject-loader!services/chat/providers/chrome"; 4 | 5 | 6 | module( "services/chat/providers/chrome" ); 7 | 8 | 9 | test( "Exports ChatProviderChromium", assert => { 10 | 11 | const ChatProviderChromium = class {}; 12 | 13 | const { default: ChatProviderChrome } = chatProviderChromeInjector({ 14 | "./chromium": ChatProviderChromium 15 | }); 16 | 17 | assert.strictEqual( 18 | ChatProviderChrome, 19 | ChatProviderChromium, 20 | "Exports the Chromium chat provider" 21 | ); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /src/test/tests/services/chat/providers/custom.js: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | 3 | import chatProviderCustomInjector from "inject-loader!services/chat/providers/custom"; 4 | 5 | 6 | module( "services/chat/providers/custom" ); 7 | 8 | 9 | test( "Exports ChatProviderBasic", assert => { 10 | 11 | const ChatProviderBasic = class {}; 12 | 13 | const { default: ChatProviderCustom } = chatProviderCustomInjector({ 14 | "./-basic": ChatProviderBasic 15 | }); 16 | 17 | assert.strictEqual( 18 | ChatProviderCustom, 19 | ChatProviderBasic, 20 | "Exports the basic chat provider" 21 | ); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /src/test/tests/services/notification/providers/auto.js: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | 3 | import NotificationProviderAuto from "services/notification/providers/auto"; 4 | 5 | 6 | module( "services/notification/providers/auto" ); 7 | 8 | 9 | test( "isSupported", assert => { 10 | 11 | assert.notOk( NotificationProviderAuto.isSupported(), "Is not supported" ); 12 | 13 | }); 14 | -------------------------------------------------------------------------------- /src/test/web_modules/cartesian-product/index.js: -------------------------------------------------------------------------------- 1 | const fill = ( A, B ) => [].concat( ...A.map( a => B.map( b => [].concat( a, b ) ) ) ); 2 | const cartesian = ( a, b, ...c ) => b ? cartesian( fill( a, b ), ...c ) : a; 3 | 4 | 5 | export default cartesian; 6 | -------------------------------------------------------------------------------- /src/test/web_modules/cartesian-product/test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | 3 | import cartesian from "./index"; 4 | 5 | 6 | module( "cartesian-product", function() { 7 | test( "Cartesian product", function( assert ) { 8 | const prod = cartesian( [ 1, 2, 3 ], [ 4, 5 ], [ 6, 7 ], [ 8 ] ); 9 | assert.propEqual( prod, [ 10 | [ 1, 4, 6, 8 ], 11 | [ 1, 4, 7, 8 ], 12 | [ 1, 5, 6, 8 ], 13 | [ 1, 5, 7, 8 ], 14 | [ 2, 4, 6, 8 ], 15 | [ 2, 4, 7, 8 ], 16 | [ 2, 5, 6, 8 ], 17 | [ 2, 5, 7, 8 ], 18 | [ 3, 4, 6, 8 ], 19 | [ 3, 4, 7, 8 ], 20 | [ 3, 5, 6, 8 ], 21 | [ 3, 5, 7, 8 ] 22 | ], "Correctly builds the cartesian product of multiple input arrays of various lengths" ); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/test/web_modules/ember-qunit/test-loader.js: -------------------------------------------------------------------------------- 1 | export class TestLoader {} 2 | 3 | export function loadTests() {} 4 | -------------------------------------------------------------------------------- /src/test/web_modules/ember-test.js: -------------------------------------------------------------------------------- 1 | import "root/web_modules/ember"; 2 | import "ember-source/dist/ember-template-compiler"; 3 | import "ember-data"; 4 | import "ember-qunit"; 5 | -------------------------------------------------------------------------------- /src/test/web_modules/event-utils/index.js: -------------------------------------------------------------------------------- 1 | export * from "./events"; 2 | export { blur } from "./blur"; 3 | export { triggerEvent } from "./trigger-event"; 4 | export { triggerKeyEvent, triggerKeyDownEvent } from "./trigger-key-event"; 5 | -------------------------------------------------------------------------------- /src/test/web_modules/htmlbars-inline-precompile.js: -------------------------------------------------------------------------------- 1 | export { hbs as default } from "./test-utils"; 2 | -------------------------------------------------------------------------------- /src/test/web_modules/qunit/index.js: -------------------------------------------------------------------------------- 1 | import QUnit from "qunit/qunit/qunit"; 2 | import "qunit/qunit/qunit.css"; 3 | 4 | 5 | export default QUnit; 6 | 7 | export const config = QUnit.config; 8 | 9 | export const module = QUnit.module; 10 | export const only = QUnit.only; 11 | export const test = QUnit.test; 12 | export const todo = QUnit.todo; 13 | export const skip = QUnit.skip; 14 | export const start = QUnit.start; 15 | -------------------------------------------------------------------------------- /src/test/web_modules/require.js: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | 3 | 4 | export default Ember.__loader.require; 5 | -------------------------------------------------------------------------------- /src/test/web_modules/translation-string/test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | 3 | import t from "translation-key"; 4 | 5 | 6 | module( "translation-key", function() { 7 | test( "translation-key", function( assert ) { 8 | const bar = "bar"; 9 | const qux = "qux"; 10 | 11 | assert.strictEqual( 12 | t`foo`, 13 | "foo", 14 | "Returns a simple string" 15 | ); 16 | assert.strictEqual( 17 | t`foo ${bar} baz ${qux} quux`, 18 | "foo bar baz qux quux", 19 | "Properly concats the template string with its expressions" 20 | ); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/web_modules/ember-app.js: -------------------------------------------------------------------------------- 1 | // empty module: see the ember-app-loader 2 | -------------------------------------------------------------------------------- /src/web_modules/ember-data/version.js: -------------------------------------------------------------------------------- 1 | import metadata from "metadata"; 2 | 3 | 4 | export default metadata.dependencies[ "ember-data" ]; 5 | -------------------------------------------------------------------------------- /src/web_modules/ember-get-config.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /src/web_modules/fetch.js: -------------------------------------------------------------------------------- 1 | export default window.fetch; 2 | -------------------------------------------------------------------------------- /src/web_modules/metadata.js: -------------------------------------------------------------------------------- 1 | // empty module: see the metadata-loader 2 | -------------------------------------------------------------------------------- /src/web_modules/snoretoast-binaries.js: -------------------------------------------------------------------------------- 1 | // snoretoast binary dependencies 2 | // see build/tasks/webpack/configurators/stylesheets-and-assets.js 3 | // see services/NotificationService/provider/snoretoast 4 | export default { 5 | x86: [ "bin", "win32", "snoretoast.exe" ], 6 | x64: [ "bin", "win64", "snoretoast.exe" ] 7 | }; 8 | -------------------------------------------------------------------------------- /src/web_modules/translation-key.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple template string function for annotating translation strings 3 | * @param {string[]} strings 4 | * @param {*} expressions 5 | * @returns {string} 6 | */ 7 | export default function t( strings, ...expressions ) { 8 | if ( strings.length === 1 ) { 9 | return strings[0]; 10 | } 11 | 12 | const res = [ strings[ 0 ] ]; 13 | for ( let i = 0, l = strings.length - 1; i < l; ) { 14 | res.push( expressions[ i ], strings[ ++i ] ); 15 | } 16 | 17 | return res.join( "" ); 18 | } 19 | -------------------------------------------------------------------------------- /src/web_modules/transparent-image.js: -------------------------------------------------------------------------------- 1 | export default "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; 2 | --------------------------------------------------------------------------------