├── .gitattributes
├── src
├── config
│ ├── log.json
│ ├── themes.json
│ ├── update.json
│ ├── vars.json
│ ├── files.json
│ └── notification.json
├── app
│ ├── nwjs
│ │ ├── process.js
│ │ ├── debug.js
│ │ ├── Screen.js
│ │ ├── nwGui.js
│ │ └── App.js
│ ├── ui
│ │ ├── components
│ │ │ ├── list
│ │ │ │ ├── headline-totals
│ │ │ │ │ ├── template.hbs
│ │ │ │ │ └── component.js
│ │ │ │ ├── game-item
│ │ │ │ │ ├── template.hbs
│ │ │ │ │ └── component.js
│ │ │ │ ├── infinite-scroll
│ │ │ │ │ ├── styles.less
│ │ │ │ │ └── template.hbs
│ │ │ │ ├── team-item
│ │ │ │ │ ├── component.js
│ │ │ │ │ ├── styles.less
│ │ │ │ │ └── template.hbs
│ │ │ │ ├── -list-item
│ │ │ │ │ └── component.js
│ │ │ │ ├── channel-item
│ │ │ │ │ └── component.js
│ │ │ │ ├── content-list
│ │ │ │ │ └── template.hbs
│ │ │ │ └── settings-channel-item
│ │ │ │ │ └── component.js
│ │ │ ├── modal
│ │ │ │ ├── modal-dialog
│ │ │ │ │ └── template.hbs
│ │ │ │ ├── modal-body
│ │ │ │ │ └── component.js
│ │ │ │ ├── modal-footer
│ │ │ │ │ └── component.js
│ │ │ │ ├── modal-header
│ │ │ │ │ └── component.js
│ │ │ │ ├── modal-log
│ │ │ │ │ ├── template.hbs
│ │ │ │ │ └── component.js
│ │ │ │ ├── modal-service
│ │ │ │ │ ├── template.hbs
│ │ │ │ │ ├── component.js
│ │ │ │ │ └── styles.less
│ │ │ │ ├── modal-debug
│ │ │ │ │ ├── component.js
│ │ │ │ │ └── template.hbs
│ │ │ │ ├── modal-firstrun
│ │ │ │ │ ├── template.hbs
│ │ │ │ │ └── component.js
│ │ │ │ ├── modal-confirm
│ │ │ │ │ ├── component.js
│ │ │ │ │ └── template.hbs
│ │ │ │ ├── modal-changelog
│ │ │ │ │ └── template.hbs
│ │ │ │ ├── modal-newrelease
│ │ │ │ │ └── template.hbs
│ │ │ │ └── modal-quit
│ │ │ │ │ └── component.js
│ │ │ ├── loading-spinner
│ │ │ │ ├── template.hbs
│ │ │ │ └── component.js
│ │ │ ├── link
│ │ │ │ ├── documentation-link
│ │ │ │ │ ├── template.hbs
│ │ │ │ │ └── styles.less
│ │ │ │ └── embedded-links
│ │ │ │ │ ├── template.hbs
│ │ │ │ │ └── component.js
│ │ │ ├── helper
│ │ │ │ ├── t.js
│ │ │ │ ├── get-index.js
│ │ │ │ ├── is-gt.js
│ │ │ │ ├── is-gte.js
│ │ │ │ ├── bool-and.js
│ │ │ │ ├── bool-not.js
│ │ │ │ ├── bool-or.js
│ │ │ │ ├── get-param.js
│ │ │ │ ├── math-add.js
│ │ │ │ ├── math-div.js
│ │ │ │ ├── math-mul.js
│ │ │ │ ├── math-sub.js
│ │ │ │ ├── find-by.js
│ │ │ │ ├── is-null.js
│ │ │ │ ├── is-equal.js
│ │ │ │ ├── format-viewers.js
│ │ │ │ └── -from-now.js
│ │ │ ├── stream
│ │ │ │ ├── stream-preview-image
│ │ │ │ │ └── template.hbs
│ │ │ │ ├── stats-row
│ │ │ │ │ ├── styles.less
│ │ │ │ │ ├── component.js
│ │ │ │ │ └── template.hbs
│ │ │ │ └── stream-presentation
│ │ │ │ │ ├── component.js
│ │ │ │ │ ├── styles.less
│ │ │ │ │ └── template.hbs
│ │ │ ├── button
│ │ │ │ ├── channel-button
│ │ │ │ │ └── styles.less
│ │ │ │ ├── form-button
│ │ │ │ │ └── template.hbs
│ │ │ │ └── open-chat
│ │ │ │ │ └── component.js
│ │ │ ├── preview-image
│ │ │ │ ├── template.hbs
│ │ │ │ └── styles.less
│ │ │ ├── quick
│ │ │ │ └── quick-bar
│ │ │ │ │ └── template.hbs
│ │ │ ├── form
│ │ │ │ ├── file-select
│ │ │ │ │ └── template.hbs
│ │ │ │ ├── check-box
│ │ │ │ │ └── component.js
│ │ │ │ ├── drop-down-selection
│ │ │ │ │ ├── template.hbs
│ │ │ │ │ └── component.js
│ │ │ │ ├── radio-buttons-item
│ │ │ │ │ └── component.js
│ │ │ │ ├── -input-btn
│ │ │ │ │ └── template.hbs
│ │ │ │ ├── radio-buttons
│ │ │ │ │ ├── component.js
│ │ │ │ │ └── template.hbs
│ │ │ │ ├── number-field
│ │ │ │ │ └── template.hbs
│ │ │ │ ├── drop-down-list
│ │ │ │ │ └── template.hbs
│ │ │ │ └── drop-down
│ │ │ │ │ └── template.hbs
│ │ │ ├── sub-menu
│ │ │ │ ├── template.hbs
│ │ │ │ └── component.js
│ │ │ ├── settings-hotkey
│ │ │ │ └── styles.less
│ │ │ ├── settings-submit
│ │ │ │ ├── template.hbs
│ │ │ │ └── styles.less
│ │ │ ├── main-menu
│ │ │ │ └── template.hbs
│ │ │ └── selectable-text
│ │ │ │ └── component.js
│ │ └── routes
│ │ │ ├── settings
│ │ │ ├── gui
│ │ │ │ └── route.js
│ │ │ ├── chat
│ │ │ │ └── route.js
│ │ │ ├── hotkeys
│ │ │ │ ├── route.js
│ │ │ │ ├── template.hbs
│ │ │ │ └── controller.js
│ │ │ ├── main
│ │ │ │ └── route.js
│ │ │ ├── player
│ │ │ │ └── route.js
│ │ │ ├── streams
│ │ │ │ ├── route.js
│ │ │ │ └── controller.js
│ │ │ ├── notifications
│ │ │ │ └── route.js
│ │ │ ├── streaming
│ │ │ │ └── route.js
│ │ │ ├── channels
│ │ │ │ ├── template.hbs
│ │ │ │ └── controller.js
│ │ │ ├── -submenu
│ │ │ │ └── route.js
│ │ │ ├── index
│ │ │ │ └── route.js
│ │ │ └── template.hbs
│ │ │ ├── channel
│ │ │ ├── loading
│ │ │ │ └── route.js
│ │ │ ├── teams
│ │ │ │ ├── template.hbs
│ │ │ │ └── route.js
│ │ │ ├── index
│ │ │ │ └── route.js
│ │ │ └── controller.js
│ │ │ ├── games
│ │ │ ├── loading
│ │ │ │ └── route.js
│ │ │ ├── index
│ │ │ │ ├── template.hbs
│ │ │ │ └── route.js
│ │ │ └── game
│ │ │ │ └── template.hbs
│ │ │ ├── team
│ │ │ ├── loading
│ │ │ │ └── route.js
│ │ │ ├── controller.js
│ │ │ ├── index
│ │ │ │ └── template.hbs
│ │ │ ├── members
│ │ │ │ └── template.hbs
│ │ │ ├── info
│ │ │ │ ├── template.hbs
│ │ │ │ └── route.js
│ │ │ └── route.js
│ │ │ ├── user
│ │ │ ├── loading
│ │ │ │ └── route.js
│ │ │ ├── controller.js
│ │ │ ├── auth
│ │ │ │ └── route.js
│ │ │ ├── followed-channels
│ │ │ │ └── template.hbs
│ │ │ └── followed-streams
│ │ │ │ ├── template.hbs
│ │ │ │ └── route.js
│ │ │ ├── loading
│ │ │ ├── template.hbs
│ │ │ ├── controller.js
│ │ │ ├── route.js
│ │ │ └── styles.less
│ │ │ ├── application
│ │ │ ├── template.hbs
│ │ │ ├── controller.js
│ │ │ ├── styles
│ │ │ │ ├── index.less
│ │ │ │ ├── scrollbars.less
│ │ │ │ ├── fixes.less
│ │ │ │ ├── fontawesome.less
│ │ │ │ └── content.less
│ │ │ └── route.js
│ │ │ ├── error
│ │ │ ├── controller.js
│ │ │ ├── template.hbs
│ │ │ └── styles.less
│ │ │ ├── search
│ │ │ ├── styles.less
│ │ │ └── controller.js
│ │ │ ├── index
│ │ │ └── route.js
│ │ │ ├── streams
│ │ │ ├── template.hbs
│ │ │ └── route.js
│ │ │ ├── -mixins
│ │ │ ├── controllers
│ │ │ │ └── retry-transition.js
│ │ │ └── routes
│ │ │ │ └── infinite-scroll
│ │ │ │ └── record-array.js
│ │ │ ├── about
│ │ │ ├── controller.js
│ │ │ └── styles.less
│ │ │ └── watching
│ │ │ ├── route.js
│ │ │ ├── controller.js
│ │ │ └── styles.less
│ ├── locales
│ │ ├── ja
│ │ │ ├── themes.yml
│ │ │ ├── qualities.yml
│ │ │ ├── helpers.yml
│ │ │ ├── hotkeys.yml
│ │ │ ├── services.yml
│ │ │ ├── contextmenu.yml
│ │ │ ├── models.yml
│ │ │ └── languages.yml
│ │ ├── zh-cn
│ │ │ ├── themes.yml
│ │ │ ├── qualities.yml
│ │ │ ├── helpers.yml
│ │ │ ├── services.yml
│ │ │ ├── hotkeys.yml
│ │ │ ├── contextmenu.yml
│ │ │ ├── models.yml
│ │ │ └── languages.yml
│ │ ├── zh-tw
│ │ │ ├── themes.yml
│ │ │ ├── qualities.yml
│ │ │ ├── helpers.yml
│ │ │ ├── services.yml
│ │ │ ├── contextmenu.yml
│ │ │ ├── models.yml
│ │ │ └── hotkeys.yml
│ │ ├── it
│ │ │ ├── themes.yml
│ │ │ ├── qualities.yml
│ │ │ ├── helpers.yml
│ │ │ ├── hotkeys.yml
│ │ │ ├── services.yml
│ │ │ ├── contextmenu.yml
│ │ │ ├── languages.yml
│ │ │ └── models.yml
│ │ ├── en
│ │ │ ├── themes.yml
│ │ │ ├── qualities.yml
│ │ │ ├── helpers.yml
│ │ │ ├── hotkeys.yml
│ │ │ ├── services.yml
│ │ │ ├── contextmenu.yml
│ │ │ ├── models.yml
│ │ │ └── languages.yml
│ │ ├── de
│ │ │ ├── themes.yml
│ │ │ ├── qualities.yml
│ │ │ ├── helpers.yml
│ │ │ ├── services.yml
│ │ │ ├── hotkeys.yml
│ │ │ ├── contextmenu.yml
│ │ │ ├── models.yml
│ │ │ └── languages.yml
│ │ ├── es
│ │ │ ├── themes.yml
│ │ │ ├── qualities.yml
│ │ │ ├── helpers.yml
│ │ │ ├── hotkeys.yml
│ │ │ ├── languages.yml
│ │ │ ├── contextmenu.yml
│ │ │ ├── models.yml
│ │ │ └── services.yml
│ │ ├── fr
│ │ │ ├── themes.yml
│ │ │ ├── qualities.yml
│ │ │ ├── helpers.yml
│ │ │ ├── hotkeys.yml
│ │ │ ├── contextmenu.yml
│ │ │ ├── services.yml
│ │ │ ├── models.yml
│ │ │ └── languages.yml
│ │ ├── pt-br
│ │ │ ├── themes.yml
│ │ │ ├── qualities.yml
│ │ │ ├── helpers.yml
│ │ │ ├── hotkeys.yml
│ │ │ ├── services.yml
│ │ │ ├── contextmenu.yml
│ │ │ ├── languages.yml
│ │ │ └── models.yml
│ │ └── ru
│ │ │ ├── themes.yml
│ │ │ ├── qualities.yml
│ │ │ ├── helpers.yml
│ │ │ ├── hotkeys.yml
│ │ │ ├── contextmenu.yml
│ │ │ ├── services.yml
│ │ │ ├── languages.yml
│ │ │ └── models.yml
│ ├── services
│ │ ├── chat
│ │ │ ├── providers
│ │ │ │ ├── chrome.js
│ │ │ │ ├── custom.js
│ │ │ │ ├── chatty-standalone.js
│ │ │ │ ├── browser.js
│ │ │ │ ├── chatterino.js
│ │ │ │ ├── index.js
│ │ │ │ ├── chatty.js
│ │ │ │ └── chromium.js
│ │ │ ├── logger.js
│ │ │ └── launch.js
│ │ ├── notification
│ │ │ ├── logger.js
│ │ │ ├── providers
│ │ │ │ ├── auto.js
│ │ │ │ ├── native.js
│ │ │ │ └── rich.js
│ │ │ └── cache
│ │ │ │ └── item.js
│ │ ├── streaming
│ │ │ ├── logger.js
│ │ │ ├── cache
│ │ │ │ ├── index.js
│ │ │ │ └── item.js
│ │ │ ├── is-aborted.js
│ │ │ ├── exec-obj.js
│ │ │ ├── launch
│ │ │ │ └── parse-error.js
│ │ │ └── spawn.js
│ │ └── settings.js
│ ├── data
│ │ └── models
│ │ │ ├── stream
│ │ │ └── adapter.js
│ │ │ ├── application
│ │ │ └── adapter.js
│ │ │ ├── auth
│ │ │ ├── serializer.js
│ │ │ ├── adapter.js
│ │ │ └── model.js
│ │ │ ├── search
│ │ │ ├── serializer.js
│ │ │ ├── adapter.js
│ │ │ └── model.js
│ │ │ ├── settings
│ │ │ ├── serializer.js
│ │ │ ├── adapter.js
│ │ │ ├── streaming
│ │ │ │ ├── quality
│ │ │ │ │ └── fragment.js
│ │ │ │ ├── provider
│ │ │ │ │ └── fragment.js
│ │ │ │ ├── player
│ │ │ │ │ ├── serializer.js
│ │ │ │ │ └── fragment.js
│ │ │ │ ├── qualities
│ │ │ │ │ └── fragment.js
│ │ │ │ ├── providers
│ │ │ │ │ └── fragment.js
│ │ │ │ └── players
│ │ │ │ │ └── fragment.js
│ │ │ ├── chat
│ │ │ │ ├── provider
│ │ │ │ │ └── serializer.js
│ │ │ │ ├── fragment.js
│ │ │ │ └── providers
│ │ │ │ │ └── fragment.js
│ │ │ ├── hotkeys
│ │ │ │ ├── namespace
│ │ │ │ │ └── serializer.js
│ │ │ │ ├── action
│ │ │ │ │ └── fragment.js
│ │ │ │ └── hotkey
│ │ │ │ │ └── fragment.js
│ │ │ └── streams
│ │ │ │ └── languages
│ │ │ │ └── fragment.js
│ │ │ ├── window
│ │ │ ├── serializer.js
│ │ │ ├── adapter.js
│ │ │ └── model.js
│ │ │ ├── versioncheck
│ │ │ ├── serializer.js
│ │ │ ├── adapter.js
│ │ │ └── model.js
│ │ │ ├── channel-settings
│ │ │ ├── serializer.js
│ │ │ └── adapter.js
│ │ │ ├── twitch
│ │ │ ├── channel
│ │ │ │ ├── adapter.js
│ │ │ │ ├── model.js
│ │ │ │ └── serializer.js
│ │ │ ├── game
│ │ │ │ ├── serializer.js
│ │ │ │ ├── adapter.js
│ │ │ │ └── model.js
│ │ │ ├── team
│ │ │ │ ├── adapter.js
│ │ │ │ └── serializer.js
│ │ │ ├── game-top
│ │ │ │ ├── model.js
│ │ │ │ └── serializer.js
│ │ │ ├── search-game
│ │ │ │ ├── model.js
│ │ │ │ └── serializer.js
│ │ │ ├── stream-followed
│ │ │ │ ├── model.js
│ │ │ │ └── serializer.js
│ │ │ ├── stream
│ │ │ │ └── adapter.js
│ │ │ ├── user
│ │ │ │ └── serializer.js
│ │ │ ├── search-channel
│ │ │ │ └── serializer.js
│ │ │ └── channels-followed
│ │ │ │ ├── model.js
│ │ │ │ └── serializer.js
│ │ │ └── github
│ │ │ └── releases
│ │ │ ├── serializer.js
│ │ │ ├── adapter.js
│ │ │ └── model.js
│ ├── init
│ │ ├── initializers
│ │ │ ├── ember-data.js
│ │ │ ├── localstorage
│ │ │ │ ├── localstorage.js
│ │ │ │ ├── namespaces.js
│ │ │ │ ├── channelsettings.js
│ │ │ │ └── search.js
│ │ │ ├── env.js
│ │ │ ├── nwjs.js
│ │ │ ├── settings-chat-provider.js
│ │ │ ├── settings-streaming-player.js
│ │ │ ├── settings-hotkeys-namespace.js
│ │ │ ├── keyboard-layout-map.js
│ │ │ └── model-fragments.js
│ │ └── instance-initializers
│ │ │ ├── nwjs
│ │ │ └── integrations.js
│ │ │ └── boolean-transform.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
│ ├── utils
│ │ ├── is-focused.js
│ │ ├── node
│ │ │ ├── env-path.js
│ │ │ ├── child_process
│ │ │ │ └── spawn.js
│ │ │ ├── fs
│ │ │ │ └── mkdirp.js
│ │ │ ├── resolvePath.js
│ │ │ └── onShutdown.js
│ │ ├── wait.js
│ │ ├── system-locale.js
│ │ └── getStreamFromUrl.js
│ ├── index.html
│ ├── app.js
│ ├── package.json
│ └── config.js
├── web_modules
│ ├── ember-get-config.js
│ ├── fetch.js
│ ├── metadata.js
│ ├── ember-app.js
│ ├── transparent-image.js
│ ├── ember-data
│ │ └── version.js
│ ├── snoretoast-binaries.js
│ └── translation-key.js
└── test
│ ├── web_modules
│ ├── htmlbars-inline-precompile.js
│ ├── ember-qunit
│ │ └── test-loader.js
│ ├── require.js
│ ├── ember-test.js
│ ├── event-utils
│ │ └── index.js
│ ├── cartesian-product
│ │ ├── index.js
│ │ └── test.js
│ ├── qunit
│ │ └── index.js
│ └── translation-string
│ │ └── test.js
│ ├── main-coverage.js
│ ├── main-dev.js
│ ├── fixtures
│ ├── data
│ │ └── models
│ │ │ ├── twitch
│ │ │ ├── game-top.yml
│ │ │ ├── search-game.yml
│ │ │ ├── stream-followed.yml
│ │ │ ├── serializer
│ │ │ │ ├── single-response.yml
│ │ │ │ ├── response.yml
│ │ │ │ └── metadata.yml
│ │ │ ├── adapter
│ │ │ │ ├── coalesce-stream-single.yml
│ │ │ │ └── coalesce-various-single.yml
│ │ │ ├── game.yml
│ │ │ └── channel.yml
│ │ │ └── github
│ │ │ └── releases.json
│ ├── services
│ │ └── auth
│ │ │ └── validate-session.yml
│ └── ui
│ │ └── routes
│ │ └── games
│ │ └── index.yml
│ ├── index.html
│ ├── tests
│ ├── services
│ │ ├── notification
│ │ │ └── providers
│ │ │ │ └── auto.js
│ │ └── chat
│ │ │ └── providers
│ │ │ ├── custom.js
│ │ │ └── chrome.js
│ ├── index.js
│ ├── init
│ │ └── initializers
│ │ │ └── localstorage
│ │ │ └── search.js
│ └── ui
│ │ └── components
│ │ └── helper
│ │ └── find-by.js
│ ├── package.json
│ └── dev.css
├── .github
├── FUNDING.yml
├── release_template.md
└── ISSUE_TEMPLATE
│ └── config.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── .codecov.yml
└── .editorconfig
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
--------------------------------------------------------------------------------
/src/config/log.json:
--------------------------------------------------------------------------------
1 | {
2 | "maxAgeDays": 7
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/nwjs/process.js:
--------------------------------------------------------------------------------
1 | export default global.process;
2 |
--------------------------------------------------------------------------------
/src/web_modules/ember-get-config.js:
--------------------------------------------------------------------------------
1 | export default {};
2 |
--------------------------------------------------------------------------------
/src/web_modules/fetch.js:
--------------------------------------------------------------------------------
1 | export default window.fetch;
2 |
--------------------------------------------------------------------------------
/src/app/ui/components/list/headline-totals/template.hbs:
--------------------------------------------------------------------------------
1 | ({{total}})
--------------------------------------------------------------------------------
/src/app/ui/components/modal/modal-dialog/template.hbs:
--------------------------------------------------------------------------------
1 |
{{yield}}
--------------------------------------------------------------------------------
/src/web_modules/metadata.js:
--------------------------------------------------------------------------------
1 | // empty module: see the metadata-loader
2 |
--------------------------------------------------------------------------------
/src/app/locales/ja/themes.yml:
--------------------------------------------------------------------------------
1 | system: システム設定
2 | light: ライト
3 | dark: ダーク
4 |
--------------------------------------------------------------------------------
/src/app/locales/zh-cn/themes.yml:
--------------------------------------------------------------------------------
1 | system: 系统设置
2 | light: 浅色
3 | dark: 深色
4 |
--------------------------------------------------------------------------------
/src/app/locales/zh-tw/themes.yml:
--------------------------------------------------------------------------------
1 | system: 系統設置
2 | light: 浅色
3 | dark: 深色
4 |
--------------------------------------------------------------------------------
/src/web_modules/ember-app.js:
--------------------------------------------------------------------------------
1 | // empty module: see the ember-app-loader
2 |
--------------------------------------------------------------------------------
/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/data/models/stream/adapter.js:
--------------------------------------------------------------------------------
1 | export { default } from "ember-data/adapter";
2 |
--------------------------------------------------------------------------------
/src/app/locales/it/themes.yml:
--------------------------------------------------------------------------------
1 | system: Impostazioni
2 | light: Chiaro
3 | dark: Scuro
4 |
--------------------------------------------------------------------------------
/src/app/ui/components/loading-spinner/template.hbs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/ui/routes/settings/gui/route.js:
--------------------------------------------------------------------------------
1 | export { default } from "../-submenu/route";
2 |
--------------------------------------------------------------------------------
/src/app/locales/en/themes.yml:
--------------------------------------------------------------------------------
1 | system: System settings
2 | light: Light
3 | dark: Dark
4 |
--------------------------------------------------------------------------------
/src/app/ui/components/link/documentation-link/template.hbs:
--------------------------------------------------------------------------------
1 | {{item}}
--------------------------------------------------------------------------------
/src/app/ui/routes/settings/chat/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/main/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/streams/route.js:
--------------------------------------------------------------------------------
1 | export { default } from "../-submenu/route";
2 |
--------------------------------------------------------------------------------
/src/app/locales/de/themes.yml:
--------------------------------------------------------------------------------
1 | system: Systemeinstellungen
2 | light: Hell
3 | dark: Dunkel
4 |
--------------------------------------------------------------------------------
/src/app/locales/es/themes.yml:
--------------------------------------------------------------------------------
1 | system: Ajustes del sistema
2 | light: Claro
3 | dark: Oscuro
4 |
--------------------------------------------------------------------------------
/src/app/locales/fr/themes.yml:
--------------------------------------------------------------------------------
1 | system: Réglages du système
2 | light: Clair
3 | dark: Sombre
4 |
--------------------------------------------------------------------------------
/src/app/locales/pt-br/themes.yml:
--------------------------------------------------------------------------------
1 | system: Definições do sistema
2 | light: Light
3 | dark: Dark
4 |
--------------------------------------------------------------------------------
/src/app/locales/ru/themes.yml:
--------------------------------------------------------------------------------
1 | system: Настройки системы
2 | light: Светлая
3 | dark: Темная
4 |
--------------------------------------------------------------------------------
/src/app/ui/components/helper/t.js:
--------------------------------------------------------------------------------
1 | export { default as helper } from "ember-intl/helpers/t";
2 |
--------------------------------------------------------------------------------
/src/app/ui/routes/channel/loading/route.js:
--------------------------------------------------------------------------------
1 | export { default } from "ui/routes/loading/route";
2 |
--------------------------------------------------------------------------------
/src/app/ui/routes/games/loading/route.js:
--------------------------------------------------------------------------------
1 | export { default } from "ui/routes/loading/route";
2 |
--------------------------------------------------------------------------------
/src/app/ui/routes/settings/notifications/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/team/loading/route.js:
--------------------------------------------------------------------------------
1 | export { default } from "ui/routes/loading/route";
2 |
--------------------------------------------------------------------------------
/src/app/ui/routes/user/loading/route.js:
--------------------------------------------------------------------------------
1 | export { default } from "ui/routes/loading/route";
2 |
--------------------------------------------------------------------------------
/src/app/data/models/application/adapter.js:
--------------------------------------------------------------------------------
1 | export { default } from "data/models/twitch/adapter";
2 |
--------------------------------------------------------------------------------
/src/app/locales/ja/qualities.yml:
--------------------------------------------------------------------------------
1 | source: ソース
2 | high: 高
3 | medium: 中
4 | low: 低
5 | audio: 音声のみ
6 |
--------------------------------------------------------------------------------
/src/app/locales/zh-cn/qualities.yml:
--------------------------------------------------------------------------------
1 | source: 来源
2 | high: 高
3 | medium: 中
4 | low: 低
5 | audio: 仅音频
6 |
--------------------------------------------------------------------------------
/src/app/locales/zh-tw/qualities.yml:
--------------------------------------------------------------------------------
1 | source: 來源
2 | high: 高
3 | medium: 中
4 | low: 低
5 | audio: 僅有語音
6 |
--------------------------------------------------------------------------------
/src/test/web_modules/htmlbars-inline-precompile.js:
--------------------------------------------------------------------------------
1 | export { hbs as default } from "./test-utils";
2 |
--------------------------------------------------------------------------------
/src/app/init/initializers/ember-data.js:
--------------------------------------------------------------------------------
1 | export { default } from "ember-data/app/initializers/ember-data";
2 |
--------------------------------------------------------------------------------
/src/app/ui/routes/loading/template.hbs:
--------------------------------------------------------------------------------
1 |
2 | {{loading-spinner}}
3 |
--------------------------------------------------------------------------------
/src/test/web_modules/ember-qunit/test-loader.js:
--------------------------------------------------------------------------------
1 | export class TestLoader {}
2 |
3 | export function loadTests() {}
4 |
--------------------------------------------------------------------------------
/src/test/web_modules/require.js:
--------------------------------------------------------------------------------
1 | import Ember from "ember";
2 |
3 |
4 | export default Ember.__loader.require;
5 |
--------------------------------------------------------------------------------
/src/app/data/models/auth/serializer.js:
--------------------------------------------------------------------------------
1 | export { default } from "ember-localstorage-adapter/serializers/ls-serializer";
2 |
--------------------------------------------------------------------------------
/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/en/qualities.yml:
--------------------------------------------------------------------------------
1 | source: Source
2 | high: High
3 | medium: Medium
4 | low: Low
5 | audio: Audio only
6 |
--------------------------------------------------------------------------------
/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/fr/qualities.yml:
--------------------------------------------------------------------------------
1 | source: Source
2 | high: Haute
3 | medium: Moyenne
4 | low: Basse
5 | audio: Audio
6 |
--------------------------------------------------------------------------------
/src/app/data/models/search/serializer.js:
--------------------------------------------------------------------------------
1 | export { default } from "ember-localstorage-adapter/serializers/ls-serializer";
2 |
--------------------------------------------------------------------------------
/src/app/data/models/settings/serializer.js:
--------------------------------------------------------------------------------
1 | export { default } from "ember-localstorage-adapter/serializers/ls-serializer";
2 |
--------------------------------------------------------------------------------
/src/app/data/models/window/serializer.js:
--------------------------------------------------------------------------------
1 | export { default } from "ember-localstorage-adapter/serializers/ls-serializer";
2 |
--------------------------------------------------------------------------------
/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/pt-br/qualities.yml:
--------------------------------------------------------------------------------
1 | source: Original
2 | high: Alta
3 | medium: Média
4 | low: Baixa
5 | audio: Apenas áudio
6 |
--------------------------------------------------------------------------------
/src/app/ui/components/stream/stream-preview-image/template.hbs:
--------------------------------------------------------------------------------
1 | {{preview-image src=src}}
2 | {{#if hasBlock}}{{yield}}{{/if}}
--------------------------------------------------------------------------------
/src/app/assets/icons/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/HEAD/src/app/assets/icons/icon-16.png
--------------------------------------------------------------------------------
/src/app/data/models/versioncheck/serializer.js:
--------------------------------------------------------------------------------
1 | export { default } from "ember-localstorage-adapter/serializers/ls-serializer";
2 |
--------------------------------------------------------------------------------
/src/app/locales/ru/qualities.yml:
--------------------------------------------------------------------------------
1 | source: Источник
2 | high: Высокое
3 | medium: Среднее
4 | low: Низкое
5 | audio: Только звук
6 |
--------------------------------------------------------------------------------
/src/app/ui/components/button/channel-button/styles.less:
--------------------------------------------------------------------------------
1 | .channel-button-component > i {
2 | transform: translateY(1px);
3 | }
4 |
--------------------------------------------------------------------------------
/src/web_modules/transparent-image.js:
--------------------------------------------------------------------------------
1 | export default "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
2 |
--------------------------------------------------------------------------------
/src/app/assets/icons/icon-16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/HEAD/src/app/assets/icons/icon-16@2x.png
--------------------------------------------------------------------------------
/src/app/assets/icons/icon-16@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/HEAD/src/app/assets/icons/icon-16@3x.png
--------------------------------------------------------------------------------
/src/app/assets/icons/icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/HEAD/src/app/assets/icons/icon-256.png
--------------------------------------------------------------------------------
/src/app/data/models/channel-settings/serializer.js:
--------------------------------------------------------------------------------
1 | export { default } from "ember-localstorage-adapter/serializers/ls-serializer";
2 |
--------------------------------------------------------------------------------
/src/app/ui/components/list/game-item/template.hbs:
--------------------------------------------------------------------------------
1 | {{preview-image src=content.box_art_url.latest}}
2 |
--------------------------------------------------------------------------------
/src/app/assets/icons/icon-osx-18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/HEAD/src/app/assets/icons/icon-osx-18.png
--------------------------------------------------------------------------------
/src/web_modules/ember-data/version.js:
--------------------------------------------------------------------------------
1 | import metadata from "metadata";
2 |
3 |
4 | export default metadata.dependencies[ "ember-data" ];
5 |
--------------------------------------------------------------------------------
/src/app/assets/icons/icon-osx-18@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/HEAD/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/HEAD/src/app/assets/icons/icon-osx-18@3x.png
--------------------------------------------------------------------------------
/src/app/utils/is-focused.js:
--------------------------------------------------------------------------------
1 | export default function isFocused( element ) {
2 | return element.ownerDocument.activeElement === element;
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/init/initializers/localstorage/localstorage.js:
--------------------------------------------------------------------------------
1 | import nwWindow from "nwjs/Window";
2 |
3 |
4 | export default nwWindow.window.localStorage;
5 |
--------------------------------------------------------------------------------
/src/app/nwjs/debug.js:
--------------------------------------------------------------------------------
1 | /* globals DEBUG, DEVELOPMENT */
2 | export const isDebug = DEBUG || DEVELOPMENT;
3 | export const isDevelopment = DEVELOPMENT;
4 |
--------------------------------------------------------------------------------
/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/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/user/controller.js:
--------------------------------------------------------------------------------
1 | import Controller from "@ember/controller";
2 | import "./styles.less";
3 |
4 |
5 | export default Controller;
6 |
--------------------------------------------------------------------------------
/src/app/assets/images/Twitch_Logo_Purple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/streamlink/streamlink-twitch-gui/HEAD/src/app/assets/images/Twitch_Logo_Purple.png
--------------------------------------------------------------------------------
/src/app/ui/routes/application/template.hbs:
--------------------------------------------------------------------------------
1 | {{title-bar}}
2 |
3 | {{main-menu}}
4 | {{outlet}}
5 |
6 | {{modal-service}}
--------------------------------------------------------------------------------
/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/loading/controller.js:
--------------------------------------------------------------------------------
1 | import Controller from "@ember/controller";
2 | import "./styles.less";
3 |
4 |
5 | export default Controller;
6 |
--------------------------------------------------------------------------------
/src/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Streamlink Twitch GUI
6 |
7 |
8 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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-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/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | QUnit
6 |
7 |
8 |
9 |
10 |
13 |
14 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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/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/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-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/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/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/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/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/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-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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/number-field/template.hbs:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/ui/routes/settings/channels/template.hbs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 |
19 | {{/if}}
20 |
--------------------------------------------------------------------------------
/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/ui/routes/watching/styles.less:
--------------------------------------------------------------------------------
1 | @import (reference) "ui/styles/mixins";
2 |
3 |
4 | .content-watching {
5 | .list-empty {
6 | margin-top: .5em;
7 |
8 | a {
9 | text-decoration: none;
10 | }
11 | }
12 |
13 | .stream-presentation-component {
14 | margin-bottom: 2rem;
15 |
16 | > .image {
17 | > .preview {
18 | .block-aspect-ratio( ( 16 / 9 ) );
19 | }
20 | }
21 |
22 | > .info {
23 | display: flex;
24 | flex-flow: column nowrap;
25 | justify-content: space-between;
26 |
27 | > :not(.status) {
28 | flex-grow: 0;
29 | }
30 |
31 | > .status {
32 | flex: 1 1;
33 | margin: .5em 0;
34 | overflow: auto;
35 | word-break: break-word;
36 | }
37 |
38 | > .buttons {
39 | display: flex;
40 | justify-content: space-between;
41 | width: 20rem;
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/app/ui/components/button/open-chat/component.js:
--------------------------------------------------------------------------------
1 | import { inject as service } from "@ember/service";
2 | import { t } from "ember-intl";
3 | import FormButtonComponent from "../form-button/component";
4 | import HotkeyMixin from "ui/components/-mixins/hotkey";
5 |
6 |
7 | export default FormButtonComponent.extend( HotkeyMixin, {
8 | /** @type {IntlService} */
9 | intl: service(),
10 | /** @type {ChatService} */
11 | chat: service(),
12 |
13 | /** @type {TwitchUser} */
14 | user: null,
15 |
16 | classNames: [ "open-chat-component", "btn-hint" ],
17 | icon: "fa-comments",
18 | _title: t( "components.open-chat.title" ),
19 | iconanim: true,
20 |
21 | hotkeysNamespace: "openchatbutton",
22 | hotkeys: {
23 | default() {
24 | this.click();
25 | }
26 | },
27 |
28 | action() {
29 | return this.chat.openChat( this.user.login );
30 | }
31 | });
32 |
--------------------------------------------------------------------------------
/src/app/ui/components/form/drop-down/template.hbs:
--------------------------------------------------------------------------------
1 | {{#if hasBlock}}
2 | {{yield (hash
3 | selection=(component "drop-down-selection"
4 | selection=selection
5 | optionValuePath=optionValuePath
6 | optionLabelPath=optionLabelPath
7 | action=(action "toggle")
8 | )
9 | list=(component "drop-down-list"
10 | expanded=expanded
11 | content=content
12 | selection=selection
13 | optionValuePath=optionValuePath
14 | optionLabelPath=optionLabelPath
15 | )
16 | )}}
17 | {{else}}
18 | {{drop-down-selection
19 | selection=selection
20 | optionValuePath=optionValuePath
21 | optionLabelPath=optionLabelPath
22 | action=(action "toggle")
23 | }}
24 | {{drop-down-list
25 | expanded=expanded
26 | content=content
27 | selection=selection
28 | optionValuePath=optionValuePath
29 | optionLabelPath=optionLabelPath
30 | }}
31 | {{/if}}
--------------------------------------------------------------------------------
/src/app/ui/components/main-menu/template.hbs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/ui/routes/settings/hotkeys/template.hbs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/data/models/settings/streaming/player/fragment.js:
--------------------------------------------------------------------------------
1 | import attr from "ember-data/attr";
2 | import Fragment from "ember-data-model-fragments/fragment";
3 | import { players as playersConfig } from "config";
4 |
5 |
6 | const typeKey = "type";
7 | const players = new Map();
8 |
9 |
10 | const SettingsStreamingPlayer = Fragment.extend({
11 | exec: attr( "string" ),
12 | args: attr( "string" )
13 | });
14 |
15 |
16 | for ( const [ id, { params } ] of Object.entries( playersConfig ) ) {
17 | const attributes = {};
18 | for ( const { name, type, default: defaultValue } of params ) {
19 | attributes[ name ] = attr( type, { defaultValue } );
20 | }
21 | const player = SettingsStreamingPlayer.extend( attributes );
22 | players.set( id, player );
23 | }
24 |
25 |
26 | export {
27 | typeKey,
28 | players,
29 | SettingsStreamingPlayer as default
30 | };
31 |
--------------------------------------------------------------------------------
/src/app/ui/routes/settings/hotkeys/controller.js:
--------------------------------------------------------------------------------
1 | import Controller from "@ember/controller";
2 | import { computed } from "@ember/object";
3 | import { hotkeys as hotkeysConfig } from "config";
4 |
5 |
6 | export default Controller.extend({
7 | // filter out alias-actions
8 | defaultHotkeys: computed(function() {
9 | const namespaces = {};
10 | for ( const [ name, { icon, actions: data } ] of Object.entries( hotkeysConfig ) ) {
11 | const actions = {};
12 | for ( const [ action, hotkeys ] of Object.entries( data ) ) {
13 | if ( typeof hotkeys === "string" ) { continue; }
14 | const [ primary, secondary = null ] = hotkeys;
15 | actions[ action ] = { primary, secondary };
16 | }
17 | if ( !Object.keys( actions ).length ) { continue; }
18 | namespaces[ name ] = { icon, actions };
19 | }
20 |
21 | return namespaces;
22 | })
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/services/settings.js:
--------------------------------------------------------------------------------
1 | import { get, set } from "@ember/object";
2 | import Evented from "@ember/object/evented";
3 | import ObjectProxy from "@ember/object/proxy";
4 | import { inject as service } from "@ember/service";
5 |
6 |
7 | // A service object is just a regular object, so we can use an ObjectProxy as well
8 | export default ObjectProxy.extend( Evented, {
9 | store: service(),
10 |
11 | content: null,
12 |
13 | init() {
14 | const store = get( this, "store" );
15 | // don't use async functions here and use Ember RSVP promises instead
16 | store.findOrCreateRecord( "settings" )
17 | .then( settings => {
18 | set( this, "content", settings );
19 | settings.on( "didUpdate", ( ...args ) => this.trigger( "didUpdate", ...args ) );
20 | this.trigger( "initialized" );
21 | });
22 | }
23 |
24 | }).reopenClass({
25 | isServiceFactory: true
26 | });
27 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 |
7 | # global file encoding
8 | [*]
9 | charset = utf-8
10 | end_of_line = lf
11 |
12 | # javascript and stylesheets
13 | [**.{js,json,less}]
14 | indent_size = 4
15 | indent_style = tab
16 | insert_final_newline = true
17 | trim_trailing_whitespace = true
18 |
19 | # markup files
20 | [**.{html,hbs}]
21 | indent_size = 4
22 | insert_final_newline = false
23 |
24 | # xml files
25 | [**.xml]
26 | indent_size = 2
27 | indent_style = space
28 | insert_final_newline = true
29 | trim_trailing_whitespace = true
30 |
31 | # markdown files
32 | [**.md]
33 | trim_trailing_whitespace = false
34 |
35 | # yaml configs
36 | [**.yml]
37 | indent_size = 4
38 | indent_style = space
39 | insert_final_newline = true
40 | trim_trailing_whitespace = true
41 |
42 | [.github/**.yml]
43 | indent_size = 2
44 |
--------------------------------------------------------------------------------
/src/test/fixtures/data/models/twitch/adapter/coalesce-various-single.yml:
--------------------------------------------------------------------------------
1 | stream:
2 | request:
3 | method: "GET"
4 | url: "https://api.twitch.tv/helix/streams"
5 | query:
6 | - name: "user_id"
7 | value: "1"
8 | - name: "user_id"
9 | value: "2"
10 | response:
11 | data:
12 | - id: "1"
13 | user_id: "1"
14 | game_id: "1"
15 | - id: "2"
16 | user_id: "2"
17 | game_id: "2"
18 | user:
19 | request:
20 | method: "GET"
21 | url: "https://api.twitch.tv/helix/users"
22 | query:
23 | - name: "id"
24 | value: "1"
25 | - name: "id"
26 | value: "2"
27 | response:
28 | data:
29 | - id: "1"
30 | - id: "2"
31 |
--------------------------------------------------------------------------------
/src/app/services/chat/providers/chromium.js:
--------------------------------------------------------------------------------
1 | import ChatProviderBasic from "./-basic";
2 | import ParameterCustom from "utils/parameters/ParameterCustom";
3 | import Substitution from "utils/parameters/Substitution";
4 |
5 |
6 | /**
7 | * @class ChatProviderChromium
8 | * @implements ChatProviderBasic
9 | */
10 | export default class ChatProviderChromium extends ChatProviderBasic {
11 | // noinspection JSCheckFunctionSignatures
12 | async _getContext() {
13 | const context = await super._getContext( ...arguments );
14 | context.chromiumArgs = "\"--app={url}\"";
15 |
16 | return context;
17 | }
18 |
19 | async _getParameters() {
20 | const parameters = await super._getParameters( ...arguments );
21 | parameters.unshift(
22 | new ParameterCustom( null, "chromiumArgs", [
23 | new Substitution( "url", "url" )
24 | ])
25 | );
26 |
27 | return parameters;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/.github/release_template.md:
--------------------------------------------------------------------------------
1 | [<%= display_name %>](<%= homepage %>)
2 | ===
3 |
4 | ## 🎉 Release highlights (v<%= version %>)
5 |
6 | <%= changelog %>
7 |
8 | ## ⚙️ Installation and Configuration
9 |
10 | See [the project's wiki](https://github.com/streamlink/streamlink-twitch-gui/wiki) for detailed installation and configuration guides.
11 |
12 | <% if ( donation && donation.length ) { %>
13 | ## ❤️ Support
14 |
15 | If you think that Streamlink Twitch GUI is useful and if you want to keep the project alive, then please consider supporting its creator/maintainer by sending a small and optionally recurring tip via the available options listed below.
16 | Your support is very much appreciated, thank you!
17 |
18 | <% JSON.parse( donation ).forEach(function( item ) { %>* [<%= item.text %>](<%= item.url %>)<% if ( item.coinaddress ) { %> (`<%= item.coinaddress %>`)<% } %>
19 | <% }) %>
20 | <% } %>
21 |
--------------------------------------------------------------------------------
/src/app/config.js:
--------------------------------------------------------------------------------
1 | export { default as main } from "../config/main.json";
2 | export { default as log } from "../config/log.json";
3 | export { default as locales } from "../config/locales.json";
4 | export { default as files } from "parse-json-loader!../config/files.json";
5 | export { default as vars } from "../config/vars.json";
6 | export { default as update } from "../config/update.json";
7 | export { default as themes } from "../config/themes.json";
8 | export { default as hotkeys } from "../config/hotkeys.json";
9 | export { default as langs } from "../config/langs.yml";
10 | export { default as streaming } from "../config/streaming.json";
11 | export { default as players } from "../config/players.json";
12 | export { default as twitch } from "../config/twitch.json";
13 | export { default as notification } from "../config/notification.json";
14 | export { default as chat } from "../config/chat.json";
15 |
--------------------------------------------------------------------------------
/src/app/ui/components/modal/modal-confirm/template.hbs:
--------------------------------------------------------------------------------
1 |
2 | {{#modal-header}}
3 | {{t "modal.confirm.header"}}
4 | {{/modal-header}}
5 | {{#modal-body}}
6 | {{t "modal.confirm.body"}}
7 | {{/modal-body}}
8 | {{#modal-footer classNames="button-list-horizontal"}}
9 | {{#form-button
10 | action=(action "apply")
11 | classNames="btn-success"
12 | icon="fa-check"
13 | iconanim=true
14 | }}
15 | {{t "modal.confirm.action.apply"}}
16 | {{/form-button}}
17 | {{#form-button
18 | action=(action "discard")
19 | classNames="btn-danger"
20 | icon="fa-trash-o"
21 | iconanim=true
22 | }}
23 | {{t "modal.confirm.action.discard"}}
24 | {{/form-button}}
25 | {{#form-button
26 | action=(action "cancel")
27 | classNames="btn-neutral"
28 | icon="fa-times"
29 | }}
30 | {{t "modal.confirm.action.cancel"}}
31 | {{/form-button}}
32 | {{/modal-footer}}
33 |
--------------------------------------------------------------------------------
/src/test/tests/ui/components/helper/find-by.js:
--------------------------------------------------------------------------------
1 | import { module, test } from "qunit";
2 | import { setupRenderingTest } from "ember-qunit";
3 | import { buildResolver } from "test-utils";
4 | import { render } from "@ember/test-helpers";
5 | import hbs from "htmlbars-inline-precompile";
6 |
7 | import { helper as FindByHelper } from "ui/components/helper/find-by";
8 |
9 |
10 | module( "ui/components/helper/find-by", function( hooks ) {
11 | setupRenderingTest( hooks, {
12 | resolver: buildResolver({
13 | FindByHelper
14 | })
15 | });
16 |
17 |
18 | test( "FindByHelper", async function( assert ) {
19 | this.setProperties({
20 | arr: [
21 | { foo: 1, bar: "one" },
22 | { foo: 2, bar: "two" }
23 | ]
24 | });
25 | await render( hbs`{{get (find-by arr "foo" 2) "bar"}}` );
26 |
27 | assert.strictEqual( this.element.innerText, "two", "Finds two" );
28 | });
29 |
30 | });
31 |
--------------------------------------------------------------------------------
/src/app/locales/de/languages.yml:
--------------------------------------------------------------------------------
1 | ar: Arabisch
2 | asl: Amerikanische Gebärdensprache
3 | bg: Bulgarisch
4 | ca: Katalanisch
5 | cs: Tschechisch
6 | da: Dänisch
7 | de: Deutsch
8 | el: Griechisch
9 | en: Englisch
10 | en-gb: Britisch
11 | en-us: Amerikanisch
12 | es: Spanisch
13 | es-mx: Mexikanisch
14 | fi: Finnisch
15 | fr: Französisch
16 | hi: Hindi
17 | hu: Ungarisch
18 | # special uppercase language code
19 | ID: Indonesisch
20 | it: Italienisch
21 | ja: Japanisch
22 | ko: Koreanisch
23 | ms: Malayisch
24 | nl: Niederländisch
25 | no: Norwegisch
26 | other: "\"Andere\""
27 | pl: Polnisch
28 | pt: Portugiesisch
29 | pt-br: Brazilianisch
30 | ro: Rumänisch
31 | ru: Russisch
32 | sk: Slovakisch
33 | sv: Schwedisch
34 | tl: Tagalog
35 | th: Thailändisch
36 | tr: Türkisch
37 | uk: Ukrainisch
38 | vi: Vietnamesisch
39 | zh: Chinesisch
40 | zh-cn: Chinesisch
41 | zh-hk: Kantonesisch
42 | zh-tw: Chinesisch (traditionell)
43 |
--------------------------------------------------------------------------------
/src/app/ui/routes/user/followed-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 RefreshRouteMixin from "ui/routes/-mixins/routes/refresh";
4 |
5 |
6 | export default UserIndexRoute.extend( PaginationMixin, RefreshRouteMixin, {
7 | modelName: "twitch-stream-followed",
8 | modelMapBy: "stream",
9 | modelPreload: "thumbnail_url.latest",
10 | itemSelector: ".stream-item-component",
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 | query() {
24 | const query = this._super();
25 | const { user_id } = this.auth.session;
26 |
27 | return Object.assign( query, { user_id } );
28 | }
29 | });
30 |
--------------------------------------------------------------------------------
/src/app/services/notification/providers/rich.js:
--------------------------------------------------------------------------------
1 | import NotificationProviderChromeNotifications from "./chrome-notifications";
2 | import { isWin7 } from "utils/node/platform";
3 |
4 |
5 | /**
6 | * Chromium Rich Notifications
7 | * https://developer.chrome.com/apps/richNotifications
8 | *
9 | * Used on Windows 7, due to the lack of a native notification system.
10 | * Native notifications on Windows 8 and above require the snoretoast provider as long as Chromium
11 | * doesn't support them. Rich notifications are being used as a fallback.
12 | *
13 | * @class NotificationProviderRich
14 | * @implements NotificationProviderChromeNotifications
15 | * @implements NotificationProvider
16 | */
17 | export default class NotificationProviderRich extends NotificationProviderChromeNotifications {
18 | static isSupported() {
19 | return isWin7;
20 | }
21 |
22 | static supportsListNotifications() {
23 | return true;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: "Streamlink"
4 | about: "This is the Streamlink Twitch GUI repository. Anything Streamlink related belongs on the Streamlink repository."
5 | url: https://github.com/streamlink/streamlink
6 | - name: "Help"
7 | about: "The issue tracker is only meant for bug reports and feature requests. Please ask your questions on the discussions forum and look for already existing answers."
8 | url: https://github.com/streamlink/streamlink-twitch-gui/discussions
9 | - name: "Wiki"
10 | about: "Please refer to Streamlink Twitch GUI's wiki first before opening new issues or asking questions."
11 | url: https://github.com/streamlink/streamlink-twitch-gui/wiki
12 | - name: "Gitter Matrix channel"
13 | about: "Get in touch with other Streamlink Twitch GUI users."
14 | url: https://matrix.to/#/#streamlink_streamlink-twitch-gui:gitter.im
15 |
--------------------------------------------------------------------------------
/src/app/ui/components/selectable-text/component.js:
--------------------------------------------------------------------------------
1 | import Component from "@ember/component";
2 | import { inject as service } from "@ember/service";
3 | import t from "translation-key";
4 |
5 |
6 | export default Component.extend({
7 | /** @type {NwjsService} */
8 | nwjs: service(),
9 |
10 | tagName: "div",
11 |
12 | classNameBindings: [ "class" ],
13 | attributeBindings: [ "selectable:data-selectable" ],
14 |
15 | "class" : "",
16 | selectable: true,
17 |
18 | contextMenu( event ) {
19 | if ( this.attrs.noContextmenu ) { return; }
20 |
21 | const selection = window.getSelection();
22 | const selected = selection.toString();
23 |
24 | if ( !selected.length && event.target.tagName === "A" ) { return; }
25 |
26 | this.nwjs.contextMenu( event, [{
27 | label: [ t`contextmenu.copy-selection` ],
28 | enabled: selected.length,
29 | click() {
30 | this.nwjs.clipboard.set( selected );
31 | }
32 | }] );
33 | }
34 | });
35 |
--------------------------------------------------------------------------------
/src/app/data/models/twitch/search-game/serializer.js:
--------------------------------------------------------------------------------
1 | import TwitchSerializer from "data/models/twitch/serializer";
2 |
3 |
4 | const reStaticBoxArtRes = /\d{1,10}x\d{1,10}\.(\w+)$/;
5 |
6 |
7 | export default TwitchSerializer.extend({
8 | modelNameFromPayloadKey() {
9 | return "twitch-search-game";
10 | },
11 |
12 | attrs: {
13 | game: { deserialize: "records" }
14 | },
15 |
16 | normalize( modelClass, resourceHash, prop ) {
17 | const { primaryKey } = this;
18 |
19 | // workaround for: https://github.com/twitchdev/issues/issues/329
20 | /* istanbul ignore next */
21 | if ( resourceHash[ "box_art_url" ] ) {
22 | resourceHash[ "box_art_url" ] = `${resourceHash[ "box_art_url" ]}`
23 | .replace( reStaticBoxArtRes, "{width}x{height}.$1" );
24 | }
25 |
26 | resourceHash = {
27 | [ primaryKey ]: resourceHash[ primaryKey ],
28 | game: resourceHash
29 | };
30 |
31 | return this._super( modelClass, resourceHash, prop );
32 | }
33 | });
34 |
--------------------------------------------------------------------------------
/src/app/ui/routes/settings/channels/controller.js:
--------------------------------------------------------------------------------
1 | import Controller from "@ember/controller";
2 | import { computed } from "@ember/object";
3 | import { run } from "@ember/runloop";
4 |
5 |
6 | const reFilter = /^\w+$/;
7 |
8 |
9 | export default Controller.extend({
10 | filter: "",
11 |
12 | modelFiltered: computed( "model.[]", "all", "filter", function() {
13 | const filter = this.filter.toLowerCase();
14 | if ( !reFilter.test( filter ) ) {
15 | return this.model;
16 | }
17 |
18 | return this.all.filter( item => item.settings.id
19 | .toLowerCase()
20 | .indexOf( filter ) !== -1
21 | );
22 | }),
23 |
24 |
25 | actions: {
26 | erase( modelItem ) {
27 | const { settings: settingsRecord } = modelItem;
28 | if ( settingsRecord.isDeleted ) { return; }
29 |
30 | const { model } = this;
31 | run( () => settingsRecord.destroyRecord() )
32 | .then( () => {
33 | model.removeObject( modelItem );
34 | });
35 | }
36 | }
37 | });
38 |
--------------------------------------------------------------------------------
/src/test/fixtures/ui/routes/games/index.yml:
--------------------------------------------------------------------------------
1 | - request:
2 | method: "GET"
3 | url: "https://api.twitch.tv/helix/games/top"
4 | query:
5 | first: 2
6 | response:
7 | data:
8 | - id: "1"
9 | name: "first game"
10 | box_art_url: "https://mock/twitch-game-top/1/box_art-{width}x{height}.png"
11 | - id: "2"
12 | name: "second game"
13 | box_art_url: "https://mock/twitch-game-top/2/box_art-{width}x{height}.png"
14 | pagination:
15 | cursor: "cursor1"
16 | - request:
17 | method: "GET"
18 | url: "https://api.twitch.tv/helix/games/top"
19 | query:
20 | first: 2
21 | after: "cursor1"
22 | response:
23 | data:
24 | - id: "3"
25 | name: "third game"
26 | box_art_url: "https://mock/twitch-game-top/3/box_art-{width}x{height}.png"
27 |
--------------------------------------------------------------------------------
/src/app/init/initializers/model-fragments.js:
--------------------------------------------------------------------------------
1 | import metadata from "metadata";
2 | import FragmentTransform from "ember-data-model-fragments/transforms/fragment";
3 | import FragmentArrayTransform from "ember-data-model-fragments/transforms/fragment-array";
4 | import ArrayTransform from "ember-data-model-fragments/transforms/array";
5 |
6 |
7 | const Ember = window.Ember || {};
8 | if ( Ember.libraries ) {
9 | const version = metadata.dependencies[ "ember-data-model-fragments" ];
10 | Ember.libraries.register( "Model Fragments", version );
11 | }
12 |
13 |
14 | export default {
15 | name: "fragmentTransform",
16 | before: "ember-data",
17 |
18 | initialize( application ) {
19 | application.inject( "transform", "store", "service:store" );
20 | application.register( "transform:fragment", FragmentTransform );
21 | application.register( "transform:fragment-array", FragmentArrayTransform );
22 | application.register( "transform:array", ArrayTransform );
23 | }
24 | };
25 |
--------------------------------------------------------------------------------
/src/app/utils/node/onShutdown.js:
--------------------------------------------------------------------------------
1 | import Process from "nwjs/process";
2 | import nwWindow from "nwjs/Window";
3 |
4 |
5 | /**
6 | * @param {Function} callback
7 | * @returns {Function}
8 | */
9 | function onShutdown( callback ) {
10 | const wrapper = () => {
11 | callback();
12 | unregister();
13 | };
14 |
15 | const unregister = () => {
16 | Process.removeListener( "exit", wrapper );
17 | Process.removeListener( "SIGHUP", wrapper );
18 | Process.removeListener( "SIGINT", wrapper );
19 | Process.removeListener( "SIGTERM", wrapper );
20 | nwWindow.window.removeEventListener( "beforeunload", wrapper, false );
21 | };
22 |
23 | Process.addListener( "exit", wrapper );
24 | Process.addListener( "SIGHUP", wrapper );
25 | Process.addListener( "SIGINT", wrapper );
26 | Process.addListener( "SIGTERM", wrapper );
27 | nwWindow.window.addEventListener( "beforeunload", wrapper, false );
28 |
29 | return unregister;
30 | }
31 |
32 |
33 | export default onShutdown;
34 |
--------------------------------------------------------------------------------