?} env
6 | */
7 | constructor( exec = null, params = null, env = null ) {
8 | this.exec = exec;
9 | this.params = params;
10 | this.env = env;
11 | }
12 |
13 | /**
14 | * @param {ExecObj} execObj
15 | */
16 | merge( execObj ) {
17 | if ( execObj.exec ) {
18 | this.exec = execObj.exec;
19 | }
20 | if ( execObj.params ) {
21 | this.params = execObj.params;
22 | }
23 | if ( execObj.env ) {
24 | this.env = execObj.env;
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/services/streaming/is-aborted.js:
--------------------------------------------------------------------------------
1 | import { get } from "@ember/object";
2 | import { Aborted } from "./errors";
3 |
4 |
5 | export default function( stream ) {
6 | if ( get( stream, "isAborted" ) ) {
7 | // remove the record from the store
8 | if ( !get( stream, "isDeleted" ) ) {
9 | stream.destroyRecord();
10 | }
11 |
12 | throw new Aborted();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/services/streaming/launch/parse-error.js:
--------------------------------------------------------------------------------
1 | import {
2 | PlayerError,
3 | UnableToOpenError,
4 | NoStreamsFoundError,
5 | TimeoutError,
6 | Warning
7 | } from "../errors";
8 |
9 |
10 | const errors = [
11 | PlayerError,
12 | UnableToOpenError,
13 | NoStreamsFoundError,
14 | TimeoutError,
15 | Warning
16 | ];
17 |
18 |
19 | /**
20 | * @param {String} data
21 | * @returns {(Error|null)}
22 | */
23 | export default function( data ) {
24 | for ( let ErrorClass of errors ) {
25 | for ( let regex of ErrorClass.regex ) {
26 | const match = regex.exec( data );
27 | if ( match ) {
28 | return new ErrorClass( ...match );
29 | }
30 | }
31 | }
32 | return null;
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/services/streaming/logger.js:
--------------------------------------------------------------------------------
1 | import Logger from "utils/Logger";
2 |
3 |
4 | const { logDebug, logError } = new Logger( "StreamingService" );
5 |
6 |
7 | export {
8 | logDebug,
9 | logError
10 | };
11 |
--------------------------------------------------------------------------------
/src/app/services/streaming/spawn.js:
--------------------------------------------------------------------------------
1 | import { logDebug } from "./logger";
2 | import spawn from "utils/node/child_process/spawn";
3 |
4 |
5 | const { assign } = Object;
6 |
7 |
8 | /**
9 | * @param {ExecObj} execObj
10 | * @param {String[]} additonalParams
11 | * @param {Object} options
12 | * @returns {ChildProcess}
13 | */
14 | export default function( execObj, additonalParams = [], options = {} ) {
15 | let { exec, params, env } = execObj;
16 |
17 | params = [
18 | ...( params || [] ),
19 | ...( additonalParams || [] )
20 | ];
21 |
22 | if ( env ) {
23 | options.env = assign( {}, options.env, env );
24 | }
25 |
26 | logDebug( "Spawning process", {
27 | exec,
28 | params,
29 | env
30 | });
31 |
32 | return spawn( exec, params, options );
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/ui/components/button/channel-button/styles.less:
--------------------------------------------------------------------------------
1 | .channel-button-component > i {
2 | transform: translateY(1px);
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/ui/components/button/form-button/template.hbs:
--------------------------------------------------------------------------------
1 | {{#if hasBlock}}{{_setHasBlock}}{{/if}}
2 | {{#if icon}}
3 | {{#if isLoading}}
4 | {{loading-spinner}}
5 | {{else}}
6 |
7 | {{/if}}
8 | {{/if}}
9 | {{#if hasBlock}}{{yield}}{{/if}}
--------------------------------------------------------------------------------
/src/app/ui/components/form/-input-btn/template.hbs:
--------------------------------------------------------------------------------
1 |
2 | {{#if hasBlock}}
3 | {{_setHasBlock}}
4 |
5 |
{{yield}}
6 | {{#if (hasBlock "inverse")}}
7 |
{{yield to="inverse"}}
8 | {{/if}}
9 |
10 | {{else if label}}
11 |
12 |
{{label}}
13 | {{#if description}}
14 |
{{description}}
15 | {{/if}}
16 |
17 | {{/if}}
--------------------------------------------------------------------------------
/src/app/ui/components/form/check-box/component.js:
--------------------------------------------------------------------------------
1 | import { get } from "@ember/object";
2 | import InputBtnComponent from "../-input-btn/component";
3 |
4 |
5 | export default InputBtnComponent.extend({
6 | classNames: [ "check-box-component" ],
7 |
8 | click() {
9 | if ( get( this, "disabled" ) ) { return; }
10 | this.toggleProperty( "checked" );
11 | }
12 | });
13 |
--------------------------------------------------------------------------------
/src/app/ui/components/form/drop-down-list/template.hbs:
--------------------------------------------------------------------------------
1 | {{#if hasBlock}}
2 | {{#each content as |item|}}
3 |
4 | {{yield (hash
5 | isSelected=(is-equal item selection)
6 | label=(get item optionLabelPath)
7 | value=(get item optionValuePath)
8 | )}}
9 |
10 | {{/each}}
11 | {{else}}
12 | {{#each content as |item|}}
13 |
14 | {{get item optionLabelPath}}
15 |
16 | {{/each}}
17 | {{/if}}
--------------------------------------------------------------------------------
/src/app/ui/components/form/drop-down-selection/component.js:
--------------------------------------------------------------------------------
1 | import Component from "@ember/component";
2 | import { get } from "@ember/object";
3 | import { inject as service } from "@ember/service";
4 | import { t } from "ember-intl";
5 | import layout from "./template.hbs";
6 |
7 |
8 | export default Component.extend({
9 | /** @type {IntlService} */
10 | intl: service(),
11 |
12 | layout,
13 |
14 | tagName: "div",
15 | classNames: [ "drop-down-selection-component" ],
16 | classNameBindings: [ "class" ],
17 |
18 | placeholder: t( "components.drop-down-selection.placeholder" ),
19 |
20 | click() {
21 | get( this, "action" )();
22 | return false;
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/src/app/ui/components/form/drop-down-selection/template.hbs:
--------------------------------------------------------------------------------
1 | {{#if hasBlock}}
2 | {{yield (hash
3 | hasSelection=(if selection true false)
4 | label=(get selection optionLabelPath)
5 | value=(get selection optionValuePath)
6 | placeholder=placeholder
7 | )}}
8 | {{else}}
9 | {{if selection
10 | (get selection optionLabelPath)
11 | placeholder
12 | }}
13 | {{/if}}
--------------------------------------------------------------------------------
/src/app/ui/components/form/file-select/template.hbs:
--------------------------------------------------------------------------------
1 | {{input type="text" value=value class=inputClass placeholder=placeholder disabled=disabled}}
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/app/ui/components/form/number-field/template.hbs:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/app/ui/components/form/radio-buttons-item/component.js:
--------------------------------------------------------------------------------
1 | import { get } from "@ember/object";
2 | import InputBtnComponent from "../-input-btn/component";
3 |
4 |
5 | export default InputBtnComponent.extend({
6 | classNames: [ "radio-buttons-item-component" ],
7 |
8 | click() {
9 | if ( get( this, "disabled" ) ) { return; }
10 | get( this, "action" )();
11 | }
12 | });
13 |
--------------------------------------------------------------------------------
/src/app/ui/components/form/radio-buttons/component.js:
--------------------------------------------------------------------------------
1 | import { set } from "@ember/object";
2 | import Selectable from "../-selectable/component";
3 | import layout from "./template.hbs";
4 | import "./styles.less";
5 |
6 |
7 | export default Selectable.extend({
8 | layout,
9 |
10 | tagName: "div",
11 | classNames: [ "radio-buttons-component" ],
12 |
13 | actions: {
14 | change( item ) {
15 | set( this, "selection", item );
16 | }
17 | }
18 | });
19 |
--------------------------------------------------------------------------------
/src/app/ui/components/form/radio-buttons/template.hbs:
--------------------------------------------------------------------------------
1 |
2 | {{#if hasBlock}}
3 | {{#each content as |item|}}
4 | {{yield (hash
5 | button=(component "radio-buttons-item"
6 | label=(get item optionLabelPath)
7 | checked=(is-equal item selection)
8 | disabled=item.disabled
9 | action=(action "change" item bubbles=false)
10 | )
11 | label=(get item optionLabelPath)
12 | value=(get item optionValuePath)
13 | checked=(is-equal item selection)
14 | disabled=item.disabled
15 | )}}
16 | {{/each}}
17 | {{else}}
18 | {{#each content as |item|}}
19 | {{#radio-buttons-item
20 | checked=(is-equal item selection)
21 | disabled=item.disabled
22 | action=(action "change" item bubbles=false)
23 | }}
24 | {{get item optionLabelPath}}
25 | {{/radio-buttons-item}}
26 | {{/each}}
27 | {{/if}}
--------------------------------------------------------------------------------
/src/app/ui/components/helper/-from-now.js:
--------------------------------------------------------------------------------
1 | import Helper from "@ember/component/helper";
2 | import { run } from "@ember/runloop";
3 |
4 |
5 | export const helper = Helper.extend({
6 | compute( params, hash ) {
7 | if ( hash.interval ) {
8 | this._interval = setTimeout( () => run( () => this.recompute() ), hash.interval );
9 | }
10 |
11 | return this._compute( ...arguments );
12 | },
13 |
14 | destroy() {
15 | if ( this._interval ) {
16 | clearTimeout( this._interval );
17 | this._interval = null;
18 | }
19 |
20 | this._super( ...arguments );
21 | }
22 | });
23 |
--------------------------------------------------------------------------------
/src/app/ui/components/helper/bool-and.js:
--------------------------------------------------------------------------------
1 | import { helper as h } from "@ember/component/helper";
2 |
3 |
4 | export const helper = h( params => params.every( value => value ) );
5 |
--------------------------------------------------------------------------------
/src/app/ui/components/helper/bool-not.js:
--------------------------------------------------------------------------------
1 | import { helper as h } from "@ember/component/helper";
2 |
3 |
4 | export const helper = h( params => params.every( value => !value ) );
5 |
--------------------------------------------------------------------------------
/src/app/ui/components/helper/bool-or.js:
--------------------------------------------------------------------------------
1 | import { helper as h } from "@ember/component/helper";
2 |
3 |
4 | export const helper = h( params => params.some( value => value ) );
5 |
--------------------------------------------------------------------------------
/src/app/ui/components/helper/find-by.js:
--------------------------------------------------------------------------------
1 | import { helper as h } from "@ember/component/helper";
2 |
3 |
4 | export const helper = h( ([ arr, key, value ]) => arr.findBy( key, value ) );
5 |
--------------------------------------------------------------------------------
/src/app/ui/components/helper/format-viewers.js:
--------------------------------------------------------------------------------
1 | import { helper as h } from "@ember/component/helper";
2 |
3 |
4 | export const helper = h( params => {
5 | const viewers = Number( params[ 0 ] );
6 |
7 | return isNaN( viewers )
8 | ? "0"
9 | : viewers >= 1000000
10 | ? `${( Math.floor( viewers / 10000 ) / 100 ).toFixed( 2 )}m`
11 | : viewers >= 100000
12 | ? `${( Math.floor( viewers / 1000 ) ).toFixed( 0 )}k`
13 | : viewers >= 10000
14 | ? `${( Math.floor( viewers / 100 ) / 10 ).toFixed( 1 )}k`
15 | : viewers.toFixed( 0 );
16 | });
17 |
--------------------------------------------------------------------------------
/src/app/ui/components/helper/get-index.js:
--------------------------------------------------------------------------------
1 | import { helper as h } from "@ember/component/helper";
2 |
3 |
4 | export const helper = h( ([ arr, key ]) => arr[ key ] );
5 |
--------------------------------------------------------------------------------
/src/app/ui/components/helper/get-param.js:
--------------------------------------------------------------------------------
1 | import { helper as h } from "@ember/component/helper";
2 |
3 |
4 | export const helper = h( ( params, hash ) => params[ hash.index ] );
5 |
--------------------------------------------------------------------------------
/src/app/ui/components/helper/is-equal.js:
--------------------------------------------------------------------------------
1 | import { helper as h } from "@ember/component/helper";
2 |
3 |
4 | export const helper = h( params =>
5 | params.every( ( currentValue, index, arr ) => currentValue === arr[ 0 ] )
6 | );
7 |
--------------------------------------------------------------------------------
/src/app/ui/components/helper/is-gt.js:
--------------------------------------------------------------------------------
1 | import { helper as h } from "@ember/component/helper";
2 |
3 |
4 | export const helper = h( params => params[0] > params[1] );
5 |
--------------------------------------------------------------------------------
/src/app/ui/components/helper/is-gte.js:
--------------------------------------------------------------------------------
1 | import { helper as h } from "@ember/component/helper";
2 |
3 |
4 | export const helper = h( params => params[0] >= params[1] );
5 |
--------------------------------------------------------------------------------
/src/app/ui/components/helper/is-null.js:
--------------------------------------------------------------------------------
1 | import { helper as h } from "@ember/component/helper";
2 |
3 |
4 | export const helper = h( params => params.every( value => value === null ) );
5 |
--------------------------------------------------------------------------------
/src/app/ui/components/helper/math-add.js:
--------------------------------------------------------------------------------
1 | import { helper as h } from "@ember/component/helper";
2 |
3 |
4 | export const helper = h( params => params.reduce( ( a, b ) => a + b ) );
5 |
--------------------------------------------------------------------------------
/src/app/ui/components/helper/math-div.js:
--------------------------------------------------------------------------------
1 | import { helper as h } from "@ember/component/helper";
2 |
3 |
4 | export const helper = h( params => params.reduce( ( a, b ) => a / b ) );
5 |
--------------------------------------------------------------------------------
/src/app/ui/components/helper/math-mul.js:
--------------------------------------------------------------------------------
1 | import { helper as h } from "@ember/component/helper";
2 |
3 |
4 | export const helper = h( params => params.reduce( ( a, b ) => a * b ) );
5 |
--------------------------------------------------------------------------------
/src/app/ui/components/helper/math-sub.js:
--------------------------------------------------------------------------------
1 | import { helper as h } from "@ember/component/helper";
2 |
3 |
4 | export const helper = h( params => params.reduce( ( a, b ) => a - b ) );
5 |
--------------------------------------------------------------------------------
/src/app/ui/components/helper/t.js:
--------------------------------------------------------------------------------
1 | export { default as helper } from "ember-intl/helpers/t";
2 |
--------------------------------------------------------------------------------
/src/app/ui/components/link/documentation-link/styles.less:
--------------------------------------------------------------------------------
1 |
2 | .documentation-link-component {
3 | font-style: italic;
4 |
5 | &.with-url {
6 | cursor: pointer;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/app/ui/components/link/documentation-link/template.hbs:
--------------------------------------------------------------------------------
1 | {{item}}
--------------------------------------------------------------------------------
/src/app/ui/components/link/embedded-links/component.js:
--------------------------------------------------------------------------------
1 | import Component from "@ember/component";
2 | import { get, computed } from "@ember/object";
3 | import { parseString } from "utils/linkparser";
4 | import layout from "./template.hbs";
5 |
6 |
7 | export default Component.extend({
8 | layout,
9 |
10 | classNames: [ "embedded-links-component" ],
11 |
12 | content: computed( "text", function() {
13 | const text = get( this, "text" );
14 | const parsed = parseString( text );
15 | const links = parsed.links;
16 |
17 | // merge texts and links
18 | return parsed.texts.reduce(function( output, textItem, index ) {
19 | if ( textItem.length ) {
20 | output.push({ text: textItem });
21 | }
22 | if ( links[ index ] ) {
23 | output.push( links[ index ] );
24 | }
25 | return output;
26 | }, [] );
27 | })
28 | });
29 |
--------------------------------------------------------------------------------
/src/app/ui/components/link/embedded-links/template.hbs:
--------------------------------------------------------------------------------
1 | {{~#each content as |item|~}}
2 | {{~#if item.url~}}
3 | {{~#external-link url=item.url~}}
4 | {{~item.text~}}
5 | {{~/external-link~}}
6 | {{~else~}}
7 | {{~item.text~}}
8 | {{~/if~}}
9 | {{~/each~}}
--------------------------------------------------------------------------------
/src/app/ui/components/list/-list-item/component.js:
--------------------------------------------------------------------------------
1 | import Component from "@ember/component";
2 | import { inject as service } from "@ember/service";
3 |
4 |
5 | export default Component.extend({
6 | settings: service(),
7 |
8 | tagName: "li",
9 | classNameBindings: [
10 | "isNewItem:newItem",
11 | "isDuplicateItem:duplicateItem"
12 | ],
13 |
14 | isNewItem: false,
15 | isDuplicateItem: false
16 | });
17 |
--------------------------------------------------------------------------------
/src/app/ui/components/list/channel-item/component.js:
--------------------------------------------------------------------------------
1 | import ListItemComponent from "../-list-item/component";
2 | import { alias } from "@ember/object/computed";
3 | import layout from "./template.hbs";
4 | import "./styles.less";
5 |
6 |
7 | export default ListItemComponent.extend({
8 | layout,
9 |
10 | classNames: [ "channel-item-component" ],
11 |
12 | user: alias( "content.user" ),
13 | followed_at: alias( "content.followed_at" )
14 | });
15 |
--------------------------------------------------------------------------------
/src/app/ui/components/list/content-list/template.hbs:
--------------------------------------------------------------------------------
1 | {{#if content.length}}
2 |
3 | {{#each content as |item index|}}
4 | {{yield
5 | item
6 | (is-gte index lengthInitial)
7 | (get-index duplicates index)
8 | }}
9 | {{/each}}
10 |
11 | {{#if infiniteScroll}}
12 | {{infinite-scroll
13 | content=content
14 | isFetching=isFetching
15 | hasFetchedAll=hasFetchedAll
16 | fetchError=fetchError
17 | action=(action "willFetchContent")
18 | }}
19 | {{/if}}
20 | {{else if (hasBlock "inverse")}}
21 |
22 | {{yield to="inverse"}}
23 |
24 | {{/if}}
--------------------------------------------------------------------------------
/src/app/ui/components/list/game-item/component.js:
--------------------------------------------------------------------------------
1 | import { inject as service } from "@ember/service";
2 | import ListItemComponent from "../-list-item/component";
3 | import layout from "./template.hbs";
4 | import "./styles.less";
5 |
6 |
7 | export default ListItemComponent.extend({
8 | /** @type {RouterService} */
9 | router: service(),
10 |
11 | layout,
12 |
13 | classNames: [ "game-item-component" ],
14 |
15 | click() {
16 | this.router.transitionTo( "games.game", this.content.id );
17 | }
18 | });
19 |
--------------------------------------------------------------------------------
/src/app/ui/components/list/game-item/template.hbs:
--------------------------------------------------------------------------------
1 | {{preview-image src=content.box_art_url.latest}}
2 |
--------------------------------------------------------------------------------
/src/app/ui/components/list/headline-totals/component.js:
--------------------------------------------------------------------------------
1 | import Component from "@ember/component";
2 | import { gte } from "@ember/object/computed";
3 | import layout from "./template.hbs";
4 |
5 |
6 | export default Component.extend({
7 | layout,
8 |
9 | tagName: "div",
10 | classNames: [ "total" ],
11 |
12 | total: null,
13 |
14 | isVisible: gte( "total", 0 )
15 |
16 | }).reopenClass({
17 | positionalParams: [ "total" ]
18 | });
19 |
--------------------------------------------------------------------------------
/src/app/ui/components/list/headline-totals/template.hbs:
--------------------------------------------------------------------------------
1 | ({{total}})
--------------------------------------------------------------------------------
/src/app/ui/components/list/infinite-scroll/styles.less:
--------------------------------------------------------------------------------
1 |
2 | .form-button-component.infinite-scroll-component {
3 | display: block;
4 | min-width: 8em;
5 | height: auto;
6 | margin: 2em auto;
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/ui/components/list/infinite-scroll/template.hbs:
--------------------------------------------------------------------------------
1 | {{#if fetchError}}
2 | {{t "components.infinite-scroll.error"}}
3 | {{else if isLocked}}
4 | {{loading-spinner}}{{t "components.infinite-scroll.loading"}}
5 | {{else}}
6 | {{t "components.infinite-scroll.fetch"}}
7 | {{/if}}
--------------------------------------------------------------------------------
/src/app/ui/components/list/settings-channel-item/component.js:
--------------------------------------------------------------------------------
1 | import { get, set } from "@ember/object";
2 | import ListItemComponent from "../-list-item/component";
3 | import layout from "./template.hbs";
4 | import "./styles.less";
5 |
6 |
7 | export default ListItemComponent.extend({
8 | layout,
9 |
10 | classNames: [ "settings-channel-item-component" ],
11 |
12 | dialog: false,
13 |
14 | actions: {
15 | eraseDialog() {
16 | set( this, "dialog", true );
17 | },
18 |
19 | confirm() {
20 | get( this, "erase" )();
21 | },
22 |
23 | decline() {
24 | set( this, "dialog", false );
25 | }
26 | }
27 | });
28 |
--------------------------------------------------------------------------------
/src/app/ui/components/list/team-item/component.js:
--------------------------------------------------------------------------------
1 | import ListItemComponent from "../-list-item/component";
2 | import layout from "./template.hbs";
3 | import "./styles.less";
4 |
5 |
6 | export default ListItemComponent.extend({
7 | layout,
8 |
9 | classNames: [ "team-item-component" ]
10 | });
11 |
--------------------------------------------------------------------------------
/src/app/ui/components/list/team-item/styles.less:
--------------------------------------------------------------------------------
1 | @import (reference) "ui/styles/config";
2 | @import (reference) "ui/styles/mixins";
3 |
4 |
5 | .team-item-component {
6 | @logo: 6.5rem;
7 |
8 | min-height: 91px;
9 | margin-bottom: 1.5rem;
10 | .flexbox();
11 | .dynamic-elems-per-row( 2, @content-width, 4%, @additional-width );
12 |
13 | > .logo {
14 | display: block;
15 | position: relative;
16 | width: @logo;
17 | height: @logo;
18 | cursor: pointer;
19 | }
20 |
21 | > .info {
22 | flex-grow: 1;
23 | width: 0;
24 | margin: 0 .5rem;
25 |
26 | > header {
27 | margin: 0 0 .5rem;
28 | .text-overflow();
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/app/ui/components/list/team-item/template.hbs:
--------------------------------------------------------------------------------
1 | {{#link-to "team" content.id tagName="div" class="logo"}}
2 | {{preview-image src=content.thumbnail_url title=content.title}}
3 | {{/link-to}}
4 |
5 |
{{#link-to "team" content.id}}{{content.title}}{{/link-to}}
6 |
{{t "components.team-item.created-at.format" created_at=content.created_at}}
7 |
{{t "components.team-item.updated-at.format" updated_at=content.updated_at}}
8 |
--------------------------------------------------------------------------------
/src/app/ui/components/loading-spinner/component.js:
--------------------------------------------------------------------------------
1 | import Component from "@ember/component";
2 | import { on } from "@ember/object/evented";
3 | import layout from "./template.hbs";
4 | import "./styles.less";
5 |
6 |
7 | export default Component.extend({
8 | layout,
9 |
10 | tagName: "svg",
11 | attributeBindings: [ "viewBox" ],
12 | classNames: [ "loading-spinner-component" ],
13 |
14 | viewBox: "0 0 1 1",
15 |
16 | _setRadiusAttribute: on( "didInsertElement", function() {
17 | let circle = this.element.querySelector( "circle" );
18 | let strokeWidth = window.getComputedStyle( circle ).strokeWidth;
19 | let radius = 50 - parseFloat( strokeWidth );
20 | circle.setAttribute( "r", `${radius}%` );
21 | })
22 | });
23 |
--------------------------------------------------------------------------------
/src/app/ui/components/loading-spinner/template.hbs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/ui/components/modal/modal-body/component.js:
--------------------------------------------------------------------------------
1 | import Component from "@ember/component";
2 |
3 |
4 | export default Component.extend({
5 | tagName: "section",
6 | classNames: [ "modal-body-component" ]
7 | });
8 |
--------------------------------------------------------------------------------
/src/app/ui/components/modal/modal-changelog/template.hbs:
--------------------------------------------------------------------------------
1 |
2 | {{#modal-header}}
3 | {{t "modal.changelog.header" version=modalContext.version}}
4 | {{/modal-header}}
5 | {{#modal-body}}
6 | {{t "modal.changelog.body"}}
7 | {{/modal-body}}
8 | {{#modal-footer classNames="button-list-horizontal"}}
9 | {{#form-button
10 | action=(action "close")
11 | classNames="btn-danger"
12 | icon="fa-times"
13 | }}
14 | {{t "modal.dialog.action.close"}}
15 | {{/form-button}}
16 | {{#form-button
17 | action=(action "showChangelog")
18 | classNames="btn-success"
19 | icon="fa-list-alt"
20 | iconanim=true
21 | }}
22 | {{t "modal.changelog.action.show"}}
23 | {{/form-button}}
24 | {{/modal-footer}}
25 |
--------------------------------------------------------------------------------
/src/app/ui/components/modal/modal-confirm/component.js:
--------------------------------------------------------------------------------
1 | import ModalDialogComponent from "../modal-dialog/component";
2 | import layout from "./template.hbs";
3 |
4 |
5 | function actionFactory( action ) {
6 | return function( success, failure ) {
7 | this.modalContext.send( action, success, failure );
8 | };
9 | }
10 |
11 |
12 | export default ModalDialogComponent.extend({
13 | layout,
14 |
15 | "class": "modal-confirm",
16 |
17 | hotkeysNamespace: "modalconfirm",
18 | hotkeys: {
19 | confirm: "apply"
20 | },
21 |
22 |
23 | actions: {
24 | "apply" : actionFactory( "apply" ),
25 | "discard": actionFactory( "discard" ),
26 | "cancel" : actionFactory( "cancel" )
27 | }
28 | });
29 |
--------------------------------------------------------------------------------
/src/app/ui/components/modal/modal-debug/component.js:
--------------------------------------------------------------------------------
1 | import ModalDialogComponent from "../modal-dialog/component";
2 | import layout from "./template.hbs";
3 |
4 |
5 | export default ModalDialogComponent.extend({
6 | layout,
7 | classNames: [ "modal-debug-component" ]
8 | });
9 |
--------------------------------------------------------------------------------
/src/app/ui/components/modal/modal-debug/template.hbs:
--------------------------------------------------------------------------------
1 |
2 | {{#modal-header}}
3 | {{t "modal.debug.header" version=modalContext.buildVersion}}
4 | {{/modal-header}}
5 | {{#modal-body}}
6 | {{t "modal.debug.body" name=modalContext.displayName htmlSafe=true}}
7 | {{/modal-body}}
8 | {{#modal-footer classNames="button-list-horizontal"}}
9 | {{#form-button
10 | action=(action "close")
11 | classNames="btn-primary"
12 | icon="fa-arrow-left"
13 | }}
14 | {{t "modal.debug.action.close"}}
15 | {{/form-button}}
16 | {{/modal-footer}}
17 |
--------------------------------------------------------------------------------
/src/app/ui/components/modal/modal-dialog/template.hbs:
--------------------------------------------------------------------------------
1 | {{yield}}
--------------------------------------------------------------------------------
/src/app/ui/components/modal/modal-firstrun/component.js:
--------------------------------------------------------------------------------
1 | import { inject as service } from "@ember/service";
2 | import { main as mainConfig } from "config";
3 | import ModalDialogComponent from "../modal-dialog/component";
4 | import layout from "./template.hbs";
5 |
6 |
7 | export default ModalDialogComponent.extend( /** @class ModalFirstrunComponent */ {
8 | /** @type {RouterService} */
9 | router: service(),
10 |
11 | layout,
12 | classNames: [ "modal-firstrun-component" ],
13 |
14 | name: mainConfig[ "display-name" ],
15 |
16 |
17 | actions: {
18 | /** @this {ModalFirstrunComponent} */
19 | settings() {
20 | this.router.transitionTo( "settings" );
21 | this.send( "close" );
22 | }
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/src/app/ui/components/modal/modal-firstrun/template.hbs:
--------------------------------------------------------------------------------
1 |
2 | {{#modal-header}}
3 | {{name}}
4 | {{/modal-header}}
5 | {{#modal-body}}
6 | {{t "modal.firstrun.body" name=name htmlSafe=true}}
7 | {{/modal-body}}
8 | {{#modal-footer classNames="button-list-horizontal"}}
9 | {{#form-button
10 | action=(action "close")
11 | classNames="btn-primary"
12 | icon="fa-arrow-left"
13 | }}
14 | {{t "modal.firstrun.action.start"}}
15 | {{/form-button}}
16 | {{#form-button
17 | action=(action "settings")
18 | classNames="btn-success"
19 | icon="fa-cog"
20 | }}
21 | {{t "modal.firstrun.action.settings"}}
22 | {{/form-button}}
23 | {{/modal-footer}}
24 |
--------------------------------------------------------------------------------
/src/app/ui/components/modal/modal-footer/component.js:
--------------------------------------------------------------------------------
1 | import Component from "@ember/component";
2 |
3 |
4 | export default Component.extend({
5 | tagName: "footer",
6 | classNames: [ "modal-footer-component" ]
7 | });
8 |
--------------------------------------------------------------------------------
/src/app/ui/components/modal/modal-header/component.js:
--------------------------------------------------------------------------------
1 | import Component from "@ember/component";
2 |
3 |
4 | export default Component.extend({
5 | tagName: "header",
6 | classNames: [ "modal-header-component" ]
7 | });
8 |
--------------------------------------------------------------------------------
/src/app/ui/components/modal/modal-log/component.js:
--------------------------------------------------------------------------------
1 | import Component from "@ember/component";
2 | import { computed, observer } from "@ember/object";
3 | import { on } from "@ember/object/evented";
4 | import { scheduleOnce } from "@ember/runloop";
5 | import layout from "./template.hbs";
6 |
7 |
8 | export default Component.extend({
9 | layout,
10 |
11 | tagName: "section",
12 | classNames: [ "modal-log-component" ],
13 |
14 | log: computed(function() {
15 | return [];
16 | }),
17 |
18 | _logObserver: observer( "log.[]", function() {
19 | scheduleOnce( "afterRender", () => this.scrollToBottom() );
20 | }),
21 |
22 | scrollToBottom: on( "didInsertElement", function() {
23 | const elem = this.element;
24 | if ( !elem ) { return; }
25 | elem.scrollTop = Math.max( 0, elem.scrollHeight - elem.clientHeight );
26 | })
27 | });
28 |
--------------------------------------------------------------------------------
/src/app/ui/components/modal/modal-log/template.hbs:
--------------------------------------------------------------------------------
1 | {{#selectable-text tagName="ul" class="list-unstyled"}}
2 | {{#each log as |line|}}
3 | {{line.line}}
4 | {{/each}}
5 | {{/selectable-text}}
--------------------------------------------------------------------------------
/src/app/ui/components/modal/modal-newrelease/template.hbs:
--------------------------------------------------------------------------------
1 |
2 | {{#modal-header}}
3 | {{t "modal.newrelease.header" version=modalContext.version}}
4 | {{/modal-header}}
5 | {{#modal-body}}
6 | {{t "modal.newrelease.body" version=modalContext.release.version}}
7 | {{/modal-body}}
8 | {{#modal-footer classNames="button-list-horizontal"}}
9 | {{#form-button
10 | action=(action "download")
11 | classNames="btn-success"
12 | icon="fa-download"
13 | iconanim=true
14 | }}
15 | {{t "modal.newrelease.action.download"}}
16 | {{/form-button}}
17 | {{#form-button
18 | action=(action "ignore")
19 | classNames="btn-danger"
20 | icon="fa-times"
21 | }}
22 | {{t "modal.newrelease.action.ignore"}}
23 | {{/form-button}}
24 | {{/modal-footer}}
25 |
--------------------------------------------------------------------------------
/src/app/ui/components/modal/modal-quit/component.js:
--------------------------------------------------------------------------------
1 | import { readOnly } from "@ember/object/computed";
2 | import { inject as service } from "@ember/service";
3 | import ModalDialogComponent from "../modal-dialog/component";
4 | import layout from "./template.hbs";
5 |
6 |
7 | export default ModalDialogComponent.extend({
8 | nwjs: service(),
9 | streaming: service(),
10 |
11 | layout,
12 |
13 | classNames: [ "modal-quit-component" ],
14 |
15 | hasStreams: readOnly( "streaming.hasStreams" ),
16 |
17 | hotkeysNamespace: "modalquit",
18 | hotkeys: {
19 | shutdown: "shutdown"
20 | },
21 |
22 | actions: {
23 | shutdown() {
24 | this.streaming.killAll();
25 | this.send( "quit" );
26 | },
27 |
28 | quit() {
29 | this.nwjs.quit();
30 | }
31 | }
32 | });
33 |
--------------------------------------------------------------------------------
/src/app/ui/components/modal/modal-service/component.js:
--------------------------------------------------------------------------------
1 | import Component from "@ember/component";
2 | import { inject as service } from "@ember/service";
3 | import layout from "./template.hbs";
4 | import "./styles.less";
5 |
6 |
7 | export default Component.extend({
8 | /** @type {ModalService} */
9 | modal: service(),
10 |
11 | layout,
12 | classNames: "modal-service-component",
13 | classNameBindings: [ "modal.isModalOpened:active" ]
14 | });
15 |
--------------------------------------------------------------------------------
/src/app/ui/components/modal/modal-service/styles.less:
--------------------------------------------------------------------------------
1 | .modal-service-component {
2 | @duration: .333s;
3 |
4 | display: flex;
5 | align-content: center;
6 | justify-content: center;
7 | align-items: center;
8 | position: fixed;
9 | left: 0;
10 | right: 0;
11 | top: 0;
12 | bottom: 0;
13 | z-index: 10000;
14 | background: fade( #000, 50% );
15 |
16 | opacity: 0;
17 | visibility: hidden;
18 | // active -> inactive: toggle visibility after the opacity transition has finished
19 | transition: opacity @duration ease-out 0s, visibility 0s linear @duration;
20 |
21 | &.active {
22 | opacity: 1;
23 | visibility: visible;
24 | // inactive -> active: toggle visibility immediately
25 | transition: opacity @duration ease-out 0s;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/ui/components/modal/modal-service/template.hbs:
--------------------------------------------------------------------------------
1 | {{#if modal.isModalOpened}}
2 | {{#with modal.modals.lastObject as |modal|}}
3 | {{component
4 | (concat "modal-" modal.name)
5 | modalName=modal.name
6 | modalContext=modal.context
7 | }}
8 | {{/with}}
9 | {{/if}}
--------------------------------------------------------------------------------
/src/app/ui/components/preview-image/styles.less:
--------------------------------------------------------------------------------
1 | @import (reference) "ui/styles/config";
2 | @import (reference) "font-awesome/less/variables.less";
3 |
4 |
5 | .previewImage,
6 | .previewError {
7 | display: block;
8 | position: absolute;
9 | top: 0;
10 | left: 0;
11 | width: 100%;
12 | height: 100%;
13 | margin: 0;
14 | }
15 |
16 | .previewError {
17 | background: @color-twitch-purple;
18 |
19 | &::before {
20 | content: @fa-var-twitch;
21 | display: block;
22 | position: absolute;
23 | top: 50%;
24 | left: 0;
25 | right: 0;
26 | font: 4.5em/.1 FontAwesome;
27 | color: #fff !important;
28 | text-align: center;
29 | opacity: 1;
30 | transition: opacity .333s ease-out;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/app/ui/components/preview-image/template.hbs:
--------------------------------------------------------------------------------
1 | {{#if error}}
2 |
3 | {{else}}
4 |
5 | {{/if}}
--------------------------------------------------------------------------------
/src/app/ui/components/quick/quick-bar/template.hbs:
--------------------------------------------------------------------------------
1 | {{form-button action=(action "menuClick") class="btn-open" icon=(if isLocked "fa-ellipsis-v" "fa-ellipsis-h")}}
2 |
--------------------------------------------------------------------------------
/src/app/ui/components/settings-hotkey/styles.less:
--------------------------------------------------------------------------------
1 | .settings-hotkey-component {
2 | display: flex;
3 |
4 | > * {
5 | @height: 2.25rem;
6 |
7 | flex: 0 0 @height;
8 | height: @height !important;
9 | }
10 | > input {
11 | flex-grow: 1;
12 |
13 | &.is-empty {
14 | font-style: italic;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/ui/components/settings-submit/styles.less:
--------------------------------------------------------------------------------
1 | @import (reference) "ui/styles/config";
2 |
3 |
4 | .settings-submit-component {
5 | display: flex;
6 | justify-content: center;
7 | padding-bottom: 2rem;
8 | overflow: hidden;
9 |
10 | > .form-button-component {
11 | margin: 0 .5rem;
12 | transition: opacity @anim-content-duration ease-out, transform @anim-content-duration ease-out;
13 | }
14 |
15 | &.faded > .form-button-component {
16 | opacity: 0;
17 | transform: translateY( 3rem );
18 | -webkit-animation: settings-submit-component-buttons @anim-content-duration 1 ease-out alternate both;
19 | }
20 | }
21 |
22 | @-webkit-keyframes settings-submit-component-buttons {
23 | 0%, 99.9999% {
24 | visibility: visible;
25 | }
26 | 100% {
27 | visibility: hidden;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/ui/components/settings-submit/template.hbs:
--------------------------------------------------------------------------------
1 | {{#form-button
2 | action=apply
3 | classNames="btn-success"
4 | icon="fa-check"
5 | iconanim=true
6 | }}
7 | {{t "components.settings-submit.apply"}}
8 | {{/form-button}}
9 | {{#form-button
10 | action=discard
11 | classNames="btn-danger"
12 | icon="fa-trash-o"
13 | iconanim=true
14 | }}
15 | {{t "components.settings-submit.discard"}}
16 | {{/form-button}}
--------------------------------------------------------------------------------
/src/app/ui/components/stream/stats-row/component.js:
--------------------------------------------------------------------------------
1 | import Component from "@ember/component";
2 | import layout from "./template.hbs";
3 | import "./styles.less";
4 |
5 |
6 | export default Component.extend({
7 | layout,
8 |
9 | tagName: "div",
10 | classNameBindings: [ ":stats-row-component", "class" ],
11 |
12 | /** @type {TwitchStream} */
13 | stream: null,
14 | /** @type {boolean} */
15 | withFlag: true
16 | });
17 |
--------------------------------------------------------------------------------
/src/app/ui/components/stream/stats-row/styles.less:
--------------------------------------------------------------------------------
1 |
2 | .stats-row-component {
3 | display: flex;
4 | justify-content: flex-start;
5 |
6 | span:not(.stats-row-item-flag) {
7 | width: 6rem;
8 | overflow: hidden;
9 | white-space: nowrap;
10 | }
11 |
12 | span.stats-row-item-flag {
13 | width: 2rem;
14 | }
15 |
16 | i {
17 | margin-right: .25em;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/app/ui/components/stream/stats-row/template.hbs:
--------------------------------------------------------------------------------
1 | {{#if withFlag}}
2 | {{#if stream.hasBroadcasterLanguage}}
3 | {{flag-icon type="broadcaster" lang=stream.channel.broadcaster_language}}
4 | {{/if}}
5 | {{/if}}
6 | {{hours-from-now stream.started_at interval=60000}}
7 | {{format-viewers stream.viewer_count}}
--------------------------------------------------------------------------------
/src/app/ui/components/stream/stream-presentation/component.js:
--------------------------------------------------------------------------------
1 | import Component from "@ember/component";
2 | import layout from "./template.hbs";
3 | import "./styles.less";
4 |
5 |
6 | export default Component.extend({
7 | layout,
8 |
9 | tagName: "section",
10 | classNameBindings: [ ":stream-presentation-component", "class" ],
11 | "class": "",
12 |
13 | clickablePreview: true
14 | });
15 |
--------------------------------------------------------------------------------
/src/app/ui/components/stream/stream-presentation/styles.less:
--------------------------------------------------------------------------------
1 |
2 | .stream-presentation-component {
3 | display: flex;
4 | flex-flow: row nowrap;
5 | justify-content: space-between;
6 |
7 | > .image {
8 | position: relative;
9 | width: 59%;
10 | }
11 |
12 | > .info {
13 | width: 38.5%;
14 |
15 | > .title {
16 | margin-top: 0;
17 | overflow: hidden;
18 | text-overflow: ellipsis;
19 | white-space: nowrap;
20 |
21 | > .flag-icon-component {
22 | margin-right: .25em;
23 | }
24 |
25 | > a {
26 | text-decoration: none;
27 | }
28 | }
29 |
30 | > .game {
31 | > i.fa-gamepad {
32 | margin-right: .333em;
33 | }
34 | }
35 |
36 | > .stats-row-component {
37 | max-width: 20em;
38 | margin-top: .5em;
39 | }
40 |
41 | > .status {
42 | margin: .5em 0;
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/app/ui/components/stream/stream-presentation/template.hbs:
--------------------------------------------------------------------------------
1 |
2 | {{stream-preview-image
3 | stream=stream
4 | user=stream.user
5 | clickable=clickablePreview
6 | src=stream.thumbnail_url.latest
7 | noContextmenu=(bool-not clickablePreview)
8 | }}
9 |
10 |
11 |
{{#if stream.hasLanguage}}{{flag-icon type="channel" lang=stream.language}}{{/if}}{{#link-to "channel" stream.id}}{{stream.user.detailedName}}{{/link-to}}
12 | {{#if stream.game_id}}
{{#link-to "games.game" stream.game_id}}{{stream.game_name}}{{/link-to}}
{{/if}}
13 | {{stats-row stream=stream}}
14 | {{embedded-links class="status" text=stream.title}}
15 | {{#if hasBlock}}
16 |
19 | {{/if}}
20 |
--------------------------------------------------------------------------------
/src/app/ui/components/stream/stream-preview-image/template.hbs:
--------------------------------------------------------------------------------
1 | {{preview-image src=src}}
2 | {{#if hasBlock}}{{yield}}{{/if}}
--------------------------------------------------------------------------------
/src/app/ui/components/sub-menu/component.js:
--------------------------------------------------------------------------------
1 | import Component from "@ember/component";
2 | import layout from "./template.hbs";
3 | import "./styles.less";
4 |
5 |
6 | export default Component.extend({
7 | layout,
8 |
9 | tagName: "ul",
10 | classNames: [
11 | "sub-menu-component"
12 | ],
13 |
14 | baseroute: null,
15 | menus: null
16 |
17 | }).reopenClass({
18 | positionalParams: [
19 | "baseroute",
20 | "menus"
21 | ]
22 | });
23 |
--------------------------------------------------------------------------------
/src/app/ui/components/sub-menu/template.hbs:
--------------------------------------------------------------------------------
1 | {{#if hasBlock}}
2 | {{#each-in menus as |subroute name|}}
3 | {{yield subroute name}}
4 | {{/each-in}}
5 | {{else}}
6 | {{#each-in menus as |subroute name|}}
7 | {{#link-to (concat baseroute "." subroute) tagName="li"}}{{name}}{{/link-to}}
8 | {{/each-in}}
9 | {{/if}}
--------------------------------------------------------------------------------
/src/app/ui/routes/-mixins/controllers/retry-transition.js:
--------------------------------------------------------------------------------
1 | import { get, set } from "@ember/object";
2 | import Mixin from "@ember/object/mixin";
3 |
4 |
5 | export default Mixin.create({
6 | /**
7 | * Retry a previously stored transition
8 | * @param {string?} route
9 | * @returns {Promise}
10 | */
11 | retryTransition( route ) {
12 | const transition = get( this, "previousTransition" );
13 |
14 | if ( !transition ) {
15 | return route
16 | ? this.transitionToRoute( route )
17 | : Promise.resolve();
18 | }
19 |
20 | set( this, "previousTransition", null );
21 | return transition.retry();
22 | }
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/ui/routes/-mixins/routes/infinite-scroll/record-array.js:
--------------------------------------------------------------------------------
1 | import { get, set } from "@ember/object";
2 |
3 |
4 | function factory( fn ) {
5 | /**
6 | * @param {(DS.RecordArray|Ember.MutableArray)} enumerable
7 | * @param {...*} args
8 | * @returns {Promise}
9 | */
10 | return async function( arr, ...args ) {
11 | const meta = get( arr, "meta" );
12 |
13 | arr = await Promise.all( arr[ fn ]( ...args ) );
14 | arr = arr.filter( Boolean );
15 |
16 | /* istanbul ignore else */
17 | if ( meta ) {
18 | set( arr, "meta", meta );
19 | }
20 |
21 | return arr;
22 | };
23 | }
24 |
25 |
26 | export const toArray = factory( "toArray" );
27 | export const map = factory( "map" );
28 |
--------------------------------------------------------------------------------
/src/app/ui/routes/about/controller.js:
--------------------------------------------------------------------------------
1 | import Controller from "@ember/controller";
2 | import { main as mainConfig, locales as localesConfig } from "config";
3 | import metadata from "metadata";
4 | import { manifest } from "nwjs/App";
5 | import { platform, release, arch } from "utils/node/platform";
6 | import "./styles.less";
7 | import process from "process";
8 |
9 |
10 | export default Controller.extend({
11 | mainConfig,
12 | localesConfig,
13 | metadata,
14 | nwjsVersion: process.versions[ "nw" ],
15 | platform,
16 | release,
17 | arch,
18 | releaseUrl: mainConfig.urls.release.replace( "{version}", manifest.version )
19 | });
20 |
--------------------------------------------------------------------------------
/src/app/ui/routes/about/styles.less:
--------------------------------------------------------------------------------
1 | @import (reference) "ui/styles/mixins";
2 |
3 |
4 | .content-about {
5 | @width: 15rem;
6 | @margin-data: ( @width + 1 );
7 |
8 | article {
9 | .clearfix();
10 | }
11 |
12 | h3 > i.fa {
13 | margin-right: .333em;
14 | }
15 |
16 | .dl-horizontal {
17 | > dt {
18 | width: @width;
19 | }
20 | > dd {
21 | margin-left: @margin-data;
22 | }
23 | }
24 |
25 | .text-donation {
26 | width: 24rem;
27 | margin-left: @margin-data;
28 | }
29 |
30 | .coinaddress {
31 | display: inline-block;
32 | margin-left: 1em;
33 | font-size: smaller;
34 | font-style: italic;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/app/ui/routes/application/controller.js:
--------------------------------------------------------------------------------
1 | import Controller from "@ember/controller";
2 | import "bootstrap/dist/css/bootstrap.css";
3 | import "./styles/index.less";
4 |
5 |
6 | export default Controller;
7 |
--------------------------------------------------------------------------------
/src/app/ui/routes/application/route.js:
--------------------------------------------------------------------------------
1 | import Route from "@ember/routing/route";
2 | import { inject as service } from "@ember/service";
3 |
4 |
5 | export default Route.extend({
6 | /** @type {AuthService} */
7 | auth: service(),
8 | /** @type {VersioncheckService} */
9 | versioncheck: service(),
10 |
11 | init() {
12 | this._super( ...arguments );
13 | this.auth.autoLogin();
14 | this.versioncheck.check();
15 | }
16 | });
17 |
--------------------------------------------------------------------------------
/src/app/ui/routes/application/styles/content.less:
--------------------------------------------------------------------------------
1 |
2 | main.content {
3 | flex: 1;
4 | padding-right: ( @app-padding - @scrollbar-size );
5 | overflow-y: scroll;
6 |
7 | > h2,
8 | > header > h2 {
9 | height: 1.5em;
10 | margin: 0;
11 | overflow: hidden;
12 | font-weight: lighter;
13 | white-space: nowrap;
14 | text-overflow: ellipsis;
15 | }
16 |
17 | > header {
18 | display: flex;
19 | justify-content: space-between;
20 |
21 | > div.total {
22 | flex-shrink: 0;
23 | flex-grow: 1;
24 | margin-left: .5em;
25 | line-height: 2.5;
26 |
27 | .theme({
28 | .theme-mix-color( @theme-headline-sub-color, @theme-background );
29 | });
30 | }
31 | }
32 |
33 | &:not( .content-loading ) {
34 | animation: animFadeInRight @anim-content-duration ease-out;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/app/ui/routes/application/styles/fixes.less:
--------------------------------------------------------------------------------
1 |
2 | button {
3 | border: 0;
4 | border-radius: 0;
5 | background: transparent;
6 | }
7 |
8 |
9 | .input-group .form-control:first-child {
10 | border-right-width: 0 !important;
11 | }
12 |
13 |
14 | .input-group-btn {
15 | font-size: inherit;
16 | }
17 |
18 |
19 | // fix the weird size of the gamepad icon
20 | i.fa.fa-gamepad {
21 | font-size: 1.2em;
22 | }
23 |
24 | // fix alignment of film icon
25 | i.fa.fa-film {
26 | position: relative;
27 | top: -.05em;
28 | }
29 |
30 | // fix size of credit-card icon in buttons
31 | .form-button-component.icon > i.fa.fa-credit-card {
32 | transform: scaleX(.9) translateX(-.025em);
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/ui/routes/application/styles/fontawesome.less:
--------------------------------------------------------------------------------
1 | @import "font-awesome/less/variables.less";
2 | @import "font-awesome/less/mixins.less";
3 | @import "font-awesome/less/core.less";
4 | @import "font-awesome/less/larger.less";
5 | @import "font-awesome/less/fixed-width.less";
6 | @import "font-awesome/less/list.less";
7 | //@import "font-awesome/less/bordered-pulled.less";
8 | //@import "font-awesome/less/animated.less";
9 | //@import "font-awesome/less/rotated-flipped.less";
10 | //@import "font-awesome/less/stacked.less";
11 | @import "font-awesome/less/icons.less";
12 |
13 |
14 | @font-face {
15 | font-family: "FontAwesome";
16 | src: url( "font-awesome/fonts/fontawesome-webfont.woff2" ) format( "woff2" );
17 | font-weight: normal;
18 | font-style: normal;
19 | }
20 |
--------------------------------------------------------------------------------
/src/app/ui/routes/application/styles/index.less:
--------------------------------------------------------------------------------
1 | @import "ui/styles/config";
2 | @import "ui/styles/mixins";
3 | @import "ui/styles/themes";
4 |
5 | @import "fonts";
6 | @import "fontawesome";
7 | @import "animations";
8 | @import "scrollbars";
9 | @import "main";
10 | @import "content";
11 | @import "form";
12 | @import "fixes";
13 |
--------------------------------------------------------------------------------
/src/app/ui/routes/application/styles/scrollbars.less:
--------------------------------------------------------------------------------
1 |
2 | ::-webkit-scrollbar {
3 | width: @scrollbar-size;
4 | height: @scrollbar-size;
5 | }
6 |
7 | ::-webkit-scrollbar-track {
8 | background: transparent;
9 | }
10 |
11 | ::-webkit-scrollbar-thumb {
12 | border: ( ( @scrollbar-size - @scrollbar-thumb-size ) / 2 ) solid transparent;
13 | background-clip: content-box !important;
14 |
15 | .theme({
16 | background: @theme-scrollbar-background;
17 |
18 | &:hover,
19 | &:active {
20 | background: @theme-scrollbar-background-hover;
21 | }
22 | });
23 | }
24 |
25 | main.content::-webkit-scrollbar-thumb:vertical {
26 | border-top-width: 0;
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/ui/routes/application/template.hbs:
--------------------------------------------------------------------------------
1 | {{title-bar}}
2 |
3 | {{main-menu}}
4 | {{outlet}}
5 |
6 | {{modal-service}}
--------------------------------------------------------------------------------
/src/app/ui/routes/channel/controller.js:
--------------------------------------------------------------------------------
1 | import Controller from "@ember/controller";
2 | import { alias } from "@ember/object/computed";
3 | import "./styles.less";
4 |
5 |
6 | export default Controller.extend({
7 | /** @type {TwitchStream} */
8 | stream : alias( "model.stream" ),
9 | /** @type {TwitchUser} */
10 | user : alias( "model.user" ),
11 | /** @type {TwitchChannel} */
12 | channel: alias( "model.channel" )
13 | });
14 |
--------------------------------------------------------------------------------
/src/app/ui/routes/channel/index/route.js:
--------------------------------------------------------------------------------
1 | import { getOwner } from "@ember/application";
2 | import UserIndexRoute from "ui/routes/user/index/route";
3 |
4 |
5 | export default UserIndexRoute.extend({
6 | async model() {
7 | return this.modelFor( "channel" );
8 | },
9 |
10 | refresh() {
11 | return getOwner( this ).lookup( "route:channel" ).refresh();
12 | }
13 | });
14 |
--------------------------------------------------------------------------------
/src/app/ui/routes/channel/loading/route.js:
--------------------------------------------------------------------------------
1 | export { default } from "ui/routes/loading/route";
2 |
--------------------------------------------------------------------------------
/src/app/ui/routes/channel/teams/route.js:
--------------------------------------------------------------------------------
1 | import UserIndexRoute from "ui/routes/user/index/route";
2 | import PaginationMixin from "ui/routes/-mixins/routes/infinite-scroll/pagination";
3 |
4 |
5 | export default UserIndexRoute.extend( PaginationMixin, {
6 | itemSelector: ".team-item-component",
7 | modelName: "twitch-team",
8 | modelPreload: "thumbnail_url",
9 |
10 | model() {
11 | const { user: { id: broadcaster_id } } = this.modelFor( "channel" );
12 |
13 | return this._super({ broadcaster_id });
14 | }
15 | });
16 |
--------------------------------------------------------------------------------
/src/app/ui/routes/channel/teams/template.hbs:
--------------------------------------------------------------------------------
1 |
2 | {{#content-list model as |item isNewItem isDuplicateItem|}}
3 | {{team-item
4 | content=item
5 | isNewItem=isNewItem
6 | isDuplicateItem=isDuplicateItem
7 | }}
8 | {{else}}
9 | {{t "routes.channel.teams.empty"}}
10 | {{/content-list}}
11 |
--------------------------------------------------------------------------------
/src/app/ui/routes/error/controller.js:
--------------------------------------------------------------------------------
1 | import Controller from "@ember/controller";
2 | import "./styles.less";
3 |
4 |
5 | export default Controller;
6 |
--------------------------------------------------------------------------------
/src/app/ui/routes/error/styles.less:
--------------------------------------------------------------------------------
1 |
2 | .content-error {
3 | @width: 8em;
4 | @margin: .25em;
5 |
6 | h4 {
7 | font-size: 1.2em;
8 | }
9 |
10 | dt {
11 | width: @width;
12 | margin-bottom: @margin;
13 | text-align: left;
14 | }
15 |
16 | dd {
17 | margin: 0 0 @margin @width;
18 | }
19 |
20 | .stack {
21 | font-family: monospace;
22 | white-space: pre-wrap;
23 |
24 | &::before,
25 | &::after {
26 | display: none;
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/ui/routes/error/template.hbs:
--------------------------------------------------------------------------------
1 | {{#selectable-text tagName="main" class="content content-error"}}
2 | {{t "routes.error.header"}}
3 | {{t "routes.error.text"}}
4 |
5 | {{#each model as |row|}}
6 | - {{row.key}}
7 | - {{row.value}}
8 | {{/each}}
9 |
10 | {{/selectable-text}}
--------------------------------------------------------------------------------
/src/app/ui/routes/games/game/template.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{game.name}}
4 | {{#quick-bar}}
5 | {{quick-bar-homepage}}
6 | {{/quick-bar}}
7 |
8 | {{#content-list model as |item isNewItem isDuplicateItem|}}
9 | {{stream-item
10 | content=item
11 | isNewItem=isNewItem
12 | isDuplicateItem=isDuplicateItem
13 | showGame=false
14 | }}
15 | {{else}}
16 | {{t "routes.game.empty"}}
17 | {{/content-list}}
18 |
--------------------------------------------------------------------------------
/src/app/ui/routes/games/index/route.js:
--------------------------------------------------------------------------------
1 | import UserIndexRoute from "ui/routes/user/index/route";
2 | import PaginationMixin from "ui/routes/-mixins/routes/infinite-scroll/pagination";
3 | import RefreshRouteMixin from "ui/routes/-mixins/routes/refresh";
4 |
5 |
6 | export default UserIndexRoute.extend( PaginationMixin, RefreshRouteMixin, {
7 | itemSelector: ".game-item-component",
8 | modelName: "twitch-game-top",
9 | modelMapBy: "game",
10 | modelPreload: "box_art_url.latest"
11 | });
12 |
--------------------------------------------------------------------------------
/src/app/ui/routes/games/index/template.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{t "routes.games.header"}}
4 | {{#quick-bar}}
5 | {{quick-bar-homepage}}
6 | {{/quick-bar}}
7 |
8 | {{#content-list model as |item isNewItem isDuplicateItem|}}
9 | {{game-item
10 | content=item
11 | isNewItem=isNewItem
12 | isDuplicateItem=isDuplicateItem
13 | }}
14 | {{else}}
15 | {{t "routes.games.empty"}}
16 | {{/content-list}}
17 |
--------------------------------------------------------------------------------
/src/app/ui/routes/games/loading/route.js:
--------------------------------------------------------------------------------
1 | export { default } from "ui/routes/loading/route";
2 |
--------------------------------------------------------------------------------
/src/app/ui/routes/index/route.js:
--------------------------------------------------------------------------------
1 | import Route from "@ember/routing/route";
2 |
3 |
4 | export default Route.extend({
5 | beforeModel( transition ) {
6 | // access to this route is restricted
7 | // but don't block the initial transition
8 | if ( transition.sequence > 0 ) {
9 | transition.abort();
10 | }
11 | }
12 | });
13 |
--------------------------------------------------------------------------------
/src/app/ui/routes/loading/controller.js:
--------------------------------------------------------------------------------
1 | import Controller from "@ember/controller";
2 | import "./styles.less";
3 |
4 |
5 | export default Controller;
6 |
--------------------------------------------------------------------------------
/src/app/ui/routes/loading/route.js:
--------------------------------------------------------------------------------
1 | import Route from "@ember/routing/route";
2 |
3 |
4 | export default Route.extend({
5 | // override automatically generated templateName for all routes which import LoadingRoute
6 | templateName: "loading"
7 | });
8 |
--------------------------------------------------------------------------------
/src/app/ui/routes/loading/styles.less:
--------------------------------------------------------------------------------
1 | @import (reference) "ui/styles/config";
2 |
3 |
4 | main.content.content-loading {
5 | @size: 6rem;
6 |
7 | display: flex;
8 | align-items: center;
9 | justify-content: center;
10 | padding: 0;
11 | overflow: hidden;
12 |
13 | > .loading-spinner-component {
14 | position: relative;
15 | width: @size;
16 | height: @size;
17 |
18 | > circle {
19 | stroke: @color-primary;
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/ui/routes/loading/template.hbs:
--------------------------------------------------------------------------------
1 |
2 | {{loading-spinner}}
3 |
--------------------------------------------------------------------------------
/src/app/ui/routes/search/controller.js:
--------------------------------------------------------------------------------
1 | import Controller from "@ember/controller";
2 | import { computed } from "@ember/object";
3 | import { alias, empty, equal } from "@ember/object/computed";
4 | import "./styles.less";
5 |
6 |
7 | export default Controller.extend({
8 | queryParams: [ "filter", "query" ],
9 |
10 | games : alias( "model.games" ),
11 | channels: alias( "model.channels" ),
12 |
13 | notFiltered: equal( "filter", "all" ),
14 |
15 | emptyGames : empty( "games" ),
16 | emptyChannels: empty( "channels" ),
17 |
18 | noResults: computed( "emptyGames", "emptyChannels", function() {
19 | return this.emptyGames && this.emptyChannels;
20 | })
21 | });
22 |
--------------------------------------------------------------------------------
/src/app/ui/routes/search/styles.less:
--------------------------------------------------------------------------------
1 |
2 | .content-search {
3 | h2 > span {
4 | padding-left: .25em;
5 | }
6 |
7 | h3 {
8 | margin-top: 0;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/ui/routes/settings/-submenu/route.js:
--------------------------------------------------------------------------------
1 | import { set } from "@ember/object";
2 | import Route from "@ember/routing/route";
3 |
4 |
5 | export default Route.extend({
6 | model() {
7 | return this.modelFor( "settings" );
8 | },
9 |
10 | activate() {
11 | const settingsController = this.controllerFor( "settings" );
12 | set( settingsController, "currentSubmenu", this.routeName );
13 | },
14 |
15 | deactivate() {
16 | const settingsController = this.controllerFor( "settings" );
17 | set( settingsController, "isAnimated", true );
18 | }
19 | });
20 |
--------------------------------------------------------------------------------
/src/app/ui/routes/settings/channels/template.hbs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/ui/routes/settings/chat/route.js:
--------------------------------------------------------------------------------
1 | export { default } from "../-submenu/route";
2 |
--------------------------------------------------------------------------------
/src/app/ui/routes/settings/gui/route.js:
--------------------------------------------------------------------------------
1 | export { default } from "../-submenu/route";
2 |
--------------------------------------------------------------------------------
/src/app/ui/routes/settings/hotkeys/route.js:
--------------------------------------------------------------------------------
1 | export { default } from "../-submenu/route";
2 |
--------------------------------------------------------------------------------
/src/app/ui/routes/settings/index/route.js:
--------------------------------------------------------------------------------
1 | import { get, set } from "@ember/object";
2 | import Route from "@ember/routing/route";
3 |
4 |
5 | export default Route.extend({
6 | actions: {
7 | didTransition() {
8 | const settingsController = this.controllerFor( "settings" );
9 | let goto = get( settingsController, "currentSubmenu" );
10 | if ( !goto ) {
11 | goto = "settings.main";
12 | }
13 |
14 | set( settingsController, "isAnimated", false );
15 |
16 | this.replaceWith( goto );
17 | },
18 |
19 | willTransition() {
20 | return false;
21 | }
22 | }
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/ui/routes/settings/main/route.js:
--------------------------------------------------------------------------------
1 | export { default } from "../-submenu/route";
2 |
--------------------------------------------------------------------------------
/src/app/ui/routes/settings/notifications/route.js:
--------------------------------------------------------------------------------
1 | export { default } from "../-submenu/route";
2 |
--------------------------------------------------------------------------------
/src/app/ui/routes/settings/player/route.js:
--------------------------------------------------------------------------------
1 | export { default } from "../-submenu/route";
2 |
--------------------------------------------------------------------------------
/src/app/ui/routes/settings/streaming/route.js:
--------------------------------------------------------------------------------
1 | export { default } from "../-submenu/route";
2 |
--------------------------------------------------------------------------------
/src/app/ui/routes/settings/streams/controller.js:
--------------------------------------------------------------------------------
1 | import Controller from "@ember/controller";
2 | import { qualities as contentStreamingQuality } from "data/models/stream/model";
3 | import {
4 | default as SettingsStreams,
5 | DEFAULT_VODCAST_REGEXP
6 | } from "data/models/settings/streams/fragment";
7 | import { isDarwin } from "utils/node/platform";
8 |
9 |
10 | const {
11 | contentName: contentStreamsName,
12 | info: contentStreamsInfo,
13 | click: contentStreamsClick
14 | } = SettingsStreams;
15 |
16 |
17 | export default Controller.extend({
18 | contentStreamingQuality,
19 | contentStreamsName,
20 | contentStreamsInfo,
21 | contentStreamsClick,
22 |
23 | DEFAULT_VODCAST_REGEXP,
24 |
25 | isDarwin
26 | });
27 |
--------------------------------------------------------------------------------
/src/app/ui/routes/settings/streams/route.js:
--------------------------------------------------------------------------------
1 | export { default } from "../-submenu/route";
2 |
--------------------------------------------------------------------------------
/src/app/ui/routes/settings/template.hbs:
--------------------------------------------------------------------------------
1 |
2 | {{t "settings.menu.header"}}
3 |
4 | {{sub-menu "settings" (hash
5 | main=(t "settings.menu.main")
6 | gui=(t "settings.menu.gui")
7 | streaming=(t "settings.menu.streaming")
8 | player=(t "settings.menu.player")
9 | streams=(t "settings.menu.streams")
10 | chat=(t "settings.menu.chat")
11 | languages=(t "settings.menu.languages")
12 | hotkeys=(t "settings.menu.hotkeys")
13 | notifications=(t "settings.menu.notifications")
14 | channels=(t "settings.menu.channels")
15 | )}}
16 |
17 | {{outlet}}
18 | {{settings-submit
19 | apply=(action "apply")
20 | discard=(action "discard")
21 | isDirty=model.isDirty
22 | disabled=(is-equal currentSubmenu "settings.channels")
23 | }}
24 |
--------------------------------------------------------------------------------
/src/app/ui/routes/streams/route.js:
--------------------------------------------------------------------------------
1 | import UserIndexRoute from "ui/routes/user/index/route";
2 | import PaginationMixin from "ui/routes/-mixins/routes/infinite-scroll/pagination";
3 | import FilterLanguagesMixin from "ui/routes/-mixins/routes/filter-languages";
4 | import RefreshRouteMixin from "ui/routes/-mixins/routes/refresh";
5 |
6 |
7 | export default UserIndexRoute.extend( PaginationMixin, FilterLanguagesMixin, RefreshRouteMixin, {
8 | itemSelector: ".stream-item-component",
9 | modelName: "twitch-stream",
10 | modelPreload: "thumbnail_url.latest",
11 |
12 | /**
13 | * @param {TwitchStream} twitchStream
14 | * @return {Promise}
15 | */
16 | async modelItemLoader( twitchStream ) {
17 | await Promise.all([
18 | twitchStream.user.promise,
19 | twitchStream.channel.promise
20 | ]);
21 | }
22 | });
23 |
--------------------------------------------------------------------------------
/src/app/ui/routes/streams/template.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{t "routes.streams.header"}}
4 | {{#quick-bar}}
5 | {{quick-bar-homepage}}
6 | {{/quick-bar}}
7 |
8 | {{#content-list model as |item isNewItem isDuplicateItem|}}
9 | {{stream-item
10 | content=item
11 | isNewItem=isNewItem
12 | isDuplicateItem=isDuplicateItem
13 | }}
14 | {{else}}
15 | {{t "routes.streams.empty"}}
16 | {{/content-list}}
17 |
--------------------------------------------------------------------------------
/src/app/ui/routes/team/controller.js:
--------------------------------------------------------------------------------
1 | import Controller from "@ember/controller";
2 | import "./styles.less";
3 |
4 |
5 | export default Controller;
6 |
--------------------------------------------------------------------------------
/src/app/ui/routes/team/index/template.hbs:
--------------------------------------------------------------------------------
1 | {{#content-list model as |item isNewItem|}}
2 | {{stream-item
3 | content=item
4 | isNewItem=isNewItem
5 | }}
6 | {{else}}
7 | {{t "routes.team.live.empty"}}
8 | {{/content-list}}
--------------------------------------------------------------------------------
/src/app/ui/routes/team/info/route.js:
--------------------------------------------------------------------------------
1 | import { getOwner } from "@ember/application";
2 | import UserIndexRoute from "ui/routes/user/index/route";
3 |
4 |
5 | export default UserIndexRoute.extend({
6 | model() {
7 | return this.modelFor( "team" );
8 | },
9 |
10 | refresh() {
11 | return getOwner( this ).lookup( "route:team" ).refresh();
12 | }
13 | });
14 |
--------------------------------------------------------------------------------
/src/app/ui/routes/team/info/template.hbs:
--------------------------------------------------------------------------------
1 | {{#selectable-text tagName="section" classNames="content-team-info"}}
2 | {{t "routes.team.info.header"}}
3 | {{#embedded-html-links classNames="user-content"}}{{{model.info}}}{{/embedded-html-links}}
4 | {{/selectable-text}}
--------------------------------------------------------------------------------
/src/app/ui/routes/team/loading/route.js:
--------------------------------------------------------------------------------
1 | export { default } from "ui/routes/loading/route";
2 |
--------------------------------------------------------------------------------
/src/app/ui/routes/team/members/template.hbs:
--------------------------------------------------------------------------------
1 | {{#content-list model as |item isNewItem|}}
2 | {{channel-item
3 | content=(hash
4 | user=item
5 | )
6 | isNewItem=isNewItem
7 | }}
8 | {{else}}
9 | {{t "routes.team.all.empty"}}
10 | {{/content-list}}
--------------------------------------------------------------------------------
/src/app/ui/routes/team/route.js:
--------------------------------------------------------------------------------
1 | import UserIndexRoute from "ui/routes/user/index/route";
2 | import RefreshRouteMixin from "ui/routes/-mixins/routes/refresh";
3 | import preload from "utils/preload";
4 |
5 |
6 | export default UserIndexRoute.extend( RefreshRouteMixin, {
7 | async model({ team_id }) {
8 | /** @type {TwitchTeam} */
9 | const record = await this.store.findRecord( "twitch-team", team_id, { reload: true } );
10 |
11 | return await preload( record, "thumbnail_url" );
12 | }
13 | });
14 |
--------------------------------------------------------------------------------
/src/app/ui/routes/user/auth/route.js:
--------------------------------------------------------------------------------
1 | import { get } from "@ember/object";
2 | import Route from "@ember/routing/route";
3 | import { inject as service } from "@ember/service";
4 |
5 |
6 | export default Route.extend({
7 | auth: service(),
8 |
9 | beforeModel( transition ) {
10 | // check if user is successfully logged in
11 | if ( get( this, "auth.session.isLoggedIn" ) ) {
12 | transition.abort();
13 | this.transitionTo( "user.index" );
14 | }
15 | },
16 |
17 | actions: {
18 | willTransition() {
19 | this.controller.send( "abort" );
20 | this.controller.resetProperties();
21 | }
22 | }
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/ui/routes/user/controller.js:
--------------------------------------------------------------------------------
1 | import Controller from "@ember/controller";
2 | import "./styles.less";
3 |
4 |
5 | export default Controller;
6 |
--------------------------------------------------------------------------------
/src/app/ui/routes/user/followed-channels/template.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{t "routes.user.followedChannels.header"}}
4 | {{headline-totals model.meta.total}}
5 | {{#quick-bar}}
6 | {{quick-bar-homepage}}
7 | {{/quick-bar}}
8 |
9 | {{#content-list model as |item isNewItem isDuplicateItem|}}
10 | {{channel-item
11 | content=(hash
12 | user=item.broadcaster_id
13 | followed_at=item.followed_at
14 | )
15 | isNewItem=isNewItem
16 | isDuplicateItem=isDuplicateItem
17 | }}
18 | {{else}}
19 | {{t "routes.user.followedChannels.empty"}}
20 | {{/content-list}}
21 |
--------------------------------------------------------------------------------
/src/app/ui/routes/user/followed-streams/template.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{t "routes.user.followedStreams.header"}}
4 | {{#quick-bar}}
5 | {{quick-bar-homepage}}
6 | {{/quick-bar}}
7 |
8 | {{#content-list model as |item isNewItem isDuplicateItem|}}
9 | {{stream-item
10 | content=item
11 | isNewItem=isNewItem
12 | isDuplicateItem=isDuplicateItem
13 | faded=false
14 | }}
15 | {{else}}
16 | {{t "routes.user.followedStreams.empty.text"}}
17 |
18 | - {{#link-to "streams"}}{{t "routes.user.followedStreams.empty.streams"}}{{/link-to}}
19 |
20 | {{/content-list}}
21 |
--------------------------------------------------------------------------------
/src/app/ui/routes/user/loading/route.js:
--------------------------------------------------------------------------------
1 | export { default } from "ui/routes/loading/route";
2 |
--------------------------------------------------------------------------------
/src/app/ui/routes/watching/controller.js:
--------------------------------------------------------------------------------
1 | import Controller from "@ember/controller";
2 | import { sort } from "@ember/object/computed";
3 | import { inject as service } from "@ember/service";
4 | import { qualities } from "data/models/stream/model";
5 | import "./styles.less";
6 |
7 |
8 | export default Controller.extend({
9 | /** @type {AuthService} */
10 | auth: service(),
11 | /** @type {StreamingService} */
12 | streaming: service(),
13 |
14 | sortedModel: sort( "model", "sortBy" ),
15 | sortBy: [ "started:desc" ],
16 |
17 | qualities,
18 |
19 | actions: {
20 | openDialog( stream ) {
21 | this.streaming.startStream( stream );
22 | },
23 |
24 | closeStream( stream ) {
25 | this.streaming.closeStream( stream );
26 | }
27 | }
28 | });
29 |
--------------------------------------------------------------------------------
/src/app/ui/routes/watching/route.js:
--------------------------------------------------------------------------------
1 | import Route from "@ember/routing/route";
2 | import { inject as service } from "@ember/service";
3 | import RefreshRouteMixin from "ui/routes/-mixins/routes/refresh";
4 | import preload from "utils/preload";
5 |
6 |
7 | export default Route.extend( RefreshRouteMixin, {
8 | /** @type {StreamingService} */
9 | streaming: service(),
10 |
11 | /**
12 | * @return {Promise>}
13 | */
14 | async model() {
15 | await preload(
16 | this.streaming.model.map( stream => stream.stream ),
17 | "thumbnail_url.latest"
18 | );
19 |
20 | return this.streaming.model;
21 | }
22 | });
23 |
--------------------------------------------------------------------------------
/src/app/utils/getStreamFromUrl.js:
--------------------------------------------------------------------------------
1 | const reUrl = /^(?:https?:\/\/)?(?:(?:www|secure|go)\.)?twitch\.tv\/(\w+)(?:\/profile)?$/;
2 | const blacklist = [ "directory", "login", "signup", "logout", "settings" ];
3 |
4 |
5 | /**
6 | * @param {String} url
7 | * @returns {(Boolean|String)}
8 | */
9 | function getStreamFromUrl( url ) {
10 | const match = reUrl.exec( String( url ) );
11 |
12 | if ( !match ) {
13 | return false;
14 | }
15 |
16 | return blacklist.indexOf( match[ 1 ] ) === -1
17 | ? match[ 1 ]
18 | : false;
19 | }
20 |
21 |
22 | export default getStreamFromUrl;
23 |
--------------------------------------------------------------------------------
/src/app/utils/is-focused.js:
--------------------------------------------------------------------------------
1 | export default function isFocused( element ) {
2 | return element.ownerDocument.activeElement === element;
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/utils/node/child_process/spawn.js:
--------------------------------------------------------------------------------
1 | import Process from "nwjs/process";
2 | import { spawn } from "child_process";
3 |
4 |
5 | const { assign } = Object;
6 |
7 |
8 | export default function( command, params, options = {} ) {
9 | const opt = assign( {}, options );
10 |
11 | if ( opt.env ) {
12 | opt.env = assign( {}, Process.env, opt.env );
13 | }
14 |
15 | return spawn( command, params, opt );
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/utils/node/env-path.js:
--------------------------------------------------------------------------------
1 | import { delimiter } from "path";
2 |
3 | export const paths = ( process.env.PATH || process.env.path || "." ).split( delimiter );
4 |
--------------------------------------------------------------------------------
/src/app/utils/node/fs/mkdirp.js:
--------------------------------------------------------------------------------
1 | import { promises as fsPromises } from "fs";
2 |
3 |
4 | const { mkdir } = fsPromises;
5 |
6 |
7 | /**
8 | * @param {string} path
9 | * @param {Object?} options
10 | * @param {(string|number)?} options.mode
11 | * @returns {Promise}
12 | */
13 | export default async function mkdirp( path, options = {} ) {
14 | return mkdir( path, Object.assign( {}, options, { recursive: true } ) );
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/utils/node/resolvePath.js:
--------------------------------------------------------------------------------
1 | import { isWin } from "utils/node/platform";
2 | import { resolve } from "path";
3 |
4 |
5 | const reVarWindows = /%([^%]+)%/g;
6 | const reVarUnix = /\$([A-Z_]+)/g;
7 |
8 |
9 | function fnVarReplace( _, v ) {
10 | return process.env[ v ] || "";
11 | }
12 |
13 | function resolvePathFactory( pattern ) {
14 | return function resolvePath( ...args ) {
15 | if ( !args.length ) {
16 | return "";
17 | }
18 |
19 | args[0] = String( args[0] || "" ).replace( pattern, fnVarReplace );
20 |
21 | return resolve( ...args );
22 | };
23 | }
24 |
25 |
26 | export default isWin
27 | ? resolvePathFactory( reVarWindows )
28 | : resolvePathFactory( reVarUnix );
29 |
--------------------------------------------------------------------------------
/src/app/utils/system-locale.js:
--------------------------------------------------------------------------------
1 | import { locales as localesConfig } from "config";
2 | import { window } from "nwjs/Window";
3 |
4 |
5 | const kLocales = Object.keys( localesConfig.locales );
6 | // test whether one of the system locales is supported
7 | const locale = window.navigator.languages
8 | .map( tag => tag.toLowerCase() )
9 | .find( tag => kLocales.includes( tag ) )
10 | // choose the first supported system locale, or use the defined default one instead
11 | || localesConfig.default;
12 |
13 |
14 | export default locale;
15 |
--------------------------------------------------------------------------------
/src/app/utils/wait.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {number} time
3 | * @param {boolean?} fail
4 | * @returns {Function}
5 | */
6 | function wait( time, fail ) {
7 | return function waitPromise( data ) {
8 | return new Promise(function( resolve, reject ) {
9 | setTimeout(function() {
10 | ( fail ? reject : resolve )( data );
11 | }, time );
12 | });
13 | };
14 | }
15 |
16 |
17 | export default wait;
18 |
--------------------------------------------------------------------------------
/src/config/files.json:
--------------------------------------------------------------------------------
1 | {
2 | "icons": {
3 | "big": "~assets/icons/icon-256.png",
4 | "tray": {
5 | "win32": {
6 | "@1x": "~assets/icons/icon-16.png",
7 | "@2x": "~assets/icons/icon-16@2x.png",
8 | "@3x": "~assets/icons/icon-16@3x.png"
9 | },
10 | "darwin": {
11 | "@1x": "~assets/icons/icon-osx-18.png",
12 | "@2x": "~assets/icons/icon-osx-18@2x.png",
13 | "@3x": "~assets/icons/icon-osx-18@3x.png"
14 | },
15 | "linux": {
16 | "@1x": "~assets/icons/icon-16@3x.png",
17 | "@2x": "~assets/icons/icon-16@3x.png",
18 | "@3x": "~assets/icons/icon-16@3x.png"
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/config/log.json:
--------------------------------------------------------------------------------
1 | {
2 | "maxAgeDays": 7
3 | }
4 |
--------------------------------------------------------------------------------
/src/config/notification.json:
--------------------------------------------------------------------------------
1 | {
2 | "cache": {
3 | "dir": "icons",
4 | "time": 604800000
5 | },
6 | "fails": {
7 | "requests": 3,
8 | "channels": 1
9 | },
10 | "interval": {
11 | "request": 60000,
12 | "retry": 1000,
13 | "error": 120000
14 | },
15 | "query": {
16 | "first": 100,
17 | "maxQueries": 5
18 | },
19 | "provider": {
20 | "snoretoast": {
21 | "timeoutSetup": 3000,
22 | "timeoutNotify": 60000
23 | },
24 | "freedesktop": {
25 | "expire": 43200
26 | },
27 | "growl": {
28 | "host": "localhost",
29 | "ports": [
30 | 23053,
31 | 23052
32 | ],
33 | "timeout": 100
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/config/themes.json:
--------------------------------------------------------------------------------
1 | {
2 | "prefix": "theme-",
3 | "system": {
4 | "no-preference": "light",
5 | "light": "light",
6 | "dark": "dark"
7 | },
8 | "themes": [
9 | "light",
10 | "dark"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/src/config/update.json:
--------------------------------------------------------------------------------
1 | {
2 | "check-again": 604800000,
3 | "show-debug-message": 86400000,
4 | "githubreleases": {
5 | "host": "https://api.github.com",
6 | "namespace": "repos/streamlink/streamlink-twitch-gui"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/config/vars.json:
--------------------------------------------------------------------------------
1 | {
2 | "time-window-event-debounce": 1000,
3 | "time-window-event-ignore": 2000,
4 |
5 | "search-history-size": 50,
6 |
7 | "stream-reload-interval": 60000,
8 | "image-expiration-time": 60000
9 | }
10 |
--------------------------------------------------------------------------------
/src/test/dev.css:
--------------------------------------------------------------------------------
1 | body {
2 | overflow-y: scroll;
3 | }
4 |
5 | #qunit {
6 | position: unset !important;
7 | }
8 |
9 | #qunit-tests {
10 | overflow: auto !important;
11 | }
12 |
13 | /* fix line height in the test module dropdown */
14 | #qunit-modulefilter-dropdown-list .clickable {
15 | display: inline-block;
16 | width: 100%;
17 | }
18 |
19 | /* use the same rules as #qunit-fixture */
20 | #ember-testing {
21 | position: absolute;
22 | top: -10000px;
23 | left: -10000px;
24 | width: 1000px;
25 | height: 1000px;
26 | }
27 |
28 | /* specific component fixes for testing */
29 | .modal-service-component {
30 | top: unset !important;
31 | right: unset !important;
32 | bottom: unset !important;
33 | left: unset !important;
34 | }
35 |
--------------------------------------------------------------------------------
/src/test/fixtures/data/models/github/releases.json:
--------------------------------------------------------------------------------
1 | {
2 | "latest-older": {
3 | "request": {
4 | "url": "https://api.github.com/repos/streamlink/streamlink-twitch-gui/releases/latest",
5 | "method": "GET",
6 | "query": {}
7 | },
8 | "response": {
9 | "id": "123456789",
10 | "tag_name": "v1.0.0",
11 | "html_url": "https://github.com/streamlink/streamlink-twitch-gui/releases/v1.0.0"
12 | }
13 | },
14 | "latest-newer": {
15 | "request": {
16 | "url": "https://api.github.com/repos/streamlink/streamlink-twitch-gui/releases/latest",
17 | "method": "GET",
18 | "query": {}
19 | },
20 | "response": {
21 | "id": "987654321",
22 | "tag_name": "v1337.0.0",
23 | "html_url": "https://github.com/streamlink/streamlink-twitch-gui/releases/v1337.0.0"
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/test/fixtures/data/models/twitch/adapter/coalesce-stream-single.yml:
--------------------------------------------------------------------------------
1 | request:
2 | method: "GET"
3 | url: "https://api.twitch.tv/helix/streams"
4 | query:
5 | - name: "user_id"
6 | value: "1"
7 | - name: "user_id"
8 | value: "2"
9 | response:
10 | data:
11 | - id: "1"
12 | user_id: "1"
13 | game_id: "1"
14 | - id: "2"
15 | user_id: "2"
16 | game_id: "2"
17 | pagination: {}
18 |
--------------------------------------------------------------------------------
/src/test/fixtures/data/models/twitch/channel.yml:
--------------------------------------------------------------------------------
1 | request:
2 | method: "GET"
3 | url: "https://api.twitch.tv/helix/channels"
4 | query:
5 | - name: "broadcaster_id"
6 | value: "11"
7 | - name: "broadcaster_id"
8 | value: "21"
9 | response:
10 | data:
11 | - broadcaster_id: "11"
12 | broadcaster_name: "foo"
13 | game_id: "12"
14 | game_name: "some game"
15 | broadcaster_language: "en"
16 | title: "some title"
17 | delay: 90
18 | - broadcaster_id: "21"
19 | broadcaster_name: "bar"
20 | game_id: "22"
21 | game_name: "another game"
22 | broadcaster_language: "id"
23 | title: "another title"
24 | delay: 0
25 |
--------------------------------------------------------------------------------
/src/test/fixtures/data/models/twitch/game-top.yml:
--------------------------------------------------------------------------------
1 | request:
2 | method: "GET"
3 | url: "https://api.twitch.tv/helix/games/top"
4 | query: {}
5 | response:
6 | data:
7 | - id: "1"
8 | - id: "2"
9 | pagination: {}
10 |
--------------------------------------------------------------------------------
/src/test/fixtures/data/models/twitch/game.yml:
--------------------------------------------------------------------------------
1 | request:
2 | method: "GET"
3 | url: "https://api.twitch.tv/helix/games"
4 | query:
5 | - name: "id"
6 | value: "1"
7 | - name: "id"
8 | value: "2"
9 | response:
10 | data:
11 | - id: "1"
12 | name: "some game"
13 | box_art_url: "https://mock/twitch-game/1/box_art-{width}x{height}.png"
14 | - id: "2"
15 | name: "another game"
16 | box_art_url: "https://mock/twitch-game/2/box_art-{width}x{height}.png"
17 |
--------------------------------------------------------------------------------
/src/test/fixtures/data/models/twitch/search-game.yml:
--------------------------------------------------------------------------------
1 | request:
2 | method: "GET"
3 | url: "https://api.twitch.tv/helix/search/categories"
4 | query:
5 | query: "foo"
6 | response:
7 | data:
8 | - id: "1"
9 | - id: "2"
10 |
--------------------------------------------------------------------------------
/src/test/fixtures/data/models/twitch/serializer/metadata.yml:
--------------------------------------------------------------------------------
1 | invalid:
2 | request:
3 | method: "GET"
4 | url: "https://api.twitch.tv/foo"
5 | query: {}
6 | response:
7 | data:
8 | - id: "1"
9 | pagination: {}
10 | nul: null
11 | obj:
12 | key: "val"
13 | arr:
14 | - "item1"
15 | - "item2"
16 | valid:
17 | request:
18 | method: "GET"
19 | url: "https://api.twitch.tv/foo"
20 | query: {}
21 | response:
22 | data:
23 | - id: "1"
24 | pagination:
25 | cursor: "cursor-value"
26 | str: "foo"
27 | num: 123
28 |
--------------------------------------------------------------------------------
/src/test/fixtures/data/models/twitch/serializer/response.yml:
--------------------------------------------------------------------------------
1 | invalid:
2 | request:
3 | method: "GET"
4 | url: "https://api.twitch.tv/foo"
5 | query: {}
6 | response: {}
7 | valid:
8 | request:
9 | method: "GET"
10 | url: "https://api.twitch.tv/foo"
11 | query: {}
12 | response:
13 | data:
14 | - id: "1"
15 | foo: "bar"
16 | - id: "2"
17 | foo: "baz"
18 |
--------------------------------------------------------------------------------
/src/test/fixtures/data/models/twitch/serializer/single-response.yml:
--------------------------------------------------------------------------------
1 | valid:
2 | request:
3 | method: "GET"
4 | url: "https://api.twitch.tv/foo"
5 | query: {}
6 | response:
7 | data:
8 | - id: "1"
9 | foo: "bar"
10 | invalid:
11 | request:
12 | method: "GET"
13 | url: "https://api.twitch.tv/foo"
14 | query: {}
15 | response:
16 | data:
17 |
--------------------------------------------------------------------------------
/src/test/fixtures/data/models/twitch/stream-followed.yml:
--------------------------------------------------------------------------------
1 | request:
2 | method: "GET"
3 | url: "https://api.twitch.tv/helix/streams/followed"
4 | query:
5 | user_id: "1"
6 | response:
7 | data:
8 | - user_id: "1"
9 | - user_id: "2"
10 |
--------------------------------------------------------------------------------
/src/test/fixtures/services/auth/validate-session.yml:
--------------------------------------------------------------------------------
1 | request:
2 | method: "GET"
3 | url: "https://api.twitch.tv/helix/users"
4 | query: {}
5 | response:
6 | data:
7 | - id: "1337"
8 | login: "user"
9 | display_name: "User"
10 |
--------------------------------------------------------------------------------
/src/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | QUnit
6 |
7 |
8 |
9 |
10 |
13 |
14 |
--------------------------------------------------------------------------------
/src/test/main-coverage.js:
--------------------------------------------------------------------------------
1 | import "./main";
2 |
3 |
4 | function importAll( r ) {
5 | r.keys().forEach( r );
6 | }
7 |
8 | importAll( require.context( "root/app/", true, /^\.\/(?!(main|app|logger)\.js$).+\.js$/ ) );
9 |
--------------------------------------------------------------------------------
/src/test/main-dev.js:
--------------------------------------------------------------------------------
1 | global._noQUnitBridge = true;
2 |
3 | import "./dev.css";
4 | import "./main";
5 | import nwGui from "nw.gui";
6 |
7 |
8 | const nwWindow = nwGui.Window.get();
9 |
10 |
11 | nwWindow.show();
12 | nwWindow.showDevTools();
13 |
--------------------------------------------------------------------------------
/src/test/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "<%= package.name %>-test",
3 | "version": "<%= package.version %>",
4 |
5 | "main": "index.html",
6 |
7 | "window": {
8 | "title": "<%= main['display-name'] %> Test",
9 | "frame": true,
10 | "resizable": true,
11 | "show": false,
12 | "position": "center",
13 | "width": 960,
14 | "height": 540,
15 | "min_width": 960,
16 | "min_height": 540
17 | },
18 |
19 | "chromium-args": "--enable-features=NativeNotifications"
20 | }
21 |
--------------------------------------------------------------------------------
/src/test/tests/index.js:
--------------------------------------------------------------------------------
1 | function importAll( r ) {
2 | r.keys().forEach( r );
3 | }
4 |
5 | // import tests in a certain order instead of importing them alphabetically
6 | importAll( require.context( "../web_modules/", true, /test\.js$|test\/[^\/]+\.js$/ ) );
7 | importAll( require.context( "./loaders/", true, /\.js$/ ) );
8 | importAll( require.context( "./nwjs/", true, /\.js$/ ) );
9 | importAll( require.context( "./utils/", true, /\.js$/ ) );
10 | importAll( require.context( "./data/", true, /\.js$/ ) );
11 | importAll( require.context( "./init/", true, /\.js$/ ) );
12 | importAll( require.context( "./ui/", true, /\.js$/ ) );
13 | importAll( require.context( "./services/", true, /\.js$/ ) );
14 |
--------------------------------------------------------------------------------
/src/test/tests/init/initializers/localstorage/search.js:
--------------------------------------------------------------------------------
1 | import { module, test } from "qunit";
2 |
3 | import updateSearch from "init/initializers/localstorage/search";
4 |
5 |
6 | module( "init/initializers/localstorage/search", function() {
7 | test( "Removes 'streams' filter", function( assert ) {
8 | const data = {
9 | "1": { id: "1", filter: "channels", query: "foo" },
10 | "2": { id: "2", filter: "streams", query: "foo" },
11 | "3": { id: "3", filter: "streams", query: "bar" },
12 | "4": { id: "4", filter: "all", query: "foo" }
13 | };
14 |
15 | updateSearch( data );
16 |
17 | assert.propEqual( data, {
18 | "1": { id: "1", filter: "channels", query: "foo" },
19 | "3": { id: "3", filter: "channels", query: "bar" },
20 | "4": { id: "4", filter: "all", query: "foo" }
21 | });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/test/tests/services/chat/providers/chrome.js:
--------------------------------------------------------------------------------
1 | import { module, test } from "qunit";
2 |
3 | import chatProviderChromeInjector from "inject-loader!services/chat/providers/chrome";
4 |
5 |
6 | module( "services/chat/providers/chrome" );
7 |
8 |
9 | test( "Exports ChatProviderChromium", assert => {
10 |
11 | const ChatProviderChromium = class {};
12 |
13 | const { default: ChatProviderChrome } = chatProviderChromeInjector({
14 | "./chromium": ChatProviderChromium
15 | });
16 |
17 | assert.strictEqual(
18 | ChatProviderChrome,
19 | ChatProviderChromium,
20 | "Exports the Chromium chat provider"
21 | );
22 |
23 | });
24 |
--------------------------------------------------------------------------------
/src/test/tests/services/chat/providers/custom.js:
--------------------------------------------------------------------------------
1 | import { module, test } from "qunit";
2 |
3 | import chatProviderCustomInjector from "inject-loader!services/chat/providers/custom";
4 |
5 |
6 | module( "services/chat/providers/custom" );
7 |
8 |
9 | test( "Exports ChatProviderBasic", assert => {
10 |
11 | const ChatProviderBasic = class {};
12 |
13 | const { default: ChatProviderCustom } = chatProviderCustomInjector({
14 | "./-basic": ChatProviderBasic
15 | });
16 |
17 | assert.strictEqual(
18 | ChatProviderCustom,
19 | ChatProviderBasic,
20 | "Exports the basic chat provider"
21 | );
22 |
23 | });
24 |
--------------------------------------------------------------------------------
/src/test/tests/services/notification/providers/auto.js:
--------------------------------------------------------------------------------
1 | import { module, test } from "qunit";
2 |
3 | import NotificationProviderAuto from "services/notification/providers/auto";
4 |
5 |
6 | module( "services/notification/providers/auto" );
7 |
8 |
9 | test( "isSupported", assert => {
10 |
11 | assert.notOk( NotificationProviderAuto.isSupported(), "Is not supported" );
12 |
13 | });
14 |
--------------------------------------------------------------------------------
/src/test/web_modules/cartesian-product/index.js:
--------------------------------------------------------------------------------
1 | const fill = ( A, B ) => [].concat( ...A.map( a => B.map( b => [].concat( a, b ) ) ) );
2 | const cartesian = ( a, b, ...c ) => b ? cartesian( fill( a, b ), ...c ) : a;
3 |
4 |
5 | export default cartesian;
6 |
--------------------------------------------------------------------------------
/src/test/web_modules/cartesian-product/test.js:
--------------------------------------------------------------------------------
1 | import { module, test } from "qunit";
2 |
3 | import cartesian from "./index";
4 |
5 |
6 | module( "cartesian-product", function() {
7 | test( "Cartesian product", function( assert ) {
8 | const prod = cartesian( [ 1, 2, 3 ], [ 4, 5 ], [ 6, 7 ], [ 8 ] );
9 | assert.propEqual( prod, [
10 | [ 1, 4, 6, 8 ],
11 | [ 1, 4, 7, 8 ],
12 | [ 1, 5, 6, 8 ],
13 | [ 1, 5, 7, 8 ],
14 | [ 2, 4, 6, 8 ],
15 | [ 2, 4, 7, 8 ],
16 | [ 2, 5, 6, 8 ],
17 | [ 2, 5, 7, 8 ],
18 | [ 3, 4, 6, 8 ],
19 | [ 3, 4, 7, 8 ],
20 | [ 3, 5, 6, 8 ],
21 | [ 3, 5, 7, 8 ]
22 | ], "Correctly builds the cartesian product of multiple input arrays of various lengths" );
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/test/web_modules/ember-qunit/test-loader.js:
--------------------------------------------------------------------------------
1 | export class TestLoader {}
2 |
3 | export function loadTests() {}
4 |
--------------------------------------------------------------------------------
/src/test/web_modules/ember-test.js:
--------------------------------------------------------------------------------
1 | import "root/web_modules/ember";
2 | import "ember-source/dist/ember-template-compiler";
3 | import "ember-data";
4 | import "ember-qunit";
5 |
--------------------------------------------------------------------------------
/src/test/web_modules/event-utils/index.js:
--------------------------------------------------------------------------------
1 | export * from "./events";
2 | export { blur } from "./blur";
3 | export { triggerEvent } from "./trigger-event";
4 | export { triggerKeyEvent, triggerKeyDownEvent } from "./trigger-key-event";
5 |
--------------------------------------------------------------------------------
/src/test/web_modules/htmlbars-inline-precompile.js:
--------------------------------------------------------------------------------
1 | export { hbs as default } from "./test-utils";
2 |
--------------------------------------------------------------------------------
/src/test/web_modules/qunit/index.js:
--------------------------------------------------------------------------------
1 | import QUnit from "qunit/qunit/qunit";
2 | import "qunit/qunit/qunit.css";
3 |
4 |
5 | export default QUnit;
6 |
7 | export const config = QUnit.config;
8 |
9 | export const module = QUnit.module;
10 | export const only = QUnit.only;
11 | export const test = QUnit.test;
12 | export const todo = QUnit.todo;
13 | export const skip = QUnit.skip;
14 | export const start = QUnit.start;
15 |
--------------------------------------------------------------------------------
/src/test/web_modules/require.js:
--------------------------------------------------------------------------------
1 | import Ember from "ember";
2 |
3 |
4 | export default Ember.__loader.require;
5 |
--------------------------------------------------------------------------------
/src/test/web_modules/translation-string/test.js:
--------------------------------------------------------------------------------
1 | import { module, test } from "qunit";
2 |
3 | import t from "translation-key";
4 |
5 |
6 | module( "translation-key", function() {
7 | test( "translation-key", function( assert ) {
8 | const bar = "bar";
9 | const qux = "qux";
10 |
11 | assert.strictEqual(
12 | t`foo`,
13 | "foo",
14 | "Returns a simple string"
15 | );
16 | assert.strictEqual(
17 | t`foo ${bar} baz ${qux} quux`,
18 | "foo bar baz qux quux",
19 | "Properly concats the template string with its expressions"
20 | );
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/web_modules/ember-app.js:
--------------------------------------------------------------------------------
1 | // empty module: see the ember-app-loader
2 |
--------------------------------------------------------------------------------
/src/web_modules/ember-data/version.js:
--------------------------------------------------------------------------------
1 | import metadata from "metadata";
2 |
3 |
4 | export default metadata.dependencies[ "ember-data" ];
5 |
--------------------------------------------------------------------------------
/src/web_modules/ember-get-config.js:
--------------------------------------------------------------------------------
1 | export default {};
2 |
--------------------------------------------------------------------------------
/src/web_modules/fetch.js:
--------------------------------------------------------------------------------
1 | export default window.fetch;
2 |
--------------------------------------------------------------------------------
/src/web_modules/metadata.js:
--------------------------------------------------------------------------------
1 | // empty module: see the metadata-loader
2 |
--------------------------------------------------------------------------------
/src/web_modules/snoretoast-binaries.js:
--------------------------------------------------------------------------------
1 | // snoretoast binary dependencies
2 | // see build/tasks/webpack/configurators/stylesheets-and-assets.js
3 | // see services/NotificationService/provider/snoretoast
4 | export default {
5 | x86: [ "bin", "win32", "snoretoast.exe" ],
6 | x64: [ "bin", "win64", "snoretoast.exe" ]
7 | };
8 |
--------------------------------------------------------------------------------
/src/web_modules/translation-key.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Simple template string function for annotating translation strings
3 | * @param {string[]} strings
4 | * @param {*} expressions
5 | * @returns {string}
6 | */
7 | export default function t( strings, ...expressions ) {
8 | if ( strings.length === 1 ) {
9 | return strings[0];
10 | }
11 |
12 | const res = [ strings[ 0 ] ];
13 | for ( let i = 0, l = strings.length - 1; i < l; ) {
14 | res.push( expressions[ i ], strings[ ++i ] );
15 | }
16 |
17 | return res.join( "" );
18 | }
19 |
--------------------------------------------------------------------------------
/src/web_modules/transparent-image.js:
--------------------------------------------------------------------------------
1 | export default "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
2 |
--------------------------------------------------------------------------------