diff --git a/build/tasks/webpack/configurators/ember/decorators.js b/build/tasks/webpack/configurators/ember/decorators.js
index aff63a68e4..5a1425f439 100644
--- a/build/tasks/webpack/configurators/ember/decorators.js
+++ b/build/tasks/webpack/configurators/ember/decorators.js
@@ -8,7 +8,10 @@ const webpack = require( "webpack" );
module.exports = function( config, grunt, isProd ) {
config.module.rules.push({
test: /\.js$/,
- include: r( pDependencies, "@ember-decorators" ),
+ include: [
+ r( pDependencies, "@ember-decorators" ),
+ r( pDependencies, "ember-decorators-polyfill" )
+ ],
loader: "babel-loader",
options: buildBabelConfig({
plugins: [
diff --git a/build/tasks/webpack/configurators/ember/source.js b/build/tasks/webpack/configurators/ember/source.js
index 9c93702e70..2b2c5366f5 100644
--- a/build/tasks/webpack/configurators/ember/source.js
+++ b/build/tasks/webpack/configurators/ember/source.js
@@ -33,13 +33,29 @@ module.exports = function( config, grunt, isProd ) {
loader: "babel-loader",
options: buildBabelConfig({
plugins: [
+ [ "@babel/plugin-transform-runtime", {
+ corejs: false,
+ helpers: true,
+ regenerator: false,
+ useESModules: true,
+ // https://github.com/babel/babel/issues/9454#issuecomment-460425922
+ version: "7.2.2"
+ } ],
// translate @ember imports (requires imports to be ignored in webpack - see below)
"babel-plugin-ember-modules-api-polyfill",
// transform decorators
[ "@babel/plugin-proposal-decorators", {
- decoratorsBeforeExport: true
+ // https://emberjs.github.io/rfcs/0440-decorator-support.html
+ // https://github.com/babel/ember-cli-babel/blob/v7.7.3/index.js#L341
+ // Move forward with the Decorators RFC via stage 1 decorators,
+ // and await a stable future decorators proposal.
+ legacy: true
} ],
- "@babel/plugin-proposal-class-properties"
+ [ "@babel/plugin-proposal-class-properties", {
+ // https://babeljs.io/docs/en/babel-plugin-proposal-decorators
+ // When using the legacy mode, class-properties must be used in loose mode
+ loose: true
+ } ]
]
})
});
diff --git a/package.json b/package.json
index 43c88e8e02..9c921b403c 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,7 @@
},
"dependencies": {
"bootstrap": "3.3.1",
- "ember-source": "3.7.1",
+ "ember-source": "3.9.1",
"ember-data": "3.9.0",
"ember-data-model-fragments": "4.0.0",
"ember-fetch": "6.5.0",
@@ -37,14 +37,14 @@
"@babel/plugin-proposal-decorators": "7.3.0",
"@babel/plugin-transform-block-scoping": "7.2.0",
"@babel/plugin-transform-modules-commonjs": "7.2.0",
+ "@babel/plugin-transform-runtime": "7.4.3",
"@babel/plugin-transform-typescript": "7.4.0",
"@ember/test-helpers": "0.7.27",
- "@ember-decorators/argument": "0.8.21",
"@glimmer/syntax": "0.47.4",
"babel-eslint": "10.0.1",
"babel-loader": "8.0.5",
"babel-plugin-debug-macros": "0.2.0",
- "babel-plugin-ember-modules-api-polyfill": "2.6.0",
+ "babel-plugin-ember-modules-api-polyfill": "2.9.0",
"babel-plugin-feature-flags": "0.3.1",
"babel-plugin-filter-imports": "2.0.4",
"babel-plugin-istanbul": "6.0.0",
@@ -54,7 +54,8 @@
"copy-webpack-plugin": "5.1.1",
"css-loader": "2.1.1",
"ember-compatibility-helpers": "1.0.2",
- "ember-decorators": "2.5.1",
+ "ember-decorators": "6.1.1",
+ "ember-decorators-polyfill": "1.0.5",
"ember-qunit": "3.5.1",
"eslint": "5.16.0",
"eslint-loader": "3.0.3",
diff --git a/src/app/data/models/-mixins/adapter.js b/src/app/data/models/-adapters/custom-rest.js
similarity index 67%
rename from src/app/data/models/-mixins/adapter.js
rename to src/app/data/models/-adapters/custom-rest.js
index 05e0f420aa..8a6b439ec6 100644
--- a/src/app/data/models/-mixins/adapter.js
+++ b/src/app/data/models/-adapters/custom-rest.js
@@ -1,118 +1,115 @@
-import { get } from "@ember/object";
import Evented from "@ember/object/evented";
-import Mixin from "@ember/object/mixin";
-import { isNone } from "@ember/utils";
+import RESTAdapter from "ember-data/adapters/rest";
import { AdapterError, InvalidError, TimeoutError } from "ember-data/adapters/errors";
import fetch from "fetch";
+import { descriptor, urlFragments } from "utils/decorators";
const reURL = /^[a-z]+:\/\/([\w.]+)\/(.+)$/i;
const reURLFragment = /^:(\w+)$/;
+const { hasOwnProperty } = {};
+
+@urlFragments({
+ id( type, id ) {
+ if ( id === null || id === undefined ) {
+ throw new Error( "Unknown ID" );
+ }
+
+ return id;
+ }
+})
/**
- * Adapter mixin for using static model names
+ * Adapter for using static model names
* instead of using type.modelName as name
+ * TODO: Change this and get the URL from the model's adapter instead of the model's toString().
+ * EmberData has added a more dynamic system for configuring request URLs a long time ago...
*/
-export default Mixin.create( Evented, {
- mergedProperties: [ "urlFragments" ],
+export default class CustomRESTAdapter extends RESTAdapter.extend( Evented ) {
+ @descriptor({ value: true })
+ useFetch;
- useFetch: true,
-
- urlFragments: {
- id( type, id ) {
- if ( isNone( id ) ) {
- throw new Error( "Unknown ID" );
- }
-
- return id;
- }
- },
findRecord( store, type, id, snapshot ) {
const url = this.buildURL( type, id, snapshot, "findRecord" );
return this.ajax( url, "GET" );
- },
+ }
findAll( store, type, sinceToken ) {
const url = this.buildURL( type, null, null, "findAll" );
const query = sinceToken ? { since: sinceToken } : undefined;
return this.ajax( url, "GET", { data: query } );
- },
+ }
query( store, type, query ) {
const url = this.buildURL( type, null, null, "query", query );
query = this.sortQueryParams ? this.sortQueryParams( query ) : query;
return this.ajax( url, "GET", { data: query } );
- },
+ }
queryRecord( store, type, query ) {
const url = this.buildURL( type, null, null, "queryRecord", query );
query = this.sortQueryParams ? this.sortQueryParams( query ) : query;
return this.ajax( url, "GET", { data: query } );
- },
+ }
- createRecordMethod: "POST",
- createRecord( store, type, snapshot ) {
+ createRecordMethod = "POST";
+ async createRecord( store, type, snapshot ) {
const url = this.buildURL( type, null, snapshot, "createRecord" );
- const method = get( this, "createRecordMethod" );
const data = this.createRecordData( store, type, snapshot );
- return this.ajax( url, method, data )
- .then( data => {
- this.trigger( "createRecord", store, type, snapshot );
- return data;
- });
- },
+ const payload = await this.ajax( url, this.createRecordMethod, data );
+ this.trigger( "createRecord", store, type, snapshot );
+
+ return payload;
+ }
createRecordData( store, type, snapshot ) {
const data = {};
const serializer = store.serializerFor( type.modelName );
serializer.serializeIntoHash( data, type, snapshot, { includeId: true } );
return { data: data };
- },
+ }
- updateRecordMethod: "PUT",
- updateRecord( store, type, snapshot ) {
+ updateRecordMethod = "PUT";
+ async updateRecord( store, type, snapshot ) {
const url = this.buildURL( type, snapshot.id, snapshot, "updateRecord" );
- const method = get( this, "updateRecordMethod" );
const data = this.updateRecordData( store, type, snapshot );
- return this.ajax( url, method, data )
- .then( data => {
- this.trigger( "updateRecord", store, type, snapshot );
- return data;
- });
- },
+ const payload = await this.ajax( url, this.updateRecordMethod, data );
+ this.trigger( "updateRecord", store, type, snapshot );
+
+ return payload;
+ }
updateRecordData( store, type, snapshot ) {
const data = {};
const serializer = store.serializerFor( type.modelName );
serializer.serializeIntoHash( data, type, snapshot );
return { data: data };
- },
+ }
- deleteRecord( store, type, snapshot ) {
+ async deleteRecord( store, type, snapshot ) {
const url = this.buildURL( type, snapshot.id, snapshot, "deleteRecord" );
- return this.ajax( url, "DELETE" )
- .then( data => {
- this.trigger( "deleteRecord", store, type, snapshot );
- return data;
- });
- },
+ const payload = await this.ajax( url, "DELETE" );
+ this.trigger( "deleteRecord", store, type, snapshot );
+
+ return payload;
+ }
urlForCreateRecord( modelName, snapshot ) {
// Why does Ember-Data do this?
// the id is missing on BuildURLMixin.urlForCreateRecord
return this._buildURL( modelName, snapshot.id );
- },
+ }
/**
* Custom buildURL method with type instead of modelName
@@ -122,17 +119,16 @@ export default Mixin.create( Evented, {
* @returns {String}
*/
_buildURL( type, id, data ) {
- const host = get( this, "host" );
- const ns = get( this, "namespace" );
+ const { host, namespace } = this;
const url = [ host ];
// append the adapter specific namespace
- if ( ns ) { url.push( ns ); }
+ if ( namespace ) { url.push( namespace ); }
// append the type fragments (and process the dynamic ones)
url.push( ...this.buildURLFragments( type, id, data ) );
return url.join( "/" );
- },
+ }
/**
* Dynamic URL fragments
@@ -142,7 +138,7 @@ export default Mixin.create( Evented, {
* @returns {String[]}
*/
buildURLFragments( type, id, data ) {
- const urlFragments = get( this, "urlFragments" );
+ const urlFragments = this.constructor.urlFragments;
let idFound = false;
const url = String( type )
@@ -152,7 +148,7 @@ export default Mixin.create( Evented, {
idFound = true;
}
- if ( urlFragments.hasOwnProperty( key ) ) {
+ if ( hasOwnProperty.call( urlFragments, key ) ) {
return urlFragments[ key ].call( this, type, id, data );
}
@@ -161,33 +157,35 @@ export default Mixin.create( Evented, {
}) );
// append ID if no :id fragment was defined and ID exists
- if ( !idFound && !isNone( id ) ) {
+ if ( !idFound && id !== null && id !== undefined ) {
url.push( id );
}
return url;
- },
+ }
- ajax( url ) {
- return this._super( ...arguments )
- .catch( err => {
- if ( err instanceof AdapterError ) {
- const _url = reURL.exec( url );
- err.host = _url && _url[1] || get( this, "host" );
- err.path = _url && _url[2] || get( this, "namespace" );
- }
+ async ajax( url ) {
+ try {
+ return await super.ajax( ...arguments );
- return Promise.reject( err );
- });
- },
+ } catch ( err ) {
+ if ( err instanceof AdapterError ) {
+ const _url = reURL.exec( url );
+ err.host = _url && _url[1] || this.host;
+ err.path = _url && _url[2] || this.namespace;
+ }
+
+ throw err;
+ }
+ }
ajaxOptions() {
- const options = this._super( ...arguments );
+ const options = super.ajaxOptions( ...arguments );
options.cache = "no-cache";
return options;
- },
+ }
_fetchRequest( options ) {
return new Promise( ( resolve, reject ) => {
@@ -199,12 +197,12 @@ export default Mixin.create( Evented, {
.finally( () => clearTimeout( timeout ) )
.then( resolve, reject );
});
- },
+ }
isSuccess( status, headers, payload ) {
- return this._super( ...arguments )
+ return super.isSuccess( ...arguments )
&& ( payload ? !payload.error : true );
- },
+ }
handleResponse( status, headers, payload ) {
if ( this.isSuccess( status, headers, payload ) ) {
@@ -220,5 +218,4 @@ export default Mixin.create( Evented, {
status
}]);
}
-
-});
+}
diff --git a/src/app/data/models/-mixins/polymorphic-fragment-serializer.js b/src/app/data/models/-serializers/polymorphic-fragment-serializer.js
similarity index 80%
rename from src/app/data/models/-mixins/polymorphic-fragment-serializer.js
rename to src/app/data/models/-serializers/polymorphic-fragment-serializer.js
index fc521aa877..03d3b08efb 100644
--- a/src/app/data/models/-mixins/polymorphic-fragment-serializer.js
+++ b/src/app/data/models/-serializers/polymorphic-fragment-serializer.js
@@ -4,14 +4,14 @@ import JSONSerializer from "ember-data/serializers/json";
const { hasOwnProperty } = {};
-export default JSONSerializer.extend({
+export default class PolymorphicFragmentSerializer extends JSONSerializer {
/**
* @param {Snapshot} snapshot
* @param {Model} snapshot.record
* @returns {Object}
*/
serialize({ record }) {
- const json = this._super( ...arguments );
+ const json = super.serialize( ...arguments );
const { models, modelBaseName, typeKey } = this;
for ( const [ type, model ] of models ) {
@@ -22,7 +22,7 @@ export default JSONSerializer.extend({
}
return json;
- },
+ }
/**
* Fix removal of the `typeKey` property
@@ -32,11 +32,11 @@ export default JSONSerializer.extend({
*/
extractAttributes( modelClass, data ) {
const { typeKey } = this;
- const attributes = this._super( ...arguments );
+ const attributes = super.extractAttributes( ...arguments );
if ( data && hasOwnProperty.call( data, typeKey ) ) {
attributes[ typeKey ] = data[ typeKey ];
}
return attributes;
}
-});
+}
diff --git a/src/app/data/models/auth/adapter.js b/src/app/data/models/auth/adapter.js
index ce07c84946..45ecca7342 100644
--- a/src/app/data/models/auth/adapter.js
+++ b/src/app/data/models/auth/adapter.js
@@ -1,6 +1,6 @@
import LocalStorageAdapter from "ember-localstorage-adapter/adapters/ls-adapter";
-export default LocalStorageAdapter.extend({
- namespace: "auth"
-});
+export default class AuthAdapter extends LocalStorageAdapter {
+ namespace = "auth";
+}
diff --git a/src/app/data/models/auth/model.js b/src/app/data/models/auth/model.js
index 05e9a2aa7b..cff4292c57 100644
--- a/src/app/data/models/auth/model.js
+++ b/src/app/data/models/auth/model.js
@@ -1,28 +1,28 @@
-import { get, computed } from "@ember/object";
+import { computed } from "@ember/object";
import attr from "ember-data/attr";
import Model from "ember-data/model";
+import { name } from "utils/decorators";
-export default Model.extend( /** @class Auth */ {
- access_token: attr( "string" ),
- scope : attr( "string" ),
- date : attr( "date" ),
+@name( "Auth" )
+export default class Auth extends Model {
+ @attr( "string" )
+ access_token;
+ @attr( "string" )
+ scope;
+ @attr( "date" )
+ date;
- // volatile property
- user_id: null,
- user_name: null,
+ // runtime "attributes"
+ user_id = null;
+ user_name = null;
// status properties
- isPending : false,
- isLoggedIn: computed( "access_token", "user_id", "isPending", function() {
- let token = get( this, "access_token" );
- let id = get( this, "user_id" );
- let pending = get( this, "isPending" );
+ isPending = false;
- return token && id && !pending;
- })
-
-}).reopenClass({
- toString() { return "Auth"; }
-});
+ @computed( "access_token", "user_id", "isPending" )
+ get isLoggedIn() {
+ return this.access_token && this.user_id && !this.isPending;
+ }
+}
diff --git a/src/app/data/models/channel-settings/adapter.js b/src/app/data/models/channel-settings/adapter.js
index d20107e153..e7f03c3e1b 100644
--- a/src/app/data/models/channel-settings/adapter.js
+++ b/src/app/data/models/channel-settings/adapter.js
@@ -1,6 +1,6 @@
import LocalStorageAdapter from "ember-localstorage-adapter/adapters/ls-adapter";
-export default LocalStorageAdapter.extend({
- namespace: "channelsettings"
-});
+export default class ChannelSettingsAdapter extends LocalStorageAdapter {
+ namespace = "channelsettings";
+}
diff --git a/src/app/data/models/channel-settings/model.js b/src/app/data/models/channel-settings/model.js
index 2e26b8c877..3972d78106 100644
--- a/src/app/data/models/channel-settings/model.js
+++ b/src/app/data/models/channel-settings/model.js
@@ -3,31 +3,31 @@ import Model from "ember-data/model";
import SettingsStreaming from "data/models/settings/streaming/fragment";
import SettingsStreams from "data/models/settings/streams/fragment";
import SettingsNotification from "data/models/settings/notification/fragment";
+import { name } from "utils/decorators";
-/**
- * @type {Object.}
- */
-const attributes = {
- streaming_quality: [ SettingsStreaming, "quality", "streaming.quality" ],
- streaming_low_latency: [ SettingsStreaming, "low_latency", "streaming.low_latency" ],
- streaming_disable_ads: [ SettingsStreaming, "disable_ads", "streaming.disable_ads" ],
- streams_chat_open: [ SettingsStreams, "chat_open", "streams.chat_open" ],
- notification_enabled: [ SettingsNotification, "enabled", "notification.enabled" ]
-};
-
-for ( const [ name, [ settings, prop, settingsPath ] ] of Object.entries( attributes ) ) {
- const meta = settings.metaForProperty( prop );
- if ( !meta || !meta.isAttribute ) { continue; }
+const settingsAttribute = ( Settings, attribute, settingsPath ) => () => {
+ const meta = Settings.metaForProperty( attribute );
+ if ( !meta || !meta.isAttribute ) { return; }
- attributes[ name ] = attr( meta.type, {
+ return attr( meta.type, {
defaultValue: null,
// the ChannelSettingsController needs this attribute option
settingsPath
});
-}
+};
-export default Model.extend( attributes ).reopenClass({
- toString() { return "ChannelSettings"; }
-});
+@name( "ChannelSettings" )
+export default class ChannelSettings extends Model {
+ @settingsAttribute( SettingsStreaming, "quality", "streaming.quality" )
+ streaming_quality;
+ @settingsAttribute( SettingsStreaming, "low_latency", "streaming.low_latency" )
+ streaming_low_latency;
+ @settingsAttribute( SettingsStreaming, "disable_ads", "streaming.disable_ads" )
+ streaming_disable_ads;
+ @settingsAttribute( SettingsStreams, "chat_open", "streams.chat_open" )
+ streams_chat_open;
+ @settingsAttribute( SettingsNotification, "enabled", "notification.enabled" )
+ notification_enabled;
+}
diff --git a/src/app/data/models/github/releases/adapter.js b/src/app/data/models/github/releases/adapter.js
index 3d0584d5be..4db2c68158 100644
--- a/src/app/data/models/github/releases/adapter.js
+++ b/src/app/data/models/github/releases/adapter.js
@@ -1,22 +1,21 @@
-import RESTAdapter from "ember-data/adapters/rest";
import { update as updateConfig } from "config";
-import AdapterMixin from "data/models/-mixins/adapter";
+import CustomRESTAdapter from "data/models/-adapters/custom-rest";
const { githubreleases: { host, namespace } } = updateConfig;
-export default RESTAdapter.extend( AdapterMixin, {
- host,
- namespace,
+export default class GithubReleasesAdapter extends CustomRESTAdapter {
+ host = host;
+ namespace = namespace;
queryRecord( store, type, query ) {
const url = this.buildURL( type, null, null, "queryRecord", query );
return this.ajax( url, "GET", { data: {} } );
- },
+ }
urlForQueryRecord( query, modelName ) {
return this._buildURL( modelName, query );
}
-});
+}
diff --git a/src/app/data/models/github/releases/model.js b/src/app/data/models/github/releases/model.js
index ebdfe8a615..9b5e1b490c 100644
--- a/src/app/data/models/github/releases/model.js
+++ b/src/app/data/models/github/releases/model.js
@@ -1,30 +1,46 @@
import { computed } from "@ember/object";
import attr from "ember-data/attr";
import Model from "ember-data/model";
+import { name } from "utils/decorators";
-export default Model.extend( /** @class GithubReleases */ {
- assets: attr(),
- assets_url: attr(),
- author: attr(),
- body: attr(),
- created_at: attr(),
- draft: attr( "boolean" ),
- html_url: attr( "string" ),
- name: attr(),
- prerelease: attr(),
- published_at: attr(),
- tag_name: attr( "string" ),
- tarball_url: attr(),
- target_commitish: attr(),
- upload_url: attr(),
- url: attr(),
- zipball_url: attr(),
+@name( "releases" )
+export default class GithubReleases extends Model {
+ @attr
+ assets;
+ @attr
+ assets_url;
+ @attr
+ author;
+ @attr
+ body;
+ @attr
+ created_at;
+ @attr( "boolean" )
+ draft;
+ @attr( "string" )
+ html_url;
+ @attr
+ name;
+ @attr
+ prerelease;
+ @attr
+ published_at;
+ @attr( "string" )
+ tag_name;
+ @attr
+ tarball_url;
+ @attr
+ target_commitish;
+ @attr
+ upload_url;
+ @attr
+ url;
+ @attr
+ zipball_url;
- version: computed( "tag_name", function() {
+ @computed( "tag_name" )
+ get version() {
return this.tag_name.replace( /^v/, "" );
- })
-
-}).reopenClass({
- toString() { return "releases"; }
-});
+ }
+}
diff --git a/src/app/data/models/github/releases/serializer.js b/src/app/data/models/github/releases/serializer.js
index b5103a4f79..fe1b1b8880 100644
--- a/src/app/data/models/github/releases/serializer.js
+++ b/src/app/data/models/github/releases/serializer.js
@@ -1,16 +1,14 @@
import RESTSerializer from "ember-data/serializers/rest";
-export default RESTSerializer.extend({
- modelNameFromPayloadKey() {
- return "githubReleases";
- },
+export default class GithubReleasesSerializer extends RESTSerializer {
+ modelNameFromPayloadKey = () => "github-releases";
normalizeResponse( store, primaryModelClass, payload, id, requestType ) {
payload = {
- githubReleases: payload
+ [ this.modelNameFromPayloadKey() ]: payload
};
- return this._super( store, primaryModelClass, payload, id, requestType );
+ return super.normalizeResponse( store, primaryModelClass, payload, id, requestType );
}
-});
+}
diff --git a/src/app/data/models/search/adapter.js b/src/app/data/models/search/adapter.js
index 9ca079821a..095320a84a 100644
--- a/src/app/data/models/search/adapter.js
+++ b/src/app/data/models/search/adapter.js
@@ -1,6 +1,6 @@
import LocalStorageAdapter from "ember-localstorage-adapter/adapters/ls-adapter";
-export default LocalStorageAdapter.extend({
- namespace: "search"
-});
+export default class SearchAdapter extends LocalStorageAdapter {
+ namespace = "search";
+}
diff --git a/src/app/data/models/search/model.js b/src/app/data/models/search/model.js
index 41dfcfa0c1..b8bb65e601 100644
--- a/src/app/data/models/search/model.js
+++ b/src/app/data/models/search/model.js
@@ -1,39 +1,47 @@
-import { get, computed } from "@ember/object";
+import { computed } from "@ember/object";
import attr from "ember-data/attr";
import Model from "ember-data/model";
+import { name } from "utils/decorators";
-export default Model.extend({
- query : attr( "string" ),
- filter: attr( "string" ),
- date : attr( "date" ),
+const { hasOwnProperty } = {};
- label: computed( "filter", function() {
- const filter = get( this, "filter" );
- return this.constructor.getLabel( filter );
- })
-}).reopenClass({
- toString() { return "Search"; },
-
- filters: [
+@name( "Search" )
+export default class Search extends Model {
+ static filters = [
{ label: "All", id: "all" },
{ label: "Game", id: "games" },
{ label: "Channel", id: "channels" },
{ label: "Stream", id: "streams" }
- ],
+ ];
- filtersmap: computed(function() {
+ @computed()
+ static get filtersmap() {
return this.filters.reduce( ( map, filter ) => {
map[ filter.id ] = filter;
+
return map;
}, {} );
- }),
+ }
+
+ static getLabel( filter ) {
+ const { filtersmap } = this;
- getLabel( filter ) {
- const map = get( this, "filtersmap" );
- return map.hasOwnProperty( filter )
- ? map[ filter ].label
+ return hasOwnProperty.call( filtersmap, filter )
+ ? filtersmap[ filter ].label
: "All";
}
-});
+
+ @attr( "string" )
+ query;
+ @attr( "string" )
+ filter;
+ @attr( "date" )
+ date;
+
+ @computed( "filter" )
+ get label() {
+ return this.constructor.getLabel( this.filter );
+ }
+}
diff --git a/src/app/data/models/settings/adapter.js b/src/app/data/models/settings/adapter.js
index f3bee52877..9e5478cf6c 100644
--- a/src/app/data/models/settings/adapter.js
+++ b/src/app/data/models/settings/adapter.js
@@ -1,6 +1,6 @@
import LocalStorageAdapter from "ember-localstorage-adapter/adapters/ls-adapter";
-export default LocalStorageAdapter.extend({
- namespace: "settings"
-});
+export default class SettingsAdapter extends LocalStorageAdapter {
+ namespace = "settings";
+}
diff --git a/src/app/data/models/settings/chat/fragment.js b/src/app/data/models/settings/chat/fragment.js
index 7c33982243..f02e761270 100644
--- a/src/app/data/models/settings/chat/fragment.js
+++ b/src/app/data/models/settings/chat/fragment.js
@@ -1,13 +1,16 @@
import attr from "ember-data/attr";
import Fragment from "ember-data-model-fragments/fragment";
-import { fragment } from "ember-data-model-fragments/attributes";
import chatProviders from "services/chat/providers";
+import { fragment } from "utils/decorators";
const defaultProvider = Object.keys( chatProviders )[0] || "browser";
-export default Fragment.extend({
- provider: attr( "string", { defaultValue: defaultProvider } ),
- providers: fragment( "settingsChatProviders", { defaultValue: {} } )
-});
+export default class SettingsChat extends Fragment {
+ @attr( "string", { defaultValue: defaultProvider } )
+ provider;
+ /** @type {SettingsChatProviders} */
+ @fragment( "settings-chat-providers" )
+ providers;
+}
diff --git a/src/app/data/models/settings/chat/provider/fragment.js b/src/app/data/models/settings/chat/provider/fragment.js
index 5d6849c25a..44c68c8925 100644
--- a/src/app/data/models/settings/chat/provider/fragment.js
+++ b/src/app/data/models/settings/chat/provider/fragment.js
@@ -1,3 +1,4 @@
+import { defineProperty } from "@ember/object";
import attr from "ember-data/attr";
import Fragment from "ember-data-model-fragments/fragment";
import { chat as chatConfig } from "config";
@@ -9,7 +10,7 @@ const providers = new Map();
// chat providers don't share common attributes
-const SettingsChatProvider = Fragment.extend();
+class SettingsChatProvider extends Fragment {}
const { hasOwnProperty } = {};
@@ -19,7 +20,7 @@ for ( const [ id ] of Object.entries( chatProviders ) ) {
// a chat provider needs to have a config object (at least a label is required)
if ( !hasOwnProperty.call( chatConfig, id ) ) { continue; }
- const providerAttributes = {};
+ const provider = class extends SettingsChatProvider {};
// dynamic fragment attributes defined in the chat config file
const { attributes } = chatConfig[ id ];
@@ -28,11 +29,11 @@ for ( const [ id ] of Object.entries( chatProviders ) ) {
const { name, type } = param;
// set the whole config object as attribute options object
// this will be used in the SettingsChatRoute template via attribute.options.property
- providerAttributes[ name ] = attr( type, param );
+ const prop = attr( type, param );
+ defineProperty( provider.prototype, name, prop );
}
}
- const provider = SettingsChatProvider.extend( providerAttributes );
providers.set( id, provider );
}
diff --git a/src/app/data/models/settings/chat/provider/serializer.js b/src/app/data/models/settings/chat/provider/serializer.js
index 742c835d66..67e0d6870e 100644
--- a/src/app/data/models/settings/chat/provider/serializer.js
+++ b/src/app/data/models/settings/chat/provider/serializer.js
@@ -1,9 +1,10 @@
-import PolymorphicFragmentSerializer from "data/models/-mixins/polymorphic-fragment-serializer";
+import PolymorphicFragmentSerializer
+ from "data/models/-serializers/polymorphic-fragment-serializer";
import { providers, typeKey } from "./fragment";
-export default PolymorphicFragmentSerializer.extend({
- models: providers,
- modelBaseName: "settings-chat-provider",
- typeKey
-});
+export default class SettingsChatProviderSerializer extends PolymorphicFragmentSerializer {
+ models = providers;
+ modelBaseName = "settings-chat-provider";
+ typeKey = typeKey;
+}
diff --git a/src/app/data/models/settings/chat/providers/fragment.js b/src/app/data/models/settings/chat/providers/fragment.js
index 79ba231d81..c9e586dfb5 100644
--- a/src/app/data/models/settings/chat/providers/fragment.js
+++ b/src/app/data/models/settings/chat/providers/fragment.js
@@ -1,19 +1,22 @@
+import { defineProperty } from "@ember/object";
import Fragment from "ember-data-model-fragments/fragment";
import { fragment } from "ember-data-model-fragments/attributes";
-import { typeKey } from "../provider/fragment";
import chatProviders from "services/chat/providers";
+import { typeKey } from "../provider/fragment";
+
+class SettingsChatProviders extends Fragment {}
-const attributes = {};
for ( const [ type ] of Object.entries( chatProviders ) ) {
- attributes[ type ] = fragment( "settings-chat-provider", {
+ const prop = fragment( "settings-chat-provider", {
defaultValue: {
[ typeKey ]: `settings-chat-provider-${type}`
},
polymorphic: true,
typeKey
});
+ defineProperty( SettingsChatProviders.prototype, type, prop );
}
-export default Fragment.extend( attributes );
+export default SettingsChatProviders;
diff --git a/src/app/data/models/settings/gui/fragment.js b/src/app/data/models/settings/gui/fragment.js
index ccbab0cf9a..b8bad9a8b1 100644
--- a/src/app/data/models/settings/gui/fragment.js
+++ b/src/app/data/models/settings/gui/fragment.js
@@ -1,4 +1,4 @@
-import { get, computed } from "@ember/object";
+import { computed } from "@ember/object";
import attr from "ember-data/attr";
import Fragment from "ember-data-model-fragments/fragment";
@@ -17,45 +17,55 @@ export const ATTR_GUI_FOCUSREFRESH_TWO = 120000;
export const ATTR_GUI_FOCUSREFRESH_FIVE = 300000;
-export default Fragment.extend({
- externalcommands: attr( "boolean", { defaultValue: false } ),
- focusrefresh: attr( "number", { defaultValue: ATTR_GUI_FOCUSREFRESH_NONE } ),
- hidebuttons: attr( "boolean", { defaultValue: false } ),
- homepage: attr( "string", { defaultValue: "/featured" } ),
- integration: attr( "number", { defaultValue: ATTR_GUI_INTEGRATION_BOTH } ),
- language: attr( "string", { defaultValue: "auto" } ),
- minimize: attr( "number", { defaultValue: ATTR_GUI_MINIMIZE_NOOP } ),
- minimizetotray: attr( "boolean", { defaultValue: false } ),
- smoothscroll: attr( "boolean", { defaultValue: true } ),
- theme: attr( "string", { defaultValue: "system" } ),
-
-
- isVisibleInTaskbar: computed( "integration", function() {
- return ( get( this, "integration" ) & ATTR_GUI_INTEGRATION_TASKBAR ) > 0;
- }),
-
- isVisibleInTray: computed( "integration", function() {
- return ( get( this, "integration" ) & ATTR_GUI_INTEGRATION_TRAY ) > 0;
- })
-
-}).reopenClass({
-
- integration: [
+export default class SettingsGui extends Fragment {
+ static integration = [
{ id: ATTR_GUI_INTEGRATION_BOTH, label: "both" },
{ id: ATTR_GUI_INTEGRATION_TASKBAR, label: "taskbar" },
{ id: ATTR_GUI_INTEGRATION_TRAY, label: "tray" }
- ],
+ ];
- minimize: [
+ static minimize = [
{ id: ATTR_GUI_MINIMIZE_NOOP, label: "noop", disabled: false },
{ id: ATTR_GUI_MINIMIZE_MINIMIZE, label: "minimize", disabled: false },
{ id: ATTR_GUI_MINIMIZE_TRAY, label: "tray", disabled: false }
- ],
+ ];
- focusrefresh: [
+ static focusrefresh = [
{ id: ATTR_GUI_FOCUSREFRESH_NONE, label: "none" },
{ id: ATTR_GUI_FOCUSREFRESH_ONE, label: "one" },
{ id: ATTR_GUI_FOCUSREFRESH_TWO, label: "two" },
{ id: ATTR_GUI_FOCUSREFRESH_FIVE, label: "five" }
- ]
-});
+ ];
+
+ @attr( "boolean", { defaultValue: false } )
+ externalcommands;
+ @attr( "number", { defaultValue: ATTR_GUI_FOCUSREFRESH_NONE } )
+ focusrefresh;
+ @attr( "boolean", { defaultValue: false } )
+ hidebuttons;
+ @attr( "string", { defaultValue: "/featured" } )
+ homepage;
+ @attr( "number", { defaultValue: ATTR_GUI_INTEGRATION_BOTH } )
+ integration;
+ @attr( "string", { defaultValue: "auto" } )
+ language;
+ @attr( "number", { defaultValue: ATTR_GUI_MINIMIZE_NOOP } )
+ minimize;
+ @attr( "boolean", { defaultValue: false } )
+ minimizetotray;
+ @attr( "boolean", { defaultValue: true } )
+ smoothscroll;
+ @attr( "string", { defaultValue: "system" } )
+ theme;
+
+
+ @computed( "integration" )
+ get isVisibleInTaskbar() {
+ return ( this.integration & ATTR_GUI_INTEGRATION_TASKBAR ) > 0;
+ }
+
+ @computed( "integration" )
+ get isVisibleInTray() {
+ return ( this.integration & ATTR_GUI_INTEGRATION_TRAY ) > 0;
+ }
+}
diff --git a/src/app/data/models/settings/hotkeys/action/fragment.js b/src/app/data/models/settings/hotkeys/action/fragment.js
index 76f97f430b..0672ab8567 100644
--- a/src/app/data/models/settings/hotkeys/action/fragment.js
+++ b/src/app/data/models/settings/hotkeys/action/fragment.js
@@ -1,8 +1,10 @@
import Fragment from "ember-data-model-fragments/fragment";
-import { fragment } from "ember-data-model-fragments/attributes";
+import { fragment } from "utils/decorators";
-export default Fragment.extend({
- primary: fragment( "settings-hotkeys-hotkey", { defaultValue: {} } ),
- secondary: fragment( "settings-hotkeys-hotkey", { defaultValue: {} } )
-});
+export default class SettingsHotkeysAction extends Fragment {
+ @fragment( "settings-hotkeys-hotkey" )
+ primary;
+ @fragment( "settings-hotkeys-hotkey" )
+ secondary;
+}
diff --git a/src/app/data/models/settings/hotkeys/fragment.js b/src/app/data/models/settings/hotkeys/fragment.js
index 338ebbc2f3..2755357f13 100644
--- a/src/app/data/models/settings/hotkeys/fragment.js
+++ b/src/app/data/models/settings/hotkeys/fragment.js
@@ -1,11 +1,14 @@
+import { defineProperty } from "@ember/object";
import Fragment from "ember-data-model-fragments/fragment";
import { fragment } from "ember-data-model-fragments/attributes";
import { hotkeys as hotkeysConfig } from "config";
import { typeKey } from "./namespace/fragment";
-const attributes = {};
-for ( const [ namespaceName, { actions } ] of Object.entries( hotkeysConfig ) ) {
+class SettingsHotkeys extends Fragment {}
+
+for ( const [ type, { actions } ] of Object.entries( hotkeysConfig ) ) {
+ let hasActions = false;
const defaultValue = {};
for ( const [ action, hotkey ] of Object.entries( actions ) ) {
if ( typeof hotkey === "string" ) { continue; }
@@ -13,16 +16,20 @@ for ( const [ namespaceName, { actions } ] of Object.entries( hotkeysConfig ) )
primary: {},
secondary: {}
};
+ hasActions = true;
}
// namespaces without actions should not exist
- if ( !Object.keys( defaultValue ).length ) { continue; }
- defaultValue[ typeKey ] = `settings-hotkeys-namespace-${namespaceName}`;
- attributes[ namespaceName ] = fragment( "settings-hotkeys-namespace", {
- defaultValue,
+ if ( !hasActions ) { continue; }
+
+ const prop = fragment( "settings-hotkeys-namespace", {
+ defaultValue: Object.assign( defaultValue, {
+ [ typeKey ]: `settings-hotkeys-namespace-${type}`
+ }),
polymorphic: true,
typeKey
});
+ defineProperty( SettingsHotkeys.prototype, type, prop );
}
-export default Fragment.extend( attributes );
+export default SettingsHotkeys;
diff --git a/src/app/data/models/settings/hotkeys/hotkey/fragment.js b/src/app/data/models/settings/hotkeys/hotkey/fragment.js
index ea5ea014fa..af3e64ceaf 100644
--- a/src/app/data/models/settings/hotkeys/hotkey/fragment.js
+++ b/src/app/data/models/settings/hotkeys/hotkey/fragment.js
@@ -2,11 +2,17 @@ import attr from "ember-data/attr";
import Fragment from "ember-data-model-fragments/fragment";
-export default Fragment.extend({
- disabled: attr( "boolean", { defaultValue: false } ),
- code: attr( "string", { defaultValue: null } ),
- altKey: attr( "boolean", { defaultValue: false } ),
- ctrlKey: attr( "boolean", { defaultValue: false } ),
- metaKey: attr( "boolean", { defaultValue: false } ),
- shiftKey: attr( "boolean", { defaultValue: false } )
-});
+export default class SettingsHotkeysHotkey extends Fragment {
+ @attr( "boolean", { defaultValue: false } )
+ disabled;
+ @attr( "string", { defaultValue: null } )
+ code;
+ @attr( "boolean", { defaultValue: false } )
+ altKey;
+ @attr( "boolean", { defaultValue: false } )
+ ctrlKey;
+ @attr( "boolean", { defaultValue: false } )
+ metaKey;
+ @attr( "boolean", { defaultValue: false } )
+ shiftKey;
+}
diff --git a/src/app/data/models/settings/hotkeys/namespace/fragment.js b/src/app/data/models/settings/hotkeys/namespace/fragment.js
index 7ec8456956..db0b3b304a 100644
--- a/src/app/data/models/settings/hotkeys/namespace/fragment.js
+++ b/src/app/data/models/settings/hotkeys/namespace/fragment.js
@@ -1,5 +1,6 @@
-import Fragment from "ember-data-model-fragments/fragment";
+import { defineProperty } from "@ember/object";
import { fragment } from "ember-data-model-fragments/attributes";
+import Fragment from "ember-data-model-fragments/fragment";
import { hotkeys as hotkeysConfig } from "config";
@@ -7,18 +8,22 @@ const typeKey = "type";
const namespaces = new Map();
-const SettingsHotkeysNamespace = Fragment.extend();
+class SettingsHotkeysNamespace extends Fragment {}
for ( const [ namespaceName, { actions } ] of Object.entries( hotkeysConfig ) ) {
- const attributes = {};
+ const namespace = class extends SettingsHotkeysNamespace {};
+
+ let hasActions = false;
for ( const [ action, hotkeys ] of Object.entries( actions ) ) {
if ( typeof hotkeys === "string" ) { continue; }
- attributes[ action ] = fragment( "settings-hotkeys-action", { defaultValue: {} } );
+ const prop = fragment( "settings-hotkeys-action" );
+ defineProperty( namespace.prototype, action, prop );
+ hasActions = true;
}
// namespaces without actions should not exist
- if ( !Object.keys( attributes ).length ) { continue; }
- const namespace = SettingsHotkeysNamespace.extend( attributes );
+ if ( !hasActions ) { continue; }
+
namespaces.set( namespaceName, namespace );
}
diff --git a/src/app/data/models/settings/hotkeys/namespace/serializer.js b/src/app/data/models/settings/hotkeys/namespace/serializer.js
index 8ec46fa45a..9c67ef3e6a 100644
--- a/src/app/data/models/settings/hotkeys/namespace/serializer.js
+++ b/src/app/data/models/settings/hotkeys/namespace/serializer.js
@@ -1,9 +1,10 @@
-import PolymorphicFragmentSerializer from "data/models/-mixins/polymorphic-fragment-serializer";
+import PolymorphicFragmentSerializer
+ from "data/models/-serializers/polymorphic-fragment-serializer";
import { namespaces, typeKey } from "./fragment";
-export default PolymorphicFragmentSerializer.extend({
- models: namespaces,
- modelBaseName: "settings-hotkeys-namespace",
- typeKey
-});
+export default class SettingsHotkeysNamespaceSerializer extends PolymorphicFragmentSerializer {
+ models = namespaces;
+ modelBaseName = "settings-hotkeys-namespace";
+ typeKey = typeKey;
+}
diff --git a/src/app/data/models/settings/model.js b/src/app/data/models/settings/model.js
index 8e3e81336f..b866b87a5a 100644
--- a/src/app/data/models/settings/model.js
+++ b/src/app/data/models/settings/model.js
@@ -1,20 +1,28 @@
import attr from "ember-data/attr";
import Model from "ember-data/model";
-import { fragment } from "ember-data-model-fragments/attributes";
+import { fragment, name } from "utils/decorators";
-/**
- * @class Settings
- */
-export default Model.extend({
- advanced: attr( "boolean", { defaultValue: false } ),
- gui: fragment( "settingsGui", { defaultValue: {} } ),
- streaming: fragment( "settingsStreaming", { defaultValue: {} } ),
- streams: fragment( "settingsStreams", { defaultValue: {} } ),
- chat: fragment( "settingsChat", { defaultValue: {} } ),
- notification: fragment( "settingsNotification", { defaultValue: {} } ),
- hotkeys: fragment( "settingsHotkeys", { defaultValue: {} } )
-
-}).reopenClass({
- toString() { return "Settings"; }
-});
+@name( "Settings" )
+export default class Settings extends Model {
+ @attr( "boolean", { defaultValue: false } )
+ advanced;
+ /** @type {SettingsGui} */
+ @fragment( "settings-gui" )
+ gui;
+ /** @type {SettingsStreaming} */
+ @fragment( "settings-streaming" )
+ streaming;
+ /** @type {SettingsStreams} */
+ @fragment( "settings-streams" )
+ streams;
+ /** @type {SettingsChat} */
+ @fragment( "settings-chat" )
+ chat;
+ /** @type {SettingsNotification} */
+ @fragment( "settings-notification" )
+ notification;
+ /** @type {SettingsHotkeys} */
+ @fragment( "settings-hotkeys" )
+ hotkeys;
+}
diff --git a/src/app/data/models/settings/notification/fragment.js b/src/app/data/models/settings/notification/fragment.js
index b94b06bcb5..6d3bcd90e7 100644
--- a/src/app/data/models/settings/notification/fragment.js
+++ b/src/app/data/models/settings/notification/fragment.js
@@ -8,43 +8,50 @@ export const ATTR_NOTIFY_CLICK_STREAM = 2;
export const ATTR_NOTIFY_CLICK_STREAMANDCHAT = 3;
-export default Fragment.extend({
- enabled: attr( "boolean", { defaultValue: true } ),
- provider: attr( "string", { defaultValue: "auto" } ),
- filter: attr( "boolean", { defaultValue: true } ),
- filter_vodcasts: attr( "boolean", { defaultValue: true } ),
- grouping: attr( "boolean", { defaultValue: true } ),
- click: attr( "number", { defaultValue: 1 } ),
- click_group: attr( "number", { defaultValue: 1 } ),
- click_restore: attr( "boolean", { defaultValue: true } ),
- badgelabel: attr( "boolean", { defaultValue: true } )
-
-}).reopenClass({
-
- providers: [
+export default class SettingsNotification extends Fragment {
+ static providers = [
{ id: "auto" },
{ id: "native" },
{ id: "snoretoast" },
{ id: "growl" },
{ id: "rich" }
- ],
+ ];
- filter: [
+ static filter = [
{ id: true, label: "blacklist" },
{ id: false, label: "whitelist" }
- ],
+ ];
- click: [
+ static click = [
{ id: ATTR_NOTIFY_CLICK_NOOP, label: "noop" },
{ id: ATTR_NOTIFY_CLICK_FOLLOWED, label: "followed" },
{ id: ATTR_NOTIFY_CLICK_STREAM, label: "stream" },
{ id: ATTR_NOTIFY_CLICK_STREAMANDCHAT, label: "stream-and-chat" }
- ],
+ ];
- clickGroup: [
+ static clickGroup = [
{ id: ATTR_NOTIFY_CLICK_NOOP, label: "noop" },
{ id: ATTR_NOTIFY_CLICK_FOLLOWED, label: "followed" },
{ id: ATTR_NOTIFY_CLICK_STREAM, label: "stream" },
{ id: ATTR_NOTIFY_CLICK_STREAMANDCHAT, label: "stream-and-chat" }
- ]
-});
+ ];
+
+ @attr( "boolean", { defaultValue: true } )
+ enabled;
+ @attr( "string", { defaultValue: "auto" } )
+ provider;
+ @attr( "boolean", { defaultValue: true } )
+ filter;
+ @attr( "boolean", { defaultValue: true } )
+ filter_vodcasts;
+ @attr( "boolean", { defaultValue: true } )
+ grouping;
+ @attr( "number", { defaultValue: 1 } )
+ click;
+ @attr( "number", { defaultValue: 1 } )
+ click_group;
+ @attr( "boolean", { defaultValue: true } )
+ click_restore;
+ @attr( "boolean", { defaultValue: true } )
+ badgelabel;
+}
diff --git a/src/app/data/models/settings/streaming/fragment.js b/src/app/data/models/settings/streaming/fragment.js
index 7f7ac3a2b5..72fd1f82f5 100644
--- a/src/app/data/models/settings/streaming/fragment.js
+++ b/src/app/data/models/settings/streaming/fragment.js
@@ -1,11 +1,11 @@
-import { get, computed } from "@ember/object";
+import { computed } from "@ember/object";
+import { equal } from "@ember/object/computed";
import attr from "ember-data/attr";
import Fragment from "ember-data-model-fragments/fragment";
-import { fragment } from "ember-data-model-fragments/attributes";
import { streaming as streamingConfig } from "config";
+import { fragment } from "utils/decorators";
-const { equal } = computed;
const { providers, "default-provider": defaultProvider } = streamingConfig;
const { MAX_SAFE_INTEGER: MAX } = Number;
@@ -16,40 +16,8 @@ export const ATTR_STREAMING_PLAYER_INPUT_HTTP = "http";
export const ATTR_STREAMING_PLAYER_INPUT_PASSTHROUGH = "passthrough";
-export default Fragment.extend({
- provider: attr( "string", { defaultValue: defaultProvider } ),
- providers: fragment( "settingsStreamingProviders", { defaultValue: {} } ),
-
- quality: attr( "string", { defaultValue: "source" } ),
- qualities: fragment( "settingsStreamingQualities", { defaultValue: {} } ),
-
- player: attr( "string", { defaultValue: "default" } ),
- players: fragment( "settingsStreamingPlayers", { defaultValue: {} } ),
-
- low_latency: attr( "boolean", { defaultValue: false } ),
- disable_ads: attr( "boolean", { defaultValue: false } ),
- player_input: attr( "string", { defaultValue: ATTR_STREAMING_PLAYER_INPUT_STDIN } ),
- player_no_close: attr( "boolean", { defaultValue: false } ),
- hls_live_edge: attr( "number", { defaultValue: 3, min: 1, max: 10 } ),
- hls_segment_threads: attr( "number", { defaultValue: 1, min: 1, max: 10 } ),
- retry_open: attr( "number", { defaultValue: 1, min: 1, max: MAX } ),
- retry_streams: attr( "number", { defaultValue: 1, min: 0, max: MAX } ),
-
- providerName: computed( "provider", function() {
- const provider = get( this, "provider" );
- return providers[ provider ][ "name" ];
- }),
-
- providerType: computed( "provider", function() {
- const provider = get( this, "provider" );
- return providers[ provider ][ "type" ];
- }),
-
- isStreamlink: equal( "providerType", "streamlink" )
-
-}).reopenClass({
-
- playerInput: [
+export default class SettingsStreaming extends Fragment {
+ static playerInput = [
{
id: ATTR_STREAMING_PLAYER_INPUT_STDIN,
documentation: null
@@ -66,5 +34,54 @@ export default Fragment.extend({
id: ATTR_STREAMING_PLAYER_INPUT_PASSTHROUGH,
documentation: "--player-passthrough"
}
- ]
-});
+ ];
+
+ @attr( "string", { defaultValue: defaultProvider } )
+ provider;
+ /** @type {SettingsStreamingProviders} */
+ @fragment( "settings-streaming-providers" )
+ providers;
+
+ @attr( "string", { defaultValue: "source" } )
+ quality;
+ /** @type {SettingsStreamingQualities} */
+ @fragment( "settings-streaming-qualities" )
+ qualities;
+
+ @attr( "string", { defaultValue: "default" } )
+ player;
+ /** @type {SettingsStreamingPlayers} */
+ @fragment( "settings-streaming-players" )
+ players;
+
+ @attr( "boolean", { defaultValue: false } )
+ low_latency;
+ @attr( "boolean", { defaultValue: false } )
+ disable_ads;
+ @attr( "string", { defaultValue: ATTR_STREAMING_PLAYER_INPUT_STDIN } )
+ player_input;
+ @attr( "boolean", { defaultValue: false } )
+ player_no_close;
+ @attr( "number", { defaultValue: 3, min: 1, max: 10 } )
+ hls_live_edge;
+ @attr( "number", { defaultValue: 1, min: 1, max: 10 } )
+ hls_segment_threads;
+ @attr( "number", { defaultValue: 1, min: 1, max: MAX } )
+ retry_open;
+ @attr( "number", { defaultValue: 1, min: 0, max: MAX } )
+ retry_streams;
+
+
+ @computed( "provider" )
+ get providerName() {
+ return providers[ this.provider ][ "name" ];
+ }
+
+ @computed( "provider" )
+ get providerType() {
+ return providers[ this.provider ][ "type" ];
+ }
+
+ @equal( "providerType", "streamlink" )
+ isStreamlink;
+}
diff --git a/src/app/data/models/settings/streaming/player/fragment.js b/src/app/data/models/settings/streaming/player/fragment.js
index b501b5df26..a72835941b 100644
--- a/src/app/data/models/settings/streaming/player/fragment.js
+++ b/src/app/data/models/settings/streaming/player/fragment.js
@@ -1,3 +1,4 @@
+import { defineProperty } from "@ember/object";
import attr from "ember-data/attr";
import Fragment from "ember-data-model-fragments/fragment";
import { players as playersConfig } from "config";
@@ -7,18 +8,23 @@ const typeKey = "type";
const players = new Map();
-const SettingsStreamingPlayer = Fragment.extend({
- exec: attr( "string" ),
- args: attr( "string" )
-});
+// the players share a few common attributes
+class SettingsStreamingPlayer extends Fragment {
+ @attr( "string" )
+ exec;
+ @attr( "string" )
+ args;
+}
for ( const [ id, { params } ] of Object.entries( playersConfig ) ) {
- const attributes = {};
+ const player = class extends SettingsStreamingPlayer {};
+
for ( const { name, type, default: defaultValue } of params ) {
- attributes[ name ] = attr( type, { defaultValue } );
+ const prop = attr( type, { defaultValue } );
+ defineProperty( player.prototype, name, prop );
}
- const player = SettingsStreamingPlayer.extend( attributes );
+
players.set( id, player );
}
diff --git a/src/app/data/models/settings/streaming/player/serializer.js b/src/app/data/models/settings/streaming/player/serializer.js
index fc29a93369..3f09bce641 100644
--- a/src/app/data/models/settings/streaming/player/serializer.js
+++ b/src/app/data/models/settings/streaming/player/serializer.js
@@ -1,9 +1,10 @@
-import PolymorphicFragmentSerializer from "data/models/-mixins/polymorphic-fragment-serializer";
+import PolymorphicFragmentSerializer
+ from "data/models/-serializers/polymorphic-fragment-serializer";
import { players, typeKey } from "./fragment";
-export default PolymorphicFragmentSerializer.extend({
- models: players,
- modelBaseName: "settings-streaming-player",
- typeKey
-});
+export default class SettingsStreamingPlayerSerializer extends PolymorphicFragmentSerializer {
+ models = players;
+ modelBaseName = "settings-streaming-player";
+ typeKey = typeKey;
+}
diff --git a/src/app/data/models/settings/streaming/players/fragment.js b/src/app/data/models/settings/streaming/players/fragment.js
index fd33ab2ada..443bf1ba90 100644
--- a/src/app/data/models/settings/streaming/players/fragment.js
+++ b/src/app/data/models/settings/streaming/players/fragment.js
@@ -1,21 +1,28 @@
+import { defineProperty } from "@ember/object";
import Fragment from "ember-data-model-fragments/fragment";
import { fragment } from "ember-data-model-fragments/attributes";
import { players as playersConfig } from "config";
import { typeKey } from "../player/fragment";
-const attributes = {
- "default": fragment( "settings-streaming-player", { defaultValue: {} } )
-};
+class SettingsStreamingPlayers extends Fragment {}
+
+defineProperty(
+ SettingsStreamingPlayers.prototype,
+ "default",
+ fragment( "settings-streaming-player", { defaultValue: {} } )
+);
+
for ( const [ type ] of Object.entries( playersConfig ) ) {
- attributes[ type ] = fragment( "settings-streaming-player", {
+ const prop = fragment( "settings-streaming-player", {
defaultValue: {
[ typeKey ]: `settings-streaming-player-${type}`
},
polymorphic: true,
typeKey
});
+ defineProperty( SettingsStreamingPlayers.prototype, type, prop );
}
-export default Fragment.extend( attributes );
+export default SettingsStreamingPlayers;
diff --git a/src/app/data/models/settings/streaming/provider/fragment.js b/src/app/data/models/settings/streaming/provider/fragment.js
index c5a74c8704..08edb10103 100644
--- a/src/app/data/models/settings/streaming/provider/fragment.js
+++ b/src/app/data/models/settings/streaming/provider/fragment.js
@@ -2,8 +2,11 @@ import attr from "ember-data/attr";
import Fragment from "ember-data-model-fragments/fragment";
-export default Fragment.extend({
- exec: attr( "string" ),
- params: attr( "string" ),
- pythonscript: attr( "string" )
-});
+export default class SettingsStreamingProvider extends Fragment {
+ @attr( "string" )
+ exec;
+ @attr( "string" )
+ params;
+ @attr( "string" )
+ pythonscript;
+}
diff --git a/src/app/data/models/settings/streaming/providers/fragment.js b/src/app/data/models/settings/streaming/providers/fragment.js
index ec14128989..990dae443f 100644
--- a/src/app/data/models/settings/streaming/providers/fragment.js
+++ b/src/app/data/models/settings/streaming/providers/fragment.js
@@ -1,3 +1,4 @@
+import { defineProperty } from "@ember/object";
import Fragment from "ember-data-model-fragments/fragment";
import { fragment } from "ember-data-model-fragments/attributes";
import { streaming as streamingConfig } from "config";
@@ -5,10 +6,12 @@ import { streaming as streamingConfig } from "config";
const { providers } = streamingConfig;
-const attributes = {};
+class SettingsStreamingProviders extends Fragment {}
+
for ( const [ provider ] of Object.entries( providers ) ) {
- attributes[ provider ] = fragment( "settingsStreamingProvider", { defaultValue: {} } );
+ const prop = fragment( "settings-streaming-provider", { defaultValue: {} } );
+ defineProperty( SettingsStreamingProviders.prototype, provider, prop );
}
-export default Fragment.extend( attributes );
+export default SettingsStreamingProviders;
diff --git a/src/app/data/models/settings/streaming/qualities/fragment.js b/src/app/data/models/settings/streaming/qualities/fragment.js
index d25f379fee..e76d15bdf0 100644
--- a/src/app/data/models/settings/streaming/qualities/fragment.js
+++ b/src/app/data/models/settings/streaming/qualities/fragment.js
@@ -1,12 +1,15 @@
+import { defineProperty } from "@ember/object";
import Fragment from "ember-data-model-fragments/fragment";
import { fragment } from "ember-data-model-fragments/attributes";
import { qualities } from "data/models/stream/model";
-const attributes = {};
+class SettingsStreamingQualities extends Fragment {}
+
for ( const { id } of qualities ) {
- attributes[ id ] = fragment( "settingsStreamingQuality", { defaultValue: {} } );
+ const prop = fragment( "settings-streaming-quality", { defaultValue: {} } );
+ defineProperty( SettingsStreamingQualities.prototype, id, prop );
}
-export default Fragment.extend( attributes );
+export default SettingsStreamingQualities;
diff --git a/src/app/data/models/settings/streaming/quality/fragment.js b/src/app/data/models/settings/streaming/quality/fragment.js
index 0e58c3f68e..429af8c05f 100644
--- a/src/app/data/models/settings/streaming/quality/fragment.js
+++ b/src/app/data/models/settings/streaming/quality/fragment.js
@@ -2,7 +2,9 @@ import attr from "ember-data/attr";
import Fragment from "ember-data-model-fragments/fragment";
-export default Fragment.extend({
- quality: attr( "string" ),
- exclude: attr( "string" )
-});
+export default class SettingsStreamingQuality extends Fragment {
+ @attr( "string" )
+ quality;
+ @attr( "string" )
+ exclude;
+}
diff --git a/src/app/data/models/settings/streams/fragment.js b/src/app/data/models/settings/streams/fragment.js
index 6926575911..567e249a88 100644
--- a/src/app/data/models/settings/streams/fragment.js
+++ b/src/app/data/models/settings/streams/fragment.js
@@ -19,58 +19,72 @@ export const ATTR_FILTER_LANGUAGES_NOOP = 0;
export const ATTR_FILTER_LANGUAGES_FADE = 1;
export const ATTR_FILTER_LANGUAGES_FILTER = 2;
-// eslint-disable-next-line max-len
-export const DEFAULT_VODCAST_REGEXP = "\\b(not live|re-?(run|streaming)|(vod-?|re-?broad)cast(ing)?)\\b";
+export const DEFAULT_VODCAST_REGEXP
+ = "\\b(not live|re-?(run|streaming)|(vod-?|re-?broad)cast(ing)?)\\b";
-export default Fragment.extend({
- name: attr( "number", { defaultValue: ATTR_STREAMS_NAME_BOTH } ),
-
- modal_close_end: attr( "boolean", { defaultValue: false } ),
- modal_close_launch: attr( "boolean", { defaultValue: false } ),
-
- chat_open: attr( "boolean", { defaultValue: false } ),
- chat_open_context: attr( "boolean", { defaultValue: false } ),
- twitchemotes: attr( "boolean", { defaultValue: false } ),
-
- filter_vodcast: attr( "boolean", { defaultValue: true } ),
- vodcast_regexp: attr( "string", { defaultValue: "" } ),
-
- filter_languages: attr( "number", { defaultValue: ATTR_FILTER_LANGUAGES_NOOP } ),
- language: attr( "string", { defaultValue: "en" } ),
-
- show_flag: attr( "boolean", { defaultValue: false } ),
- show_info: attr( "boolean", { defaultValue: false } ),
- info: attr( "number", { defaultValue: ATTR_STREAMS_INFO_TITLE } ),
- uptime_hours_only: attr( "boolean", { defaultValue: false } ),
-
- click_middle: attr( "number", { defaultValue: ATTR_STREAMS_CLICK_CHAT } ),
- click_modify: attr( "number", { defaultValue: ATTR_STREAMS_CLICK_SETTINGS } )
-
-}).reopenClass({
-
- contentName: [
+export default class SettingsStreams extends Fragment {
+ static contentName = [
{ id: ATTR_STREAMS_NAME_BOTH, label: "both" },
{ id: ATTR_STREAMS_NAME_CUSTOM, label: "custom" },
{ id: ATTR_STREAMS_NAME_ORIGINAL, label: "original" }
- ],
+ ];
- filterLanguages: [
+ static filterLanguages = [
{ id: ATTR_FILTER_LANGUAGES_NOOP, label: "noop" },
{ id: ATTR_FILTER_LANGUAGES_FADE, label: "fade" },
{ id: ATTR_FILTER_LANGUAGES_FILTER, label: "filter" }
- ],
+ ];
- info: [
+ static info = [
{ id: ATTR_STREAMS_INFO_GAME, label: "game" },
{ id: ATTR_STREAMS_INFO_TITLE, label: "title" }
- ],
+ ];
- click: [
+ static click = [
{ id: ATTR_STREAMS_CLICK_NOOP, label: "noop" },
{ id: ATTR_STREAMS_CLICK_LAUNCH, label: "launch" },
{ id: ATTR_STREAMS_CLICK_CHAT, label: "chat" },
{ id: ATTR_STREAMS_CLICK_CHANNEL, label: "channel" },
{ id: ATTR_STREAMS_CLICK_SETTINGS, label: "settings" }
- ]
-});
+ ];
+
+ @attr( "number", { defaultValue: ATTR_STREAMS_NAME_BOTH } )
+ name;
+
+ @attr( "boolean", { defaultValue: false } )
+ modal_close_end;
+ @attr( "boolean", { defaultValue: false } )
+ modal_close_launch;
+
+ @attr( "boolean", { defaultValue: false } )
+ chat_open;
+ @attr( "boolean", { defaultValue: false } )
+ chat_open_context;
+ @attr( "boolean", { defaultValue: false } )
+ twitchemotes;
+
+ @attr( "boolean", { defaultValue: true } )
+ filter_vodcast;
+ @attr( "string", { defaultValue: "" } )
+ vodcast_regexp;
+
+ @attr( "number", { defaultValue: ATTR_FILTER_LANGUAGES_NOOP } )
+ filter_languages;
+ @attr( "string", { defaultValue: "en" } )
+ languages;
+
+ @attr( "boolean", { defaultValue: false } )
+ show_flag;
+ @attr( "boolean", { defaultValue: false } )
+ show_info;
+ @attr( "number", { defaultValue: ATTR_STREAMS_INFO_TITLE } )
+ info;
+ @attr( "boolean", { defaultValue: false } )
+ uptime_hours_only;
+
+ @attr( "number", { defaultValue: ATTR_STREAMS_CLICK_CHAT } )
+ click_middle;
+ @attr( "number", { defaultValue: ATTR_STREAMS_CLICK_SETTINGS } )
+ click_modify;
+}
diff --git a/src/app/data/models/stream/model.js b/src/app/data/models/stream/model.js
index d692cb0450..ebf2cf0d63 100644
--- a/src/app/data/models/stream/model.js
+++ b/src/app/data/models/stream/model.js
@@ -1,11 +1,13 @@
-import { get, set, computed, observer } from "@ember/object";
+import { set, computed } from "@ember/object";
import { alias } from "@ember/object/computed";
import { inject as service } from "@ember/service";
import attr from "ember-data/attr";
import Model from "ember-data/model";
import { belongsTo } from "ember-data/relationships";
+import { observes } from "@ember-decorators/object";
import { twitch as twitchConfig } from "config";
import qualities from "./-qualities";
+import { name } from "utils/decorators";
const { "stream-url": twitchStreamUrl } = twitchConfig;
@@ -18,33 +20,32 @@ const STATUS_WATCHING = 3;
const STATUS_COMPLETED = 4;
+const { hasOwnProperty } = {};
+
const qualitiesById = qualities.reduce( ( presets, preset ) => {
presets[ preset.id ] = preset;
return presets;
}, {} );
-
-function computedStatus( status ) {
- return computed( "status", {
- get() {
- return get( this, "status" ) === status;
- },
- set( value ) {
- if ( value ) {
- set( this, "status", status );
- return true;
- }
+const computedStatus = status => computed( "status", {
+ get() {
+ return this.status === status;
+ },
+ set( value ) {
+ if ( value ) {
+ set( this, "status", status );
+ return true;
}
- });
-}
+ }
+});
function cpQualityFromPresetOrCustomValue( key ) {
/** @this {Stream} */
- return function() {
+ return computed( "streamQualityPreset", "settings.content.streaming.qualities", function() {
const { id, [ key ]: defaultValue } = this.streamQualityPreset;
const custom = this.settings.content.streaming.qualities.toJSON();
- if ( custom.hasOwnProperty( id ) ) {
+ if ( hasOwnProperty.call( custom, id ) ) {
const customValue = String( custom[ id ][ key ] || "" ).trim();
if ( customValue.length ) {
return customValue;
@@ -52,7 +53,7 @@ function cpQualityFromPresetOrCustomValue( key ) {
}
return defaultValue;
- };
+ });
}
@@ -62,116 +63,121 @@ export {
};
-/**
- * @class Stream
- */
-export default Model.extend({
- /** @property {TwitchStream} stream */
- stream: belongsTo( "twitchStream", { async: false } ),
- /** @property {TwitchChannel} channel */
- channel: belongsTo( "twitchChannel", { async: false } ),
- quality: attr( "string" ),
- low_latency: attr( "boolean" ),
- disable_ads: attr( "boolean" ),
- chat_open: attr( "boolean" ),
- started: attr( "date" ),
+@name( "Stream" )
+export default class Stream extends Model {
+ /** @type {AuthService} */
+ @service auth;
+ /** @type {SettingsService} */
+ @service settings;
+ /** @type {StreamingService} */
+ @service streaming;
+
+ /** @type {TwitchStream} */
+ @belongsTo( "twitch-stream", { async: false } )
+ stream;
+ /** @type {TwitchChannel} */
+ @belongsTo( "twitch-channel", { async: false } )
+ channel;
+ @attr( "string" )
+ quality;
+ @attr( "boolean" )
+ low_latency;
+ @attr( "boolean" )
+ disable_ads;
+ @attr( "boolean" )
+ chat_open;
+ @attr( "date" )
+ started;
// passthrough type (twitch streams are HLS)
- playerInputPassthrough: "hls",
-
- /** @property {String} status */
- status: STATUS_PREPARING,
-
- /** @property {ChildProcess} spawn */
- spawn: null,
+ playerInputPassthrough = "hls";
- /** @property {Error} error */
- error: null,
- warning: false,
+ /** @type {boolean} */
+ strictQuality = false;
- /** @property {Object[]} log */
- log: null,
- showLog: false,
+ /** @type {number} */
+ status = STATUS_PREPARING;
+ /** @type {ChildProcess} */
+ spawn = null;
- auth: service(),
- settings: service(),
- streaming: service(),
+ /** @type {Error} */
+ error = null;
+ warning = false;
+ /** @type {Object[]} */
+ log = null;
+ showLog = false;
- session: alias( "auth.session" ),
+ /** @type {Auth} */
+ @alias( "auth.session" )
+ session;
- isPreparing: computedStatus( STATUS_PREPARING ),
- isAborted: computedStatus( STATUS_ABORTED ),
- isLaunching: computedStatus( STATUS_LAUNCHING ),
- isWatching: computedStatus( STATUS_WATCHING ),
- isCompleted: computedStatus( STATUS_COMPLETED ),
+ @computedStatus( STATUS_PREPARING )
+ isPreparing;
+ @computedStatus( STATUS_ABORTED )
+ isAborted;
+ @computedStatus( STATUS_LAUNCHING )
+ isLaunching;
+ @computedStatus( STATUS_WATCHING )
+ isWatching;
+ @computedStatus( STATUS_COMPLETED )
+ isCompleted;
- hasEnded: computed( "status", "error", function() {
- const status = get( this, "status" );
- const error = get( this, "error" );
+ @computed( "status", "error" )
+ get hasEnded() {
+ return !!this.error
+ || this.status === STATUS_ABORTED
+ || this.status === STATUS_COMPLETED;
+ }
- return !!error || status === STATUS_ABORTED || status === STATUS_COMPLETED;
- }),
+ get customParameters() {
+ const { provider, providers } = this.settings.content.streaming;
- customParameters: computed(function() {
- const provider = get( this, "settings.streaming.provider" );
- const providers = get( this, "settings.streaming.providers" );
-
- return get( providers, `${provider}.params` ) || "";
- }).volatile(),
+ return hasOwnProperty.call( providers, provider )
+ ? providers[ provider ].params || ""
+ : "";
+ }
kill() {
if ( this.spawn ) {
this.spawn.kill( "SIGTERM" );
}
- },
+ }
pushLog( type, line ) {
- get( this, "log" ).pushObject({ type, line });
- },
+ this.log.pushObject({ type, line });
+ }
- qualityObserver: observer( "quality", function() {
+ @observes( "quality" )
+ qualityObserver() {
// the StreamingService knows that it has to spawn a new child process
this.kill();
- }),
+ }
// get the default quality object of the selected quality and streaming provider
- streamQualityPreset: computed( "quality", function() {
- const quality = get( this, "quality" );
-
- return qualitiesById[ quality ]
+ @computed( "quality" )
+ get streamQualityPreset() {
+ return qualitiesById[ this.quality ]
|| qualitiesById[ "source" ];
- }),
+ }
// get the --stream-sorting-excludes parameter value
- streamQualitiesExclude: computed(
- "streamQualityPreset",
- "settings.content.streaming.qualities",
- cpQualityFromPresetOrCustomValue( "exclude" )
- ),
+ @cpQualityFromPresetOrCustomValue( "exclude" )
+ streamQualitiesExclude;
// get the stream quality selection
- streamQuality: computed(
- "streamQualityPreset",
- "settings.content.streaming.qualities",
- cpQualityFromPresetOrCustomValue( "quality" )
- ),
-
- streamUrl: computed( "channel.name", function() {
- const channel = get( this, "channel.name" );
+ @cpQualityFromPresetOrCustomValue( "quality" )
+ streamQuality;
- return twitchStreamUrl.replace( "{channel}", channel );
- })
-
-}).reopenClass({
-
- toString() { return "Stream"; }
-
-});
+ @computed( "channel.name" )
+ get streamUrl() {
+ return twitchStreamUrl.replace( "{channel}", this.channel.name );
+ }
+}
diff --git a/src/app/data/models/twitch/adapter.js b/src/app/data/models/twitch/adapter.js
index b5cd5ae4f6..7302560f87 100644
--- a/src/app/data/models/twitch/adapter.js
+++ b/src/app/data/models/twitch/adapter.js
@@ -1,73 +1,78 @@
-import { get, observer } from "@ember/object";
+import { alias } from "@ember/object/computed";
import { inject as service } from "@ember/service";
-import RESTAdapter from "ember-data/adapters/rest";
import { twitch } from "config";
-import AdapterMixin from "data/models/-mixins/adapter";
+import CustomRESTAdapter from "data/models/-adapters/custom-rest";
+import { urlFragments } from "utils/decorators";
const { oauth: { "client-id": clientId } } = twitch;
-export default RESTAdapter.extend( AdapterMixin, {
- auth: service(),
+@urlFragments({
+ /** @this {TwitchAdapter} */
+ user_id() {
+ const user_id = this.auth.session.user_id;
+ if ( !user_id ) {
+ throw new Error( "Unknown user_id" );
+ }
- host: "https://api.twitch.tv",
- namespace: "",
- headers: {
- "Accept": "application/vnd.twitchtv.v5+json",
- "Client-ID": clientId
+ return user_id;
},
+ /** @this {TwitchAdapter} */
+ user_name() {
+ const user_name = this.auth.session.user_name;
+ if ( !user_name ) {
+ throw new Error( "Unknown user_name" );
+ }
- defaultSerializer: "twitch",
+ return user_name;
+ }
+})
+export default class TwitchAdapter extends CustomRESTAdapter {
+ /** @type {AuthService} */
+ @service auth;
+ defaultSerializer = "twitch";
- urlFragments: {
- user_id() {
- let user_id = get( this, "auth.session.user_id" );
- if ( !user_id ) {
- throw new Error( "Unknown user_id" );
- }
+ host = "https://api.twitch.tv";
+ namespace = "";
- return user_id;
- },
- user_name() {
- let user_name = get( this, "auth.session.user_name" );
- if ( !user_name ) {
- throw new Error( "Unknown user_name" );
- }
- return user_name;
- }
- },
+ static headers = {
+ "Accept": "application/vnd.twitchtv.v5+json",
+ "Client-ID": clientId
+ };
+ static set access_token( value ) {
+ if ( value === null ) {
+ delete this.headers[ "Authorization" ];
+ } else {
+ this.headers[ "Authorization" ] = `OAuth ${value}`;
+ }
+ }
- coalesceFindRequests: false,
+ @alias( "constructor.access_token" )
+ access_token;
+ @alias( "constructor.headers" )
+ headers;
- findManyIdString: null,
- findManyIdSeparator: ",",
+ coalesceFindRequests = false;
- access_token: null,
- tokenObserver: observer( "access_token", function() {
- const token = get( this, "access_token" );
- if ( token === null ) {
- delete this.headers[ "Authorization" ];
- } else {
- this.headers[ "Authorization" ] = `OAuth ${token}`;
- }
- }),
+ findManyIdString = null;
+ findManyIdSeparator = ",";
- createRecordMethod: "PUT",
+ createRecordMethod = "PUT";
createRecordData() {
// we don't need to send any data with the request (yet?)
return {};
- },
+ }
updateRecordData() {
// we don't need to send any data with the request (yet?)
return {};
- },
+ }
findMany( store, type, ids, snapshots ) {
const url = this.buildURL( type, null, snapshots, "findMany" );
@@ -76,7 +81,7 @@ export default RESTAdapter.extend( AdapterMixin, {
};
return this.ajax( url, "GET", { data } );
- },
+ }
groupRecordsForFindMany( store, snapshots ) {
@@ -105,7 +110,7 @@ export default RESTAdapter.extend( AdapterMixin, {
let length = baseLength;
snapshotGroup.forEach( snapshot => {
- const id = get( snapshot, "record.id" );
+ const id = snapshot.record.id;
const idLength = String( id ).length;
const separatorLength = group.length === 0 ? 0 : findManyIdSeparatorLength;
const newLength = length + separatorLength + idLength;
@@ -127,4 +132,4 @@ export default RESTAdapter.extend( AdapterMixin, {
return groups;
}
-});
+}
diff --git a/src/app/data/models/twitch/channel-followed/model.js b/src/app/data/models/twitch/channel-followed/model.js
index 3c30a85123..94627c3166 100644
--- a/src/app/data/models/twitch/channel-followed/model.js
+++ b/src/app/data/models/twitch/channel-followed/model.js
@@ -1,13 +1,16 @@
import attr from "ember-data/attr";
import Model from "ember-data/model";
import { belongsTo } from "ember-data/relationships";
+import { name } from "utils/decorators";
-export default Model.extend({
- channel: belongsTo( "twitchChannel", { async: false } ),
- created_at: attr( "date" ),
- notifications: attr( "boolean" )
-
-}).reopenClass({
- toString() { return "kraken/users/:user_id/follows/channels"; }
-});
+@name( "kraken/users/:user_id/follows/channels" )
+export default class TwitchChannelFollowed extends Model {
+ /** @type {TwitchChannel} */
+ @belongsTo( "twitch-channel", { async: false } )
+ channel;
+ @attr( "date" )
+ created_at;
+ @attr( "boolean" )
+ notifications;
+}
diff --git a/src/app/data/models/twitch/channel-followed/serializer.js b/src/app/data/models/twitch/channel-followed/serializer.js
index a5cbc965f9..d8a43c3934 100644
--- a/src/app/data/models/twitch/channel-followed/serializer.js
+++ b/src/app/data/models/twitch/channel-followed/serializer.js
@@ -1,17 +1,15 @@
import TwitchSerializer from "data/models/twitch/serializer";
-export default TwitchSerializer.extend({
- modelNameFromPayloadKey() {
- return "twitchChannelFollowed";
- },
+export default class TwitchChannelFollowedSerializer extends TwitchSerializer {
+ modelNameFromPayloadKey = () => "twitch-channel-followed";
- attrs: {
+ attrs = {
channel: { deserialize: "records" }
- },
+ };
normalizeArrayResponse( store, primaryModelClass, payload, id, requestType ) {
- const foreignKey = this.store.serializerFor( "twitchChannel" ).primaryKey;
+ const foreignKey = this.store.serializerFor( "twitch-channel" ).primaryKey;
// fix payload format
const follows = ( payload.follows /* istanbul ignore next */ || [] );
@@ -21,11 +19,11 @@ export default TwitchSerializer.extend({
return data;
});
- return this._super( store, primaryModelClass, payload, id, requestType );
- },
+ return super.normalizeArrayResponse( store, primaryModelClass, payload, id, requestType );
+ }
normalizeSingleResponse( store, primaryModelClass, payload, id, requestType ) {
- const foreignKey = this.store.serializerFor( "twitchChannel" ).primaryKey;
+ const foreignKey = this.store.serializerFor( "twitch-channel" ).primaryKey;
// fix payload format
payload[ this.primaryKey ] = payload.channel[ foreignKey ];
@@ -34,6 +32,6 @@ export default TwitchSerializer.extend({
[ this.modelNameFromPayloadKey() ]: payload
};
- return this._super( store, primaryModelClass, payload, id, requestType );
+ return super.normalizeSingleResponse( store, primaryModelClass, payload, id, requestType );
}
-});
+}
diff --git a/src/app/data/models/twitch/channel/model.js b/src/app/data/models/twitch/channel/model.js
index 30a9f10333..fe3093b932 100644
--- a/src/app/data/models/twitch/channel/model.js
+++ b/src/app/data/models/twitch/channel/model.js
@@ -1,4 +1,4 @@
-import { get, computed } from "@ember/object";
+import { computed } from "@ember/object";
import { inject as service } from "@ember/service";
import attr from "ember-data/attr";
import Model from "ember-data/model";
@@ -7,84 +7,111 @@ import {
ATTR_STREAMS_NAME_ORIGINAL,
ATTR_STREAMS_NAME_BOTH
} from "data/models/settings/streams/fragment";
+import { name } from "utils/decorators";
const reLang = /^([a-z]{2})(:?-([a-z]{2}))?$/;
-export default Model.extend({
- i18n: service(),
- settings: service(),
+@name( "kraken/channels" )
+export default class TwitchChannel extends Model {
+ /** @type {I18nService} */
+ @service i18n;
+ /** @type {SettingsService} */
+ @service settings;
+
+
+ @attr( "string" )
+ broadcaster_language;
+ @attr( "date" )
+ created_at;
+ @attr( "string" )
+ display_name;
+ @attr( "number" )
+ followers;
+ @attr( "string" )
+ game;
+ @attr( "string" )
+ language;
+ @attr( "string" )
+ logo;
+ @attr( "boolean" )
+ mature;
+ @attr( "string" )
+ name;
+ @attr( "boolean" )
+ partner;
+ @attr( "string" )
+ profile_banner;
+ @attr( "string" )
+ profile_banner_background_color;
+ @attr( "string" )
+ status;
+ @attr( "date" )
+ updated_at;
+ @attr( "string" )
+ url;
+ @attr( "string" )
+ video_banner;
+ @attr( "number" )
+ views;
- broadcaster_language: attr( "string" ),
- created_at: attr( "date" ),
- display_name: attr( "string" ),
- followers: attr( "number" ),
- game: attr( "string" ),
- language: attr( "string" ),
- logo: attr( "string" ),
- mature: attr( "boolean" ),
- name: attr( "string" ),
- partner: attr( "boolean" ),
- profile_banner: attr( "string" ),
- profile_banner_background_color: attr( "string" ),
- status: attr( "string" ),
- updated_at: attr( "date" ),
- url: attr( "string" ),
- video_banner: attr( "string" ),
- views: attr( "number" ),
+ /** @type {(TwitchSubscription|boolean)} subscribed */
+ subscribed = null;
+ /** @type {(TwitchChannelFollowed|boolean)} following */
+ followed = null;
- hasCustomDisplayName: computed( "name", "display_name", function() {
- return get( this, "name" ).toLowerCase() !== get( this, "display_name" ).toLowerCase();
- }),
- detailedName: computed(
+ @computed( "name", "display_name" )
+ get hasCustomDisplayName() {
+ return this.name.toLowerCase() !== this.display_name.toLowerCase();
+ }
+
+ @computed(
"name",
"display_name",
"hasCustomDisplayName",
- "settings.streams.name",
- function() {
- switch ( get( this, "settings.streams.name" ) ) {
- case ATTR_STREAMS_NAME_CUSTOM:
- return get( this, "display_name" );
- case ATTR_STREAMS_NAME_ORIGINAL:
- return get( this, "name" );
- case ATTR_STREAMS_NAME_BOTH:
- return get( this, "hasCustomDisplayName" )
- ? `${get( this, "display_name" )} (${get( this, "name" )})`
- : get( this, "display_name" );
- }
+ "settings.content.streams.name"
+ )
+ get detailedName() {
+ switch ( this.settings.content.streams.name ) {
+ case ATTR_STREAMS_NAME_ORIGINAL:
+ return this.name;
+ case ATTR_STREAMS_NAME_BOTH:
+ return this.hasCustomDisplayName
+ ? `${this.display_name} (${this.name})`
+ : this.display_name;
+ case ATTR_STREAMS_NAME_CUSTOM:
+ default:
+ return this.display_name;
}
- ),
-
-
- titleFollowers: computed( "i18n.locale", "followers", function() {
- const i18n = get( this, "i18n" );
- const count = get( this, "followers" );
+ }
- return i18n.t( "models.twitch.channel.followers", { count } );
- }),
- titleViews: computed( "i18n.locale", "views", function() {
- const i18n = get( this, "i18n" );
- const count = get( this, "views" );
+ @computed( "i18n.locale", "followers" )
+ get titleFollowers() {
+ return this.i18n.t( "models.twitch.channel.followers", { count: this.followers } );
+ }
- return i18n.t( "models.twitch.channel.views", { count } );
- }),
+ @computed( "i18n.locale", "views" )
+ get titleViews() {
+ return this.i18n.t( "models.twitch.channel.views", { count: this.views } );
+ }
- hasLanguage: computed( "language", function() {
- const lang = get( this, "language" );
+ @computed( "language" )
+ get hasLanguage() {
+ const language = this.language;
- return !!lang && lang !== "other";
- }),
+ return !!language && language !== "other";
+ }
- hasBroadcasterLanguage: computed( "broadcaster_language", "language", function() {
- const broadcaster = get( this, "broadcaster_language" );
- const language = get( this, "language" );
- const mBroadcaster = reLang.exec( broadcaster );
+ @computed( "broadcaster_language", "language" )
+ get hasBroadcasterLanguage() {
+ const { broadcaster_language, language } = this;
+ const mBroadcaster = reLang.exec( broadcaster_language );
const mLanguage = reLang.exec( language );
// show the broadcaster_language only if it is set and
@@ -92,28 +119,21 @@ export default Model.extend({
// 2. the language is different from the broadcaster_language
// WITHOUT comparing both lang variants
return !!mBroadcaster && ( !mLanguage || mLanguage[1] !== mBroadcaster[1] );
- }),
-
-
- /** @type {(TwitchSubscription|boolean)} subscribed */
- subscribed: null,
-
- /** @type {(TwitchChannelFollowed|boolean)} following */
- followed: null,
+ }
/**
* Load channel specific settings
- * @returns {Promise
{{/if}}
diff --git a/src/app/ui/components/settings-submit/component.js b/src/app/ui/components/settings-submit/component.js
index 7409279943..fcf7536bb1 100644
--- a/src/app/ui/components/settings-submit/component.js
+++ b/src/app/ui/components/settings-submit/component.js
@@ -1,61 +1,64 @@
import Component from "@ember/component";
-import { get, set, computed, observer } from "@ember/object";
-import { on } from "@ember/object/evented";
+import { set } from "@ember/object";
+import { className, classNames, layout } from "@ember-decorators/component";
+import { observes, on } from "@ember-decorators/object";
import { cancel, later } from "@ember/runloop";
-import layout from "./template.hbs";
+import template from "./template.hbs";
import "./styles.less";
-export default Component.extend({
- layout,
+@layout( template )
+@classNames( "settings-submit-component" )
+export default class SettingsSubmitComponent extends Component {
+ isDirty = true;
+ disabled = false;
- classNames: [ "settings-submit-component" ],
- classNameBindings: [ "_enabled::faded" ],
+ delay = 1000;
- isDirty: true,
- disabled: false,
+ @className( "", "faded" )
+ _enabled = false;
- delay: 1000,
+ apply() {}
+ discard() {}
- apply() {},
- discard() {},
-
- _enabled: computed(function() {
- return get( this, "isDirty" )
- && !get( this, "disabled" );
- }),
+ @on( "init" )
+ _onInit() {
+ set( this, "_enabled", this.isDirty && !this.disabled );
+ }
// immediately set enabled when disabled property changes
- _disabledObserver: observer( "disabled", function() {
- let enabled = get( this, "disabled" )
+ @observes( "disabled" )
+ _disabledObserver() {
+ const enabled = this.disabled
? false
- : get( this, "isDirty" );
+ : this.isDirty;
set( this, "_enabled", enabled );
- }),
+ }
// isDirty === true: immediately set enabled to true
// isDirty === false: wait and then set to false
- _timeout: null,
- _isDirtyObserver: observer( "isDirty", function() {
- if ( get( this, "disabled" ) ) { return; }
+ _timeout = null;
+ @observes( "isDirty" )
+ _isDirtyObserver() {
+ if ( this.disabled ) { return; }
this._clearTimeout();
- if ( get( this, "isDirty" ) ) {
+ if ( this.isDirty ) {
set( this, "_enabled", true );
} else {
- let delay = get( this, "delay" );
this._timeout = later( () => {
set( this, "_enabled", false );
this._timeout = null;
- }, delay );
+ }, this.delay );
}
- }),
+ }
- _clearTimeout: on( "willDestroyElement", function() {
+ @on( "willDestroyElement" )
+ _clearTimeout() {
if ( this._timeout ) {
cancel( this._timeout );
this._timeout = null;
}
- })
-});
+ }
+}
diff --git a/src/app/ui/components/sub-menu/component.js b/src/app/ui/components/sub-menu/component.js
index 4fda08b6fe..d4d7fb1882 100644
--- a/src/app/ui/components/sub-menu/component.js
+++ b/src/app/ui/components/sub-menu/component.js
@@ -1,22 +1,15 @@
import Component from "@ember/component";
-import layout from "./template.hbs";
+import { classNames, layout, tagName } from "@ember-decorators/component";
+import template from "./template.hbs";
import "./styles.less";
-export default Component.extend({
- layout,
+@layout( template )
+@tagName( "ul" )
+@classNames( "sub-menu-component" )
+export default class SubMenuComponent extends Component {
+ static positionalParams = [ "baseroute", "menus" ];
- tagName: "ul",
- classNames: [
- "sub-menu-component"
- ],
-
- baseroute: null,
- menus: null
-
-}).reopenClass({
- positionalParams: [
- "baseroute",
- "menus"
- ]
-});
+ baseroute = null;
+ menus = null;
+}
diff --git a/src/app/ui/components/title-bar/component.js b/src/app/ui/components/title-bar/component.js
index 2ab1cae51b..53b3219d05 100644
--- a/src/app/ui/components/title-bar/component.js
+++ b/src/app/ui/components/title-bar/component.js
@@ -1,42 +1,48 @@
import Component from "@ember/component";
-import { get } from "@ember/object";
+import { action } from "@ember/object";
import { inject as service } from "@ember/service";
+import { classNames, layout, tagName } from "@ember-decorators/component";
import { main as mainConfig } from "config";
import { isDebug } from "nwjs/debug";
-import layout from "./template.hbs";
+import template from "./template.hbs";
import "./styles.less";
const { "display-name": displayName } = mainConfig;
-export default Component.extend({
- auth: service(),
- notification: service(),
- nwjs: service(),
+@layout( template )
+@tagName( "header" )
+@classNames( "title-bar-component" )
+export default class TitleBarComponent extends Component {
+ /** @type {AuthService} */
+ @service auth;
+ /** @type {NotificationService} */
+ @service notification;
+ /** @type {NwjsService} */
+ @service nwjs;
/** @type {RouterService} */
- router: service(),
- settings: service(),
- streaming: service(),
-
- layout,
- classNames: [ "title-bar-component" ],
- tagName: "header",
-
- displayName,
- isDebug,
-
- actions: {
- goto() {
- this.router.transitionTo( ...arguments );
- },
+ @service router;
+ /** @type {SettingsService} */
+ @service settings;
+ /** @type {StreamingService} */
+ @service streaming;
+
+ displayName = displayName;
+ isDebug = isDebug;
+
+ @action
+ goto() {
+ this.router.transitionTo( ...arguments );
+ }
- homepage() {
- this.router.homepage();
- },
+ @action
+ homepage() {
+ this.router.homepage();
+ }
- nwjs( method ) {
- get( this, "nwjs" )[ method ]();
- }
+ @action
+ nwjsAction( method ) {
+ this.nwjs[ method ]();
}
-});
+}
diff --git a/src/app/ui/components/title-bar/template.hbs b/src/app/ui/components/title-bar/template.hbs
index 0c137e895b..a89ef03f46 100644
--- a/src/app/ui/components/title-bar/template.hbs
+++ b/src/app/ui/components/title-bar/template.hbs
@@ -21,13 +21,13 @@
\ No newline at end of file
diff --git a/src/app/ui/routes/about/controller.js b/src/app/ui/routes/about/controller.js
index 8bc6f1800a..8fd5b14278 100644
--- a/src/app/ui/routes/about/controller.js
+++ b/src/app/ui/routes/about/controller.js
@@ -6,10 +6,10 @@ import { arch } from "utils/node/platform";
import "./styles.less";
-export default Controller.extend({
- mainConfig,
- localesConfig,
- metadata,
- arch,
- releaseUrl: mainConfig.urls.release.replace( "{version}", manifest.version )
-});
+export default class AboutController extends Controller {
+ mainConfig = mainConfig;
+ localesConfig = localesConfig;
+ metadata = metadata;
+ arch = arch;
+ releaseUrl = mainConfig.urls.release.replace( "{version}", manifest.version );
+}
diff --git a/src/app/ui/routes/application/route.js b/src/app/ui/routes/application/route.js
index f2fc87a3bc..cb643cd6ce 100644
--- a/src/app/ui/routes/application/route.js
+++ b/src/app/ui/routes/application/route.js
@@ -2,15 +2,15 @@ import Route from "@ember/routing/route";
import { inject as service } from "@ember/service";
-export default Route.extend({
+export default class ApplicationRoute extends Route {
/** @type {AuthService} */
- auth: service(),
+ @service auth;
/** @type {VersioncheckService} */
- versioncheck: service(),
+ @service versioncheck;
init() {
- this._super( ...arguments );
+ super.init( ...arguments );
this.auth.autoLogin();
this.versioncheck.check();
}
-});
+}
diff --git a/src/app/ui/routes/channel/controller.js b/src/app/ui/routes/channel/controller.js
index 4bfcaf4e39..887eb40196 100644
--- a/src/app/ui/routes/channel/controller.js
+++ b/src/app/ui/routes/channel/controller.js
@@ -3,7 +3,9 @@ import { alias } from "@ember/object/computed";
import "./styles.less";
-export default Controller.extend({
- stream : alias( "model.stream" ),
- channel: alias( "model.channel" )
-});
+export default class ChannelController extends Controller {
+ @alias( "model.stream" )
+ stream;
+ @alias( "model.channel" )
+ channel;
+}
diff --git a/src/app/ui/routes/channel/index/controller.js b/src/app/ui/routes/channel/index/controller.js
index cb83cbbc94..ce071374a2 100644
--- a/src/app/ui/routes/channel/index/controller.js
+++ b/src/app/ui/routes/channel/index/controller.js
@@ -1,30 +1,31 @@
import Controller from "@ember/controller";
-import { get, computed } from "@ember/object";
+import { computed } from "@ember/object";
import { alias } from "@ember/object/computed";
import { inject as service } from "@ember/service";
-const day = 24 * 3600 * 1000;
+export default class UserIndexController extends Controller {
+ /** @type {I18nService} */
+ @service i18n;
+ @alias( "model.stream" )
+ stream;
+ @alias( "model.channel" )
+ channel;
-export default Controller.extend({
- i18n: service(),
+ @computed( "channel.created_at" )
+ get age() {
+ const createdAt = this.channel.created_at;
- stream: alias( "model.stream" ),
- channel: alias( "model.channel" ),
+ return ( new Date() - createdAt ) / ( 24 * 3600 * 1000 );
+ }
- age: computed( "channel.created_at", function() {
- const createdAt = get( this, "channel.created_at" );
+ @computed( "i18n.locale", "channel.broadcaster_language" )
+ get language() {
+ const blang = this.channel.broadcaster_language;
- return ( new Date() - createdAt ) / day;
- }),
-
- language: computed( "i18n.locale", "channel.broadcaster_language", function() {
- const i18n = get( this, "i18n" );
- const blang = get( this, "channel.broadcaster_language" );
-
- return blang && i18n.exists( `languages.${blang}` )
- ? i18n.t( `languages.${blang}` ).toString()
+ return blang && this.i18n.exists( `languages.${blang}` )
+ ? this.i18n.t( `languages.${blang}` ).toString()
: "";
- })
-});
+ }
+}
diff --git a/src/app/ui/routes/channel/index/route.js b/src/app/ui/routes/channel/index/route.js
index f4ba7f0328..e962d7f7ff 100644
--- a/src/app/ui/routes/channel/index/route.js
+++ b/src/app/ui/routes/channel/index/route.js
@@ -2,14 +2,14 @@ import { getOwner } from "@ember/application";
import Route from "@ember/routing/route";
-export default Route.extend({
+export default class ChannelIndexRoute extends Route {
async model() {
const { stream, channel } = this.modelFor( "channel" );
return { stream, channel };
- },
+ }
refresh() {
return getOwner( this ).lookup( "route:channel" ).refresh();
}
-});
+}
diff --git a/src/app/ui/routes/channel/route.js b/src/app/ui/routes/channel/route.js
index 30dffedde4..c2e4606521 100644
--- a/src/app/ui/routes/channel/route.js
+++ b/src/app/ui/routes/channel/route.js
@@ -1,33 +1,32 @@
-import { get } from "@ember/object";
import Route from "@ember/routing/route";
import RefreshRouteMixin from "ui/routes/-mixins/routes/refresh";
import preload from "utils/preload";
-const reNum = /^\d+$/;
-
-
-export default Route.extend( RefreshRouteMixin, {
- async model( params ) {
- const store = get( this, "store" );
- let { channel: id } = params;
+export default class ChannelRoute extends Route.extend( RefreshRouteMixin ) {
+ async model({ channel: id }) {
+ /** @type {TwitchStream} */
let stream;
+ /** @type {TwitchChannel} */
let channel;
- if ( !reNum.test( id ) ) {
- const user = await store.findRecord( "twitchUser", id );
+ if ( !/^\d+$/.test( id ) ) {
+ /** @type {TwitchUser} */
+ const user = await this.store.queryRecord( "twitch-user", id );
try {
- stream = await get( user, "stream" );
- } catch ( e ) {}
- channel = await get( user, "channel" );
+ stream = await user.loadStream();
+ channel = stream.channel;
+ } catch ( e ) {
+ channel = await user.loadChannel();
+ }
} else {
try {
- stream = await store.findRecord( "twitchStream", id, { reload: true } );
- channel = get( stream, "channel" );
+ stream = await this.store.findRecord( "twitch-stream", id, { reload: true } );
+ channel = stream.channel;
} catch ( e ) {
// if the channel is not online, just find and return the channel record
- channel = await store.findRecord( "twitchChannel", id, { reload: true } );
+ channel = await this.store.findRecord( "twitch-channel", id, { reload: true } );
}
}
@@ -40,4 +39,4 @@ export default Route.extend( RefreshRouteMixin, {
return model;
}
-});
+}
diff --git a/src/app/ui/routes/channel/settings/controller.js b/src/app/ui/routes/channel/settings/controller.js
index fa912adf70..c083188a16 100644
--- a/src/app/ui/routes/channel/settings/controller.js
+++ b/src/app/ui/routes/channel/settings/controller.js
@@ -1,20 +1,26 @@
import Controller from "@ember/controller";
-import { get, set, defineProperty, computed, observer } from "@ember/object";
+import { get, set, defineProperty, computed, action } from "@ember/object";
import { inject as service } from "@ember/service";
+import { observes } from "@ember-decorators/object";
import { qualities } from "data/models/stream/model";
import RetryTransitionMixin from "ui/routes/-mixins/controllers/retry-transition";
-export default Controller.extend( RetryTransitionMixin, {
- modal: service(),
- settings: service(),
+export default class ChannelSettingsController extends Controller.extend( RetryTransitionMixin ) {
+ /** @type {ModalService} */
+ @service modal;
+ /** @type {SettingsService} */
+ @service settings;
- qualities,
+ qualities = qualities;
- modelObserver: observer( "model", function() {
- const original = get( this, "model.model" );
- const model = get( this, "model.buffer" );
- const settings = get( this, "settings" );
+ /** @type {{model: ChannelSettings, buffer: ObjectBuffer}} */
+ model;
+
+ @observes( "model" )
+ modelObserver() {
+ const { model: original, buffer: model } = this.model;
+ const settings = this.settings;
original.eachAttribute( ( attr, meta ) => {
const {
@@ -71,7 +77,7 @@ export default Controller.extend( RetryTransitionMixin, {
defineProperty( this, attr, attributeProxy );
defineProperty( this, `_${attr}`, attributeEnabled );
});
- }),
+ }
/**
* Prevent pollution:
@@ -111,35 +117,36 @@ export default Controller.extend( RetryTransitionMixin, {
record.transitionTo( "loaded.created.uncommitted" );
});
}
- },
-
- actions: {
- apply( success, failure ) {
- const modal = get( this, "modal" );
- const model = get( this, "model.model" );
- const buffer = get( this, "model.buffer" ).applyChanges().getContent();
-
- this.saveRecord( model, buffer )
- .then( success, failure )
- .then( () => modal.closeModal( this ) )
- .then( () => this.retryTransition() )
- .catch( () => model.rollbackAttributes() );
- },
-
- discard( success ) {
- const modal = get( this, "modal" );
-
- get( this, "model.buffer" ).discardChanges();
-
- Promise.resolve()
- .then( success )
- .then( () => modal.closeModal( this ) )
- .then( () => this.retryTransition() );
- },
-
- cancel() {
- set( this, "previousTransition", null );
- get( this, "modal" ).closeModal( this );
+ }
+
+ @action
+ async apply( success, failure ) {
+ const model = this.model.model;
+ const buffer = this.model.buffer.applyChanges().getContent();
+
+ try {
+ await this.saveRecord( model, buffer );
+ await success();
+ this.modal.closeModal( this );
+ this.retryTransition();
+
+ } catch ( e ) {
+ await failure();
+ model.rollbackAttributes();
}
}
-});
+
+ @action
+ async discard( success ) {
+ this.model.buffer.discardChanges();
+ await success();
+ this.modal.closeModal( this );
+ this.retryTransition();
+ }
+
+ @action
+ cancel() {
+ set( this, "previousTransition", null );
+ this.modal.closeModal( this );
+ }
+}
diff --git a/src/app/ui/routes/channel/settings/route.js b/src/app/ui/routes/channel/settings/route.js
index 89800ca3e7..3e1f3e37d3 100644
--- a/src/app/ui/routes/channel/settings/route.js
+++ b/src/app/ui/routes/channel/settings/route.js
@@ -1,22 +1,23 @@
import { getOwner } from "@ember/application";
-import { get } from "@ember/object";
+import { action } from "@ember/object";
import Route from "@ember/routing/route";
import { inject as service } from "@ember/service";
import ObjectBuffer from "utils/ember/ObjectBuffer";
-export default Route.extend({
- modal: service(),
+export default class ChannelSettingsRoute extends Route {
+ /** @type {ModalService} */
+ @service modal;
async model() {
- const store = get( this, "store" );
+ const store = this.store;
const parentModel = this.modelFor( "channel" );
- const id = get( parentModel, "channel.name" );
+ const id = parentModel.channel.name;
- const model = await store.findRecord( "channelSettings", id )
+ const model = await store.findRecord( "channel-settings", id )
.catch( () => {
// get the record automatically created by store.findRecord()
- const model = store.recordForId( "channelSettings", id );
+ const model = store.recordForId( "channel-settings", id );
// transition from `root.empty` to `root.loaded.created.uncommitted`
model.transitionTo( "loaded.created.uncommitted" );
@@ -28,29 +29,28 @@ export default Route.extend({
const buffer = ObjectBuffer.create({ content });
return { model, buffer };
- },
+ }
refresh() {
return getOwner( this ).lookup( "route:channel" ).refresh();
- },
+ }
- actions: {
- willTransition( previousTransition ) {
- const controller = get( this, "controller" );
+ @action
+ willTransition( previousTransition ) {
+ const controller = this.controller;
- // check whether the user has changed any values
- if ( !get( controller, "model.buffer.isDirty" ) ) {
- // don't keep the channelSettings records in cache
- return get( this, "store" ).unloadAll( "channelSettings" );
- }
+ // check whether the user has changed any values
+ if ( !controller.model.buffer.isDirty ) {
+ // don't keep the channelSettings records in cache
+ return this.store.unloadAll( "channel-settings" );
+ }
- // stay here...
- previousTransition.abort();
+ // stay here...
+ previousTransition.abort();
- // and let the user decide
- get( this, "modal" ).openModal( "confirm", controller, {
- previousTransition
- });
- }
+ // and let the user decide
+ this.modal.openModal( "confirm", controller, {
+ previousTransition
+ });
}
-});
+}
diff --git a/src/app/ui/routes/channel/teams/route.js b/src/app/ui/routes/channel/teams/route.js
index cba429c335..9cecbe60be 100644
--- a/src/app/ui/routes/channel/teams/route.js
+++ b/src/app/ui/routes/channel/teams/route.js
@@ -1,17 +1,16 @@
-import { get } from "@ember/object";
import Route from "@ember/routing/route";
import InfiniteScrollOffsetMixin from "ui/routes/-mixins/routes/infinite-scroll/offset";
-export default Route.extend( InfiniteScrollOffsetMixin, {
- itemSelector: ".team-item-component",
- modelName: "twitchTeam",
- modelPreload: "logo",
+export default class ChannelTeamsRoute extends Route.extend( InfiniteScrollOffsetMixin ) {
+ itemSelector = ".team-item-component";
+ modelName = "twitch-team";
+ modelPreload = "logo";
model() {
const { channel: parentModel } = this.modelFor( "channel" );
- const channel = get( parentModel, "id" );
+ const channel = parentModel.id;
- return this._super({ channel });
+ return super.model({ channel });
}
-});
+}
diff --git a/src/app/ui/routes/error/route.js b/src/app/ui/routes/error/route.js
index 0c5b4f716f..1fe7013042 100644
--- a/src/app/ui/routes/error/route.js
+++ b/src/app/ui/routes/error/route.js
@@ -1,3 +1,4 @@
+/* globals DEBUG */
import { get, set, getProperties } from "@ember/object";
import Route from "@ember/routing/route";
import { inject as service } from "@ember/service";
@@ -25,8 +26,9 @@ const duplicates = {
};
-export default Route.extend({
- router: service(),
+export default class ErrorRoute extends Route {
+ /** @type {RouterService} */
+ @service router;
/**
* Do all the error display stuff here instead of using an error controller.
@@ -35,7 +37,7 @@ export default Route.extend({
* @param {Error} error
*/
setupController( controller, error ) {
- this._super( controller );
+ super.setupController( controller );
error = error || new Error( "Unknown error" );
@@ -100,4 +102,4 @@ export default Route.extend({
: () => getProperties( trans.to, "name", "params" )
);
}
-});
+}
diff --git a/src/app/ui/routes/featured/controller.js b/src/app/ui/routes/featured/controller.js
index 34d9130dcb..a3fe92b922 100644
--- a/src/app/ui/routes/featured/controller.js
+++ b/src/app/ui/routes/featured/controller.js
@@ -1,24 +1,25 @@
import Controller from "@ember/controller";
-import { get, set } from "@ember/object";
+import { set, action } from "@ember/object";
import { alias } from "@ember/object/computed";
import "./styles.less";
-export default Controller.extend({
- summary : alias( "model.summary" ),
- featured: alias( "model.featured" ),
+export default class FeaturedController extends Controller {
+ @alias( "model.summary" )
+ summary;
+ @alias( "model.featured" )
+ featured;
- isAnimated: false,
+ isAnimated = false;
// reference the active stream by id
// so we can safely go back to the route
- _index: 0,
+ _index = 0;
- actions: {
- switchFeatured( index ) {
- if ( index === get( this, "_index" ) ) { return; }
- set( this, "_index", index );
- set( this, "isAnimated", true );
- }
+ @action
+ switchFeatured( index ) {
+ if ( index === this._index ) { return; }
+ set( this, "_index", index );
+ set( this, "isAnimated", true );
}
-});
+}
diff --git a/src/app/ui/routes/featured/route.js b/src/app/ui/routes/featured/route.js
index 42e30188d8..30433adf04 100644
--- a/src/app/ui/routes/featured/route.js
+++ b/src/app/ui/routes/featured/route.js
@@ -1,16 +1,16 @@
-import { get, set } from "@ember/object";
+import { set } from "@ember/object";
import Route from "@ember/routing/route";
import RefreshRouteMixin from "ui/routes/-mixins/routes/refresh";
import preload from "utils/preload";
-export default Route.extend( RefreshRouteMixin, {
+export default class FeaturedRoute extends Route.extend( RefreshRouteMixin ) {
async model() {
- const store = get( this, "store" );
+ const store = this.store;
const [ summary, featured ] = await Promise.all([
- store.queryRecord( "twitchStreamSummary", {} ),
- store.query( "twitchStreamFeatured", {
+ store.queryRecord( "twitch-stream-summary", {} ),
+ store.query( "twitch-stream-featured", {
offset: 0,
limit : 5
})
@@ -20,11 +20,11 @@ export default Route.extend( RefreshRouteMixin, {
]);
return { summary, featured };
- },
+ }
resetController( controller, isExiting ) {
if ( isExiting ) {
set( controller, "isAnimated", false );
}
}
-});
+}
diff --git a/src/app/ui/routes/games/game/route.js b/src/app/ui/routes/games/game/route.js
index 64ab2b4d5a..0562fab43d 100644
--- a/src/app/ui/routes/games/game/route.js
+++ b/src/app/ui/routes/games/game/route.js
@@ -1,30 +1,31 @@
-import { get, set } from "@ember/object";
+import { set } from "@ember/object";
import Route from "@ember/routing/route";
import InfiniteScrollOffsetMixin from "ui/routes/-mixins/routes/infinite-scroll/offset";
import FilterLanguagesMixin from "ui/routes/-mixins/routes/filter-languages";
import RefreshRouteMixin from "ui/routes/-mixins/routes/refresh";
-export default Route.extend( InfiniteScrollOffsetMixin, FilterLanguagesMixin, RefreshRouteMixin, {
- itemSelector: ".stream-item-component",
- modelName: "twitchStream",
- modelPreload: "preview.mediumLatest",
+export default class GamesGamesRoute
+extends Route.extend( InfiniteScrollOffsetMixin, FilterLanguagesMixin, RefreshRouteMixin ) {
+ itemSelector = ".stream-item-component";
+ modelName = "twitch-stream";
+ modelPreload = "preview.mediumLatest";
async model({ game }) {
- const model = await this._super({ game });
+ const model = await super.model({ game });
return { game, model };
- },
+ }
async fetchContent() {
- const game = get( this.controller, "game" );
+ const game = this.controller.game;
const { model } = await this.model({ game });
return model;
- },
+ }
setupController( controller, { game, model }, ...args ) {
- this._super( controller, model, ...args );
+ super.setupController( controller, model, ...args );
set( controller, "game", game );
}
-});
+}
diff --git a/src/app/ui/routes/games/index/route.js b/src/app/ui/routes/games/index/route.js
index 1fee96f82e..28b29512f8 100644
--- a/src/app/ui/routes/games/index/route.js
+++ b/src/app/ui/routes/games/index/route.js
@@ -3,8 +3,9 @@ import InfiniteScrollOffsetMixin from "ui/routes/-mixins/routes/infinite-scroll/
import RefreshRouteMixin from "ui/routes/-mixins/routes/refresh";
-export default Route.extend( InfiniteScrollOffsetMixin, RefreshRouteMixin, {
- itemSelector: ".game-item-component",
- modelName: "twitchGameTop",
- modelPreload: "game.box.large"
-});
+export default class GamesIndexRoute
+extends Route.extend( InfiniteScrollOffsetMixin, RefreshRouteMixin ) {
+ itemSelector = ".game-item-component";
+ modelName = "twitch-game-top";
+ modelPreload = "game.box.large";
+}
diff --git a/src/app/ui/routes/index/route.js b/src/app/ui/routes/index/route.js
index 576beb6ece..fa30de7adf 100644
--- a/src/app/ui/routes/index/route.js
+++ b/src/app/ui/routes/index/route.js
@@ -1,7 +1,7 @@
import Route from "@ember/routing/route";
-export default Route.extend({
+export default class IndexRoute extends Route {
beforeModel( transition ) {
// access to this route is restricted
// but don't block the initial transition
@@ -9,4 +9,4 @@ export default Route.extend({
transition.abort();
}
}
-});
+}
diff --git a/src/app/ui/routes/loading/route.js b/src/app/ui/routes/loading/route.js
index 7138c5c16a..86d1e3b5f6 100644
--- a/src/app/ui/routes/loading/route.js
+++ b/src/app/ui/routes/loading/route.js
@@ -1,7 +1,7 @@
import Route from "@ember/routing/route";
-export default Route.extend({
+export default class LoadingRoute extends Route {
// override automatically generated templateName for all routes which import LoadingRoute
- templateName: "loading"
-});
+ templateName = "loading";
+}
diff --git a/src/app/ui/routes/search/controller.js b/src/app/ui/routes/search/controller.js
index ee94488e17..436772b504 100644
--- a/src/app/ui/routes/search/controller.js
+++ b/src/app/ui/routes/search/controller.js
@@ -1,25 +1,31 @@
import Controller from "@ember/controller";
-import { get, computed } from "@ember/object";
+import { computed } from "@ember/object";
import { alias, empty, equal } from "@ember/object/computed";
import "./styles.less";
-export default Controller.extend({
- queryParams: [ "filter", "query" ],
+export default class SearchController extends Controller {
+ queryParams = [ "filter", "query" ];
- games : alias( "model.games" ),
- streams : alias( "model.streams" ),
- channels: alias( "model.channels" ),
+ @alias( "model.games" )
+ games;
+ @alias( "model.streams" )
+ streams;
+ @alias( "model.channels" )
+ channels;
- notFiltered: equal( "filter", "all" ),
+ @equal( "filter", "all" )
+ notFiltered;
- emptyGames : empty( "games" ),
- emptyStreams : empty( "streams" ),
- emptyChannels: empty( "channels" ),
+ @empty( "games" )
+ emptyGames;
+ @empty( "streams" )
+ emptyStreams;
+ @empty( "channels" )
+ emptyChannels;
- noResults: computed( "emptyGames", "emptyStreams", "emptyChannels", function() {
- return get( this, "emptyGames" )
- && get( this, "emptyStreams" )
- && get( this, "emptyChannels" );
- })
-});
+ @computed( "emptyGames", "emptyStreams", "emptyChannels" )
+ get noResults() {
+ return this.emptyGames && this.emptyStreams && this.emptyChannels;
+ }
+}
diff --git a/src/app/ui/routes/search/route.js b/src/app/ui/routes/search/route.js
index 4aa54c2db9..1685c1d1e5 100644
--- a/src/app/ui/routes/search/route.js
+++ b/src/app/ui/routes/search/route.js
@@ -1,4 +1,4 @@
-import { get, setProperties } from "@ember/object";
+import { setProperties } from "@ember/object";
import Route from "@ember/routing/route";
import InfiniteScrollMixin from "ui/routes/-mixins/routes/infinite-scroll";
import RefreshRouteMixin from "ui/routes/-mixins/routes/refresh";
@@ -32,8 +32,8 @@ const fetchMethods = {
};
-export default Route.extend( InfiniteScrollMixin, RefreshRouteMixin, {
- queryParams: {
+export default class SearchRoute extends Route.extend( InfiniteScrollMixin, RefreshRouteMixin ) {
+ queryParams = {
filter: {
refreshModel: true,
replace: false
@@ -42,7 +42,11 @@ export default Route.extend( InfiniteScrollMixin, RefreshRouteMixin, {
refreshModel: true,
replace: false
}
- },
+ };
+
+ contentPath;
+ itemSelector;
+ fetchMethod;
beforeModel() {
@@ -55,8 +59,8 @@ export default Route.extend( InfiniteScrollMixin, RefreshRouteMixin, {
fetchMethod: fetchMethods[ filter ]
});
- return this._super( ...arguments );
- },
+ return super.beforeModel( ...arguments );
+ }
async model( params ) {
const [ games, channels, streams ] = await Promise.all([
@@ -66,59 +70,70 @@ export default Route.extend( InfiniteScrollMixin, RefreshRouteMixin, {
]);
return { games, channels, streams };
- },
+ }
fetchContent() {
- const fetchMethod = get( this, "fetchMethod" );
- const filter = get( this, "controller.filter" );
- const query = get( this, "controller.query" );
+ const fetchMethod = this.fetchMethod;
+ const { filter, query } = this.controller;
return this[ fetchMethod ]({ filter, query });
- },
+ }
+ /**
+ * @param filter
+ * @param query
+ * @returns {Promise}
+ */
async fetchGames({ filter, query }) {
if ( !filterMatches( filter, "games" ) ) {
return [];
}
- const store = get( this, "store" );
- const records = await store.query( "twitchSearchGame", {
+ const records = await this.store.query( "twitch-search-game", {
type: "suggest",
live: true,
query
});
return await preload( toArray( records ), "game.box.largeLatest" );
- },
+ }
+ /**
+ * @param filter
+ * @param query
+ * @returns {Promise}
+ */
async fetchChannels({ filter, query }) {
if ( !filterMatches( filter, "channels" ) ) {
return [];
}
- const store = get( this, "store" );
- const records = await store.query( "twitchSearchChannel", {
- offset: get( this, "offset" ),
- limit: get( this, "limit" ),
+ const records = await this.store.query( "twitch-search-channel", {
+ offset: this.offset,
+ limit: this.limit,
query
});
return await preload( mapBy( records, "channel" ), "logo" );
- },
+ }
+ /**
+ * @param filter
+ * @param query
+ * @returns {Promise}
+ */
async fetchStreams({ filter, query }) {
if ( !filterMatches( filter, "streams" ) ) {
return [];
}
- const store = get( this, "store" );
- const records = await store.query( "twitchSearchStream", {
- offset: get( this, "offset" ),
- limit: get( this, "limit" ),
+ const records = await this.store.query( "twitch-search-stream", {
+ offset: this.offset,
+ limit: this.limit,
query
});
return await preload( mapBy( records, "stream" ), "preview.mediumLatest" );
}
-});
+}
diff --git a/src/app/ui/routes/settings/-submenu/route.js b/src/app/ui/routes/settings/-submenu/route.js
index 6b5fcb557f..8a4c55e7d6 100644
--- a/src/app/ui/routes/settings/-submenu/route.js
+++ b/src/app/ui/routes/settings/-submenu/route.js
@@ -2,18 +2,18 @@ import { set } from "@ember/object";
import Route from "@ember/routing/route";
-export default Route.extend({
+export default class SettingsSubmenuRoute extends Route {
model() {
return this.modelFor( "settings" );
- },
+ }
activate() {
const settingsController = this.controllerFor( "settings" );
set( settingsController, "currentSubmenu", this.routeName );
- },
+ }
deactivate() {
const settingsController = this.controllerFor( "settings" );
set( settingsController, "isAnimated", true );
}
-});
+}
diff --git a/src/app/ui/routes/settings/channels/controller.js b/src/app/ui/routes/settings/channels/controller.js
index fc47ca67d5..bae53f1faf 100644
--- a/src/app/ui/routes/settings/channels/controller.js
+++ b/src/app/ui/routes/settings/channels/controller.js
@@ -1,36 +1,32 @@
import Controller from "@ember/controller";
-import { get, computed } from "@ember/object";
+import { computed, action } from "@ember/object";
import { run } from "@ember/runloop";
const reFilter = /^\w+$/;
-export default Controller.extend({
- filter: "",
+export default class SettingsChannelsController extends Controller {
+ filter = "";
- modelFiltered: computed( "model.[]", "all", "filter", function() {
- const filter = get( this, "filter" ).toLowerCase();
+ @computed( "model.[]", "all", "filter" )
+ get modelFiltered() {
+ const filter = this.filter.toLowerCase();
if ( !reFilter.test( filter ) ) {
- return get( this, "model" );
+ return this.model;
}
- return get( this, "all" ).filter(function( item ) {
- return get( item, "settings.id" ).toLowerCase().indexOf( filter ) !== -1;
- });
- }),
-
+ return this.all.filter( item =>
+ item.settings.id.toLowerCase().includes( filter )
+ );
+ }
- actions: {
- erase( modelItem ) {
- const model = get( this, "model" );
- const settingsRecord = get( modelItem, "settings" );
- if ( get( settingsRecord, "isDeleted" ) ) { return; }
+ @action
+ async erase( modelItem ) {
+ const settingsRecord = modelItem.settings;
+ if ( settingsRecord.isDeleted ) { return; }
- run( () => settingsRecord.destroyRecord() )
- .then( () => {
- model.removeObject( modelItem );
- });
- }
+ await run( () => settingsRecord.destroyRecord() );
+ this.model.removeObject( modelItem );
}
-});
+}
diff --git a/src/app/ui/routes/settings/channels/route.js b/src/app/ui/routes/settings/channels/route.js
index f277d46451..2f084582a1 100644
--- a/src/app/ui/routes/settings/channels/route.js
+++ b/src/app/ui/routes/settings/channels/route.js
@@ -1,63 +1,76 @@
-import { default as EmberObject, get, set, computed } from "@ember/object";
-import PromiseProxyMixin from "@ember/object/promise-proxy-mixin";
-import ObjectProxy from "@ember/object/proxy";
+import { set, computed } from "@ember/object";
+import { PromiseObject } from "ember-data/-private";
import SettingsSubmenuRoute from "../-submenu/route";
import InfiniteScrollMixin from "ui/routes/-mixins/routes/infinite-scroll";
import preload from "utils/preload";
-// build our own PromiseObject in order to avoid importing from a private ember-data module
-// TODO: import from @ember-data/promise-proxies once it becomes available
-const PromiseObject = ObjectProxy.extend( PromiseProxyMixin );
+class SettingsChannelsItem {
+ constructor( store, channelSettings ) {
+ this.store = store;
+ this.settings = channelSettings;
+ }
+
+ /** @type {DS.Store} */
+ store;
+
+ /** @type {ChannelSettings} */
+ settings;
+
+ /**
+ * @returns {Promise}
+ */
+ async _getChannel() {
+ /** @type {TwitchUser} */
+ const user = await this.store.queryRecord( "twitch-user", this.settings.id );
+ const channel = await user.loadChannel();
+ await preload( channel, "logo" );
+
+ return channel;
+ }
+ /** @type {DS.PromiseObject} channel */
+ @computed()
+ get channel() {
+ const promise = this._getChannel();
+
+ return PromiseObject.create({ promise });
+ }
+}
-export default SettingsSubmenuRoute.extend( InfiniteScrollMixin, {
- controllerName: "settingsChannels",
- itemSelector: ".settings-channel-item-component",
- all: null,
+export default class SettingsChannelsRoute
+extends SettingsSubmenuRoute.extend( InfiniteScrollMixin ) {
+ controllerName = "settingsChannels";
+ itemSelector = ".settings-channel-item-component";
+ /** @type {SettingsChannelsItem[]} */
+ all = null;
async model() {
- const store = get( this, "store" );
- const channelSettings = await store.findAll( "channelSettings" );
-
- // we need all channelSettings records, so we can search for specific ones
- // that have not been added to the controller's model yet
- this.all = channelSettings.map( record =>
- // return both channelSettings and twitchChannel records
- EmberObject.extend({
- settings: record,
- // load the twitchChannel record on demand (PromiseObject)
- // will be triggered by the first property read-access
- channel: computed(function() {
- const id = get( record, "id" );
- const promise = store.findRecord( "twitchUser", id )
- .then( user => get( user, "channel" ) )
- .then( record => preload( record, "logo" ) );
-
- return PromiseObject.create({ promise });
- })
- }).create()
- );
+ const store = this.store;
+ /** @type {ChannelSettings[]} */
+ const channelSettings = await store.findAll( "channel-settings" );
+ this.all = channelSettings.map( record => new SettingsChannelsItem( store, record ) );
return await this.fetchContent();
- },
+ }
async fetchContent() {
- const limit = get( this, "limit" );
- const offset = get( this, "offset" );
+ const limit = this.limit;
+ const offset = this.offset;
return this.all.slice( offset, offset + limit );
- },
+ }
setupController( controller ) {
set( controller, "all", this.all );
- this._super( ...arguments );
- },
+
+ return super.setupController( ...arguments );
+ }
deactivate() {
- this._super( ...arguments );
+ super.deactivate( ...arguments );
this.all = null;
}
-});
+}
diff --git a/src/app/ui/routes/settings/chat/controller.js b/src/app/ui/routes/settings/chat/controller.js
index cd6673b2e6..037ac3cfec 100644
--- a/src/app/ui/routes/settings/chat/controller.js
+++ b/src/app/ui/routes/settings/chat/controller.js
@@ -11,14 +11,16 @@ const { "chat-url": twitchChatUrl } = twitchConfig;
const { userArgsSubstitutions } = ChatProviderBasic;
-export default Controller.extend({
- chat: service(),
+export default class SettingsChatController extends Controller {
+ /** @type {ChatService} */
+ @service chat;
- providers,
- chatConfig,
- userArgsSubstitutions,
+ providers = providers;
+ chatConfig = chatConfig;
+ userArgsSubstitutions = userArgsSubstitutions;
- contentChatProvider: computed(function() {
+ @computed()
+ get contentChatProvider() {
const list = [];
for ( const [ id ] of providers ) {
const { exec } = chatConfig[ id ];
@@ -26,16 +28,18 @@ export default Controller.extend({
list.push({ id });
}
return list;
- }),
+ }
- contentChatUrl: computed(function() {
+ @computed()
+ get contentChatUrl() {
return Object.keys( twitchChatUrl )
.map( id => ({ id }) );
- }),
+ }
// EmberData (2.9) is stupid and uses an internal Map implementation that is not iterable
// so we can't iterate SettingsChatProvider.attributes in the template
- providerAttributes: computed(function() {
+ @computed()
+ get providerAttributes() {
const map = {};
for ( const [ id, provider ] of providers ) {
const attrs = [];
@@ -44,5 +48,5 @@ export default Controller.extend({
map[ id ] = attrs;
}
return map;
- })
-});
+ }
+}
diff --git a/src/app/ui/routes/settings/controller.js b/src/app/ui/routes/settings/controller.js
index f0200f4eb7..63b09b6bad 100644
--- a/src/app/ui/routes/settings/controller.js
+++ b/src/app/ui/routes/settings/controller.js
@@ -1,45 +1,45 @@
import Controller from "@ember/controller";
-import { get, set } from "@ember/object";
+import { set, action } from "@ember/object";
import { inject as service } from "@ember/service";
import RetryTransitionMixin from "ui/routes/-mixins/controllers/retry-transition";
import "./styles.less";
-export default Controller.extend( RetryTransitionMixin, {
- modal: service(),
- settings: service(),
+export default class SettingsController extends Controller.extend( RetryTransitionMixin ) {
+ /** @type {ModalService} */
+ @service modal;
+ /** @type {SettingsService} */
+ @service settings;
- isAnimated: false,
+ isAnimated = false;
- actions: {
- apply( success, failure ) {
- const modal = get( this, "modal" );
- const settings = get( this, "settings.content" );
+ @action
+ apply( success, failure ) {
+ const settings = this.settings.content;
- get( this, "model" ).applyChanges( settings );
+ this.model.applyChanges( settings );
- settings.save()
- .then( success, failure )
- .then( () => modal.closeModal( this ) )
- .then( () => this.retryTransition() )
- .catch( () => settings.rollbackAttributes() );
- },
-
- discard( success ) {
- const modal = get( this, "modal" );
+ settings.save()
+ .then( success, failure )
+ .then( () => this.modal.closeModal( this ) )
+ .then( () => this.retryTransition() )
+ .catch( () => settings.rollbackAttributes() );
+ }
- get( this, "model" ).discardChanges();
+ @action
+ discard( success ) {
+ this.model.discardChanges();
- Promise.resolve()
- .then( success )
- .then( () => modal.closeModal( this ) )
- .then( () => this.retryTransition() );
- },
+ Promise.resolve()
+ .then( success )
+ .then( () => this.modal.closeModal( this ) )
+ .then( () => this.retryTransition() );
+ }
- cancel() {
- set( this, "previousTransition", null );
- get( this, "modal" ).closeModal( this );
- }
+ @action
+ cancel() {
+ set( this, "previousTransition", null );
+ this.modal.closeModal( this );
}
-});
+}
diff --git a/src/app/ui/routes/settings/gui/controller.js b/src/app/ui/routes/settings/gui/controller.js
index b56aca1fdd..71765da324 100644
--- a/src/app/ui/routes/settings/gui/controller.js
+++ b/src/app/ui/routes/settings/gui/controller.js
@@ -1,5 +1,6 @@
import Controller from "@ember/controller";
-import { get, set, observer } from "@ember/object";
+import { get, set } from "@ember/object";
+import { observes } from "@ember-decorators/object";
import { equal } from "@ember/object/computed";
import {
default as SettingsGui,
@@ -18,15 +19,18 @@ const {
} = SettingsGui;
-export default Controller.extend({
- contentGuiIntegration,
- contentGuiMinimize,
- contentGuiFocusrefresh,
+export default class SettingsGuiController extends Controller {
+ contentGuiIntegration = contentGuiIntegration;
+ contentGuiMinimize = contentGuiMinimize;
+ contentGuiFocusrefresh = contentGuiFocusrefresh;
- hasTaskBarIntegration: equal( "model.gui.integration", ATTR_GUI_INTEGRATION_TASKBAR ),
- hasBothIntegrations: equal( "model.gui.integration", ATTR_GUI_INTEGRATION_BOTH ),
+ @equal( "model.gui.integration", ATTR_GUI_INTEGRATION_TASKBAR )
+ hasTaskBarIntegration;
+ @equal( "model.gui.integration", ATTR_GUI_INTEGRATION_BOTH )
+ hasBothIntegrations;
- _integrationObserver: observer( "model.gui.integration", function() {
+ @observes( "model.gui.integration" )
+ _integrationObserver() {
const integration = get( this, "model.gui.integration" );
const minimize = get( this, "model.gui.minimize" );
const noTask = ( integration & ATTR_GUI_INTEGRATION_TASKBAR ) === 0;
@@ -43,5 +47,5 @@ export default Controller.extend({
// enable/disable buttons
set( contentGuiMinimize[ ATTR_GUI_MINIMIZE_MINIMIZE ], "disabled", noTask );
set( contentGuiMinimize[ ATTR_GUI_MINIMIZE_TRAY ], "disabled", noTray );
- })
-});
+ }
+}
diff --git a/src/app/ui/routes/settings/index/route.js b/src/app/ui/routes/settings/index/route.js
index f64c3cdcb6..98a8e9ab45 100644
--- a/src/app/ui/routes/settings/index/route.js
+++ b/src/app/ui/routes/settings/index/route.js
@@ -1,23 +1,20 @@
-import { get, set } from "@ember/object";
+import { set, action } from "@ember/object";
import Route from "@ember/routing/route";
-export default Route.extend({
- actions: {
- didTransition() {
- const settingsController = this.controllerFor( "settings" );
- let goto = get( settingsController, "currentSubmenu" );
- if ( !goto ) {
- goto = "settings.main";
- }
+export default class SettingsIndexRoute extends Route {
+ @action
+ didTransition() {
+ const settingsController = this.controllerFor( "settings" );
+ const goto = settingsController.currentSubmenu || "settings.main";
- set( settingsController, "isAnimated", false );
+ set( settingsController, "isAnimated", false );
- this.replaceWith( goto );
- },
+ this.replaceWith( goto );
+ }
- willTransition() {
- return false;
- }
+ @action
+ willTransition() {
+ return false;
}
-});
+}
diff --git a/src/app/ui/routes/settings/languages/controller.js b/src/app/ui/routes/settings/languages/controller.js
index e0876833d6..f5fec49020 100644
--- a/src/app/ui/routes/settings/languages/controller.js
+++ b/src/app/ui/routes/settings/languages/controller.js
@@ -7,12 +7,13 @@ import SettingsStreams from "data/models/settings/streams/fragment";
const { filterLanguages: contentStreamsFilterLanguages } = SettingsStreams;
-export default Controller.extend({
- contentStreamsFilterLanguages,
+export default class SettingsLanguagesController extends Controller {
+ contentStreamsFilterLanguages = contentStreamsFilterLanguages;
- languages: computed(function() {
+ @computed()
+ get languages() {
return Object.keys( langsConfig )
.filter( code => !langsConfig[ code ].disabled )
.map( code => ({ id: code, label: code }) );
- })
-});
+ }
+}
diff --git a/src/app/ui/routes/settings/main/controller.js b/src/app/ui/routes/settings/main/controller.js
index 68a976e748..81fbcabc63 100644
--- a/src/app/ui/routes/settings/main/controller.js
+++ b/src/app/ui/routes/settings/main/controller.js
@@ -8,10 +8,11 @@ const { locales } = localesConfig;
const { themes } = themesConfig;
-export default Controller.extend({
- systemThemeId: "system",
+export default class SettingsMainController extends Controller {
+ systemThemeId = "system";
- contentGuiLanguages: computed(function() {
+ @computed()
+ get contentGuiLanguages() {
const compare = new Intl.Collator( "en", { sensitivity: "base" } ).compare;
const languages = Object.keys( locales )
.map( key => ({
@@ -25,9 +26,10 @@ export default Controller.extend({
languages.unshift({ id: "auto", label: locales[ systemLocale ] });
return languages;
- }),
+ }
- contentGuiTheme: computed(function() {
+ @computed()
+ get contentGuiTheme() {
return [ this.systemThemeId, ...themes ].map( id => ({ id }) );
- })
-});
+ }
+}
diff --git a/src/app/ui/routes/settings/notifications/controller.js b/src/app/ui/routes/settings/notifications/controller.js
index d09b0a406b..838075f3d5 100644
--- a/src/app/ui/routes/settings/notifications/controller.js
+++ b/src/app/ui/routes/settings/notifications/controller.js
@@ -1,5 +1,5 @@
import Controller from "@ember/controller";
-import { get, computed } from "@ember/object";
+import { get, computed, action } from "@ember/object";
import { inject as service } from "@ember/service";
import { main as mainConfig } from "config";
import SettingsNotification from "data/models/settings/notification/fragment";
@@ -16,34 +16,35 @@ const {
} = SettingsNotification;
-export default Controller.extend({
- contentNotificationFilter,
- contentNotificationClick,
- contentNotificationClickGroup,
+export default class SettingsNotificationsController extends Controller {
+ /** @type {I18nService} */
+ @service i18n;
- i18n: service(),
+ contentNotificationFilter = contentNotificationFilter;
+ contentNotificationClick = contentNotificationClick;
+ contentNotificationClickGroup = contentNotificationClickGroup;
// filter available notification providers
- contentNotificationProviders: computed(function() {
+ @computed()
+ get contentNotificationProviders() {
return SettingsNotification.providers
.filter( item => isSupported( item.id ) || item.id === "auto" );
- }),
-
- actions: {
- testNotification( success, failure ) {
- const i18n = get( this, "i18n" );
- const provider = get( this, "model.notification.provider" );
- const message = i18n.t( "settings.notifications.provider.test.message" ).toString();
-
- const data = new NotificationData({
- title: displayName,
- icon,
- message
- });
-
- showNotification( provider, data, true )
- .then( success, failure )
- .catch( () => {} );
- }
}
-});
+
+
+ @action
+ testNotification( success, failure ) {
+ const provider = get( this, "model.notification.provider" );
+ const message = this.i18n.t( "settings.notifications.provider.test.message" ).toString();
+
+ const data = new NotificationData({
+ title: displayName,
+ icon,
+ message
+ });
+
+ showNotification( provider, data, true )
+ .then( success, failure )
+ .catch( () => {} );
+ }
+}
diff --git a/src/app/ui/routes/settings/player/controller.js b/src/app/ui/routes/settings/player/controller.js
index 0ff823fb9d..b7476dbcfc 100644
--- a/src/app/ui/routes/settings/player/controller.js
+++ b/src/app/ui/routes/settings/player/controller.js
@@ -12,14 +12,17 @@ const { assign } = Object;
const { isArray } = Array;
-export default Controller.extend({
- i18n: service(),
- store: service(),
+export default class SettingsPlayerController extends Controller {
+ /** @type {I18nService} */
+ @service i18n;
+ /** @type {DS.Store} */
+ @service store;
- substitutionsPlayer,
+ substitutionsPlayer = substitutionsPlayer;
// filter platform dependent player parameters
- players: computed(function() {
+ @computed()
+ get players() {
const list = {};
for ( const [ id, player ] of Object.entries( playersConfig ) ) {
const obj = list[ id ] = assign( {}, player );
@@ -36,14 +39,13 @@ export default Controller.extend({
}
return list;
- }),
-
- contentStreamingPlayer: computed(function() {
- const i18n = get( this, "i18n" );
+ }
+ @computed()
+ get contentStreamingPlayer() {
const presets = [{
id: "default",
- label: i18n.t( "settings.player.players.default.label" ).toString()
+ label: this.i18n.t( "settings.player.players.default.label" ).toString()
}];
for ( const [ id, { exec, disabled } ] of Object.entries( playersConfig ) ) {
if ( disabled || !exec[ platform ] ) { continue; }
@@ -52,34 +54,31 @@ export default Controller.extend({
}
return presets;
- }),
-
- playerPlaceholder: computed( "i18n.locale", "model.streaming.player", function() {
- const i18n = get( this, "i18n" );
+ }
+ @computed( "i18n.locale", "model.streaming.player" )
+ get playerPlaceholder() {
const player = get( this, "model.streaming.player" );
if ( player === "default" || !playersConfig[ player ] ) {
- return i18n.t( "settings.player.executable.default.placeholder" ).toString();
+ return this.i18n.t( "settings.player.executable.default.placeholder" ).toString();
}
const exec = playersConfig[ player ][ "exec" ][ platform ];
if ( !exec ) {
- return i18n.t( "settings.player.executable.preset.placeholder" ).toString();
+ return this.i18n.t( "settings.player.executable.preset.placeholder" ).toString();
}
return isArray( exec )
? exec.join( `${delimiter} ` )
: exec;
- }),
+ }
- playerPresetDefault: equal( "model.streaming.player", "default" ),
+ @equal( "model.streaming.player", "default" )
+ playerPresetDefault;
- playerPresetDefaultAndPlayerEmpty: computed(
- "playerPresetDefault",
- "model.streaming.players.default.exec",
- function() {
- return get( this, "playerPresetDefault" )
- && !get( this, "model.streaming.players.default.exec" );
- }
- )
-});
+ @computed( "playerPresetDefault", "model.streaming.players.default.exec" )
+ get playerPresetDefaultAndPlayerEmpty() {
+ return this.playerPresetDefault
+ && !get( this, "model.streaming.players.default.exec" );
+ }
+}
diff --git a/src/app/ui/routes/settings/route.js b/src/app/ui/routes/settings/route.js
index 0e4e1d5a82..fe13c81317 100644
--- a/src/app/ui/routes/settings/route.js
+++ b/src/app/ui/routes/settings/route.js
@@ -1,4 +1,4 @@
-import { get, set } from "@ember/object";
+import { set, action } from "@ember/object";
import Route from "@ember/routing/route";
import { inject as service } from "@ember/service";
import ObjectBuffer from "utils/ember/ObjectBuffer";
@@ -7,42 +7,41 @@ import ObjectBuffer from "utils/ember/ObjectBuffer";
const reRouteNames = /^settings\.\w+$/;
-export default Route.extend({
- modal: service(),
- settings: service(),
+export default class SettingsRoute extends Route {
+ /** @type {ModalService} */
+ @service modal;
+ /** @type {SettingsService} */
+ @service settings;
- model() {
- const settings = get( this, "settings.content" );
+ model() {
return ObjectBuffer.create({
- content: settings.toJSON()
+ content: this.settings.content.toJSON()
});
- },
+ }
resetController( controller, isExiting ) {
if ( isExiting ) {
set( controller, "isAnimated", false );
}
- },
-
- actions: {
- willTransition( previousTransition ) {
- // don't show modal when transitioning between settings subroutes
- if ( previousTransition && reRouteNames.test( previousTransition.targetName ) ) {
- return true;
- }
-
- // check whether the user has changed any values
- const controller = get( this, "controller" );
- if ( !get( controller, "model.isDirty" ) ) { return; }
-
- // stay here...
- previousTransition.abort();
-
- // and let the user decide
- get( this, "modal" ).openModal( "confirm", controller, {
- previousTransition
- });
+ }
+
+ @action
+ willTransition( previousTransition ) {
+ // don't show modal when transitioning between settings subroutes
+ if ( previousTransition && reRouteNames.test( previousTransition.targetName ) ) {
+ return true;
}
+
+ // check whether the user has changed any values
+ if ( !this.controller.model.isDirty ) { return; }
+
+ // stay here...
+ previousTransition.abort();
+
+ // and let the user decide
+ this.modal.openModal( "confirm", this.controller, {
+ previousTransition
+ });
}
-});
+}
diff --git a/src/app/ui/routes/settings/streaming/controller.js b/src/app/ui/routes/settings/streaming/controller.js
index 3604a7f076..11116c17d9 100644
--- a/src/app/ui/routes/settings/streaming/controller.js
+++ b/src/app/ui/routes/settings/streaming/controller.js
@@ -20,12 +20,13 @@ function settingsAttrMeta( attr, prop ) {
}
-export default Controller.extend({
- platform,
- providers,
- contentStreamingPlayerInput,
+export default class SettingsStreamingController extends Controller {
+ platform = platform;
+ providers = providers;
+ contentStreamingPlayerInput = contentStreamingPlayerInput;
- contentStreamingProvider: computed(function() {
+ @computed()
+ get contentStreamingProvider() {
return Object.keys( providers )
// exclude unsupported providers
.filter( id => providers[ id ][ "exec" ][ platform ] )
@@ -33,37 +34,52 @@ export default Controller.extend({
id,
label: providers[ id ][ "label" ]
}) );
- }),
+ }
// can't use the fragment's providerName computed property here
// the controller's model is an ObjectBuffer instance
- providerName: computed( "model.streaming.provider", function() {
+ @computed( "model.streaming.provider" )
+ get providerName() {
const provider = get( this, "model.streaming.provider" );
return providers[ provider ][ "name" ];
- }),
+ }
- playerInputDocumentation: computed( "model.streaming.player_input", function() {
+ @computed( "model.streaming.player_input" )
+ get playerInputDocumentation() {
const input = get( this, "model.streaming.player_input" );
return contentStreamingPlayerInput.findBy( "id", input ).documentation;
- }),
-
- playerInputPassthrough: equal( "model.streaming.player_input", inputPassthrough ),
-
- hlsLiveEdgeDefault: settingsAttrMeta( "hls_live_edge", "defaultValue" ),
- hlsLiveEdgeMin: settingsAttrMeta( "hls_live_edge", "min" ),
- hlsLiveEdgeMax: settingsAttrMeta( "hls_live_edge", "max" ),
-
- hlsSegmentThreadsDefault: settingsAttrMeta( "hls_segment_threads", "defaultValue" ),
- hlsSegmentThreadsMin: settingsAttrMeta( "hls_segment_threads", "min" ),
- hlsSegmentThreadsMax: settingsAttrMeta( "hls_segment_threads", "max" ),
-
- retryStreamsDefault: settingsAttrMeta( "retry_streams", "defaultValue" ),
- retryStreamsMin: settingsAttrMeta( "retry_streams", "min" ),
- retryStreamsMax: settingsAttrMeta( "retry_streams", "max" ),
-
- retryOpenDefault: settingsAttrMeta( "retry_open", "defaultValue" ),
- retryOpenMin: settingsAttrMeta( "retry_open", "min" ),
- retryOpenMax: settingsAttrMeta( "retry_open", "max" )
-});
+ }
+
+ @equal( "model.streaming.player_input", inputPassthrough )
+ playerInputPassthrough;
+
+ @settingsAttrMeta( "hls_live_edge", "defaultValue" )
+ hlsLiveEdgeDefault;
+ @settingsAttrMeta( "hls_live_edge", "min" )
+ hlsLiveEdgeMin;
+ @settingsAttrMeta( "hls_live_edge", "max" )
+ hlsLiveEdgeMax;
+
+ @settingsAttrMeta( "hls_segment_threads", "defaultValue" )
+ hlsSegmentThreadsDefault;
+ @settingsAttrMeta( "hls_segment_threads", "min" )
+ hlsSegmentThreadsMin;
+ @settingsAttrMeta( "hls_segment_threads", "max" )
+ hlsSegmentThreadsMax;
+
+ @settingsAttrMeta( "retry_streams", "defaultValue" )
+ retryStreamsDefault;
+ @settingsAttrMeta( "retry_streams", "min" )
+ retryStreamsMin;
+ @settingsAttrMeta( "retry_streams", "max" )
+ retryStreamsMax;
+
+ @settingsAttrMeta( "retry_open", "defaultValue" )
+ retryOpenDefault;
+ @settingsAttrMeta( "retry_open", "min" )
+ retryOpenMin;
+ @settingsAttrMeta( "retry_open", "max" )
+ retryOpenMax;
+}
diff --git a/src/app/ui/routes/settings/streams/controller.js b/src/app/ui/routes/settings/streams/controller.js
index e0c21e9d16..7d50b44b43 100644
--- a/src/app/ui/routes/settings/streams/controller.js
+++ b/src/app/ui/routes/settings/streams/controller.js
@@ -14,13 +14,13 @@ const {
} = SettingsStreams;
-export default Controller.extend({
- contentStreamingQuality,
- contentStreamsName,
- contentStreamsInfo,
- contentStreamsClick,
+export default class SettingsStreamsController extends Controller {
+ contentStreamingQuality = contentStreamingQuality;
+ contentStreamsName = contentStreamsName;
+ contentStreamsInfo = contentStreamsInfo;
+ contentStreamsClick = contentStreamsClick;
- DEFAULT_VODCAST_REGEXP,
+ DEFAULT_VODCAST_REGEXP = DEFAULT_VODCAST_REGEXP;
- isDarwin
-});
+ isDarwin = isDarwin;
+}
diff --git a/src/app/ui/routes/streams/route.js b/src/app/ui/routes/streams/route.js
index 9d02f648f8..a55aa7485f 100644
--- a/src/app/ui/routes/streams/route.js
+++ b/src/app/ui/routes/streams/route.js
@@ -4,8 +4,9 @@ import FilterLanguagesMixin from "ui/routes/-mixins/routes/filter-languages";
import RefreshRouteMixin from "ui/routes/-mixins/routes/refresh";
-export default Route.extend( InfiniteScrollOffsetMixin, FilterLanguagesMixin, RefreshRouteMixin, {
- itemSelector: ".stream-item-component",
- modelName: "twitchStream",
- modelPreload: "preview.mediumLatest"
-});
+export default class StreamsRoute
+extends Route.extend( InfiniteScrollOffsetMixin, FilterLanguagesMixin, RefreshRouteMixin ) {
+ itemSelector = ".stream-item-component";
+ modelName = "twitch-stream";
+ modelPreload = "preview.mediumLatest";
+}
diff --git a/src/app/ui/routes/team/index/route.js b/src/app/ui/routes/team/index/route.js
index 312347ee66..c97dea90ce 100644
--- a/src/app/ui/routes/team/index/route.js
+++ b/src/app/ui/routes/team/index/route.js
@@ -1,34 +1,33 @@
-import { get } from "@ember/object";
import Route from "@ember/routing/route";
import InfiniteScrollMixin from "ui/routes/-mixins/routes/infinite-scroll";
import { toArray } from "utils/ember/recordArrayMethods";
import preload from "utils/preload";
-export default Route.extend( InfiniteScrollMixin, {
- itemSelector: ".stream-item-component",
+export default class TeamIndexRoute extends Route.extend( InfiniteScrollMixin ) {
+ itemSelector = ".stream-item-component";
beforeModel() {
this.customOffset = 0;
- return this._super( ...arguments );
- },
+ return super.beforeModel( ...arguments );
+ }
async model() {
- const store = get( this, "store" );
- const limit = get( this, "limit" );
+ const store = this.store;
+ const limit = this.limit;
const model = this.modelFor( "team" );
- const users = get( model, "users" );
- const length = get( users, "length" );
+ const channels = model.users;
+ const length = channels.length;
let offsetCalculated = false;
const options = { reload: true };
const fill = async ( streams, start ) => {
const end = start + limit;
- const records = await Promise.all( users
+ const records = await Promise.all( channels
.slice( start, end )
- .map( channel => store.findRecord( "twitchStream", get( channel, "id" ), options )
+ .map( channel => store.findRecord( "twitch-stream", channel.id, options )
.catch( () => false )
)
);
@@ -55,4 +54,4 @@ export default Route.extend( InfiniteScrollMixin, {
return await preload( records, "preview.mediumLatest" );
}
-});
+}
diff --git a/src/app/ui/routes/team/info/route.js b/src/app/ui/routes/team/info/route.js
index ac10b1ca08..9d63662705 100644
--- a/src/app/ui/routes/team/info/route.js
+++ b/src/app/ui/routes/team/info/route.js
@@ -2,12 +2,12 @@ import { getOwner } from "@ember/application";
import Route from "@ember/routing/route";
-export default Route.extend({
+export default class TeamInfoRoute extends Route {
model() {
return this.modelFor( "team" );
- },
+ }
refresh() {
return getOwner( this ).lookup( "route:team" ).refresh();
}
-});
+}
diff --git a/src/app/ui/routes/team/members/route.js b/src/app/ui/routes/team/members/route.js
index 23a019d7b0..dd96a38c5d 100644
--- a/src/app/ui/routes/team/members/route.js
+++ b/src/app/ui/routes/team/members/route.js
@@ -1,22 +1,20 @@
-import { get } from "@ember/object";
import Route from "@ember/routing/route";
import InfiniteScrollMixin from "ui/routes/-mixins/routes/infinite-scroll";
import preload from "utils/preload";
-export default Route.extend( InfiniteScrollMixin, {
- itemSelector: ".channel-item-component",
+export default class TeamMembersRoute extends Route.extend( InfiniteScrollMixin ) {
+ itemSelector = ".channel-item-component";
async model() {
const model = this.modelFor( "team" );
- const offset = get( this, "offset" );
- const limit = get( this, "limit" );
+ const offset = this.offset;
+ const limit = this.limit;
- const channels = get( model, "users" )
- .slice( offset, offset + limit );
+ const channels = model.users.slice( offset, offset + limit );
const records = await Promise.all( channels );
return await preload( records, "logo" );
}
-});
+}
diff --git a/src/app/ui/routes/team/route.js b/src/app/ui/routes/team/route.js
index 7ea01b2b83..42bdf03196 100644
--- a/src/app/ui/routes/team/route.js
+++ b/src/app/ui/routes/team/route.js
@@ -1,14 +1,12 @@
-import { get } from "@ember/object";
import Route from "@ember/routing/route";
import RefreshRouteMixin from "ui/routes/-mixins/routes/refresh";
import preload from "utils/preload";
-export default Route.extend( RefreshRouteMixin, {
+export default class TeamRoute extends Route.extend( RefreshRouteMixin ) {
async model({ team }) {
- const store = get( this, "store" );
- const record = await store.findRecord( "twitchTeam", team, { reload: true } );
+ const record = await this.store.findRecord( "twitch-team", team, { reload: true } );
return await preload( record, "logo" );
}
-});
+}
diff --git a/src/app/ui/routes/user/auth/controller.js b/src/app/ui/routes/user/auth/controller.js
index afad7e4839..a65cd0f555 100644
--- a/src/app/ui/routes/user/auth/controller.js
+++ b/src/app/ui/routes/user/auth/controller.js
@@ -1,26 +1,28 @@
import Controller from "@ember/controller";
-import { get, set, computed, observer } from "@ember/object";
+import { set, computed, action } from "@ember/object";
+import { later } from "@ember/runloop";
import { inject as service } from "@ember/service";
-import { twitch } from "config";
+import { observes } from "@ember-decorators/object";
+import { twitch as twitchConfig } from "config";
import RetryTransitionMixin from "ui/routes/-mixins/controllers/retry-transition";
-import wait from "utils/wait";
-const { oauth: { scope } } = twitch;
+const { oauth: { scope } } = twitchConfig;
-export default Controller.extend( RetryTransitionMixin, {
- auth: service(),
- settings: service(),
+export default class UserAuthController extends Controller.extend( RetryTransitionMixin ) {
+ /** @type {AuthService} */
+ @service auth;
+ /** @type {SettingsService} */
+ @service settings;
retryTransition() {
// use "user.index" as default route
- return this._super( "user.index" );
- },
-
- token: "",
+ return super.retryTransition( "user.index" );
+ }
- scope: scope.join( ", " ),
+ token = "";
+ scope = scope.join( ", " );
/**
* 0 000: start
@@ -32,107 +34,114 @@ export default Controller.extend( RetryTransitionMixin, {
* 6 110: token - failure
* 7 111: token - success
*/
- loginStatus: 0,
+ loginStatus = 0;
- userStatus: computed( "loginStatus", function() {
- return get( this, "loginStatus" ) & 3;
- }),
+ @computed( "loginStatus" )
+ get userStatus() {
+ return this.loginStatus & 0b011;
+ }
- hasUserInteraction: computed( "userStatus", function() {
- return get( this, "userStatus" ) > 0;
- }),
+ @computed( "userStatus" )
+ get hasUserInteraction() {
+ return this.userStatus > 0;
+ }
- isLoggingIn: computed( "loginStatus", function() {
- return get( this, "loginStatus" ) === 1;
- }),
+ @computed( "loginStatus" )
+ get isLoggingIn() {
+ return this.loginStatus === 1;
+ }
- hasLoginResult: computed( "userStatus", function() {
- const userStatus = get( this, "userStatus" );
- return ( userStatus & 2 ) > 0;
- }),
+ @computed( "userStatus" )
+ get hasLoginResult() {
+ return ( this.userStatus & 0b010 ) > 0;
+ }
- showFailMessage: computed( "userStatus", function() {
- return get( this, "userStatus" ) === 2;
- }),
+ @computed( "userStatus" )
+ get isFailMessageVisible() {
+ return this.userStatus === 0b010;
+ }
- showTokenForm: computed( "loginStatus", function() {
- return get( this, "loginStatus" ) & 4;
- }),
+ @computed( "loginStatus" )
+ get isTokenFormVisible() {
+ return this.loginStatus & 0b100;
+ }
- serverObserver: observer( "auth.server", function() {
- const authServer = get( this, "auth.server" );
+ @observes( "auth.server" )
+ serverObserver() {
+ const authServer = this.auth.server;
set( this, "loginStatus", authServer ? 1 : 0 );
- }),
+ }
resetProperties() {
set( this, "token", "" );
set( this, "loginStatus", 0 );
- },
+ }
+
+ @action
+ showTokenForm() {
+ set( this, "loginStatus", 4 );
+ }
- actions: {
- showTokenForm() {
+ // login via user and password
+ @action
+ async signin() {
+ if ( this.isLoggingIn ) { return; }
+ set( this, "loginStatus", 1 );
+
+ try {
+ await this.auth.signin();
+ set( this, "loginStatus", 3 );
+ this.retryTransition();
+
+ } catch ( e ) {
+ set( this, "loginStatus", 2 );
+ await new Promise( r => later( r, 3000 ) );
+ set( this, "loginStatus", 0 );
+ }
+ }
+
+ // login via access token
+ @action
+ async signinToken( success, failure ) {
+ if ( this.isLoggingIn ) { return; }
+ set( this, "loginStatus", 5 );
+
+ const token = this.token;
+
+ // show the loading icon for a sec and wait
+ await new Promise( r => later( r, 1000 ) );
+
+ let successful = false;
+ try {
+ // login attempt
+ await this.auth.login( token, false );
+
+ // visualize result: update button and icon
+ set( this, "loginStatus", 7 );
+ await new Promise( r => later( r, 1000 ) );
+ await success();
+ successful = true;
+
+ } catch ( e ) {
+ set( this, "loginStatus", 6 );
+ await new Promise( r => later( r, 3000 ) );
+ await failure();
+
+ } finally {
set( this, "loginStatus", 4 );
- },
-
- // login via user and password
- signin() {
- if ( get( this, "isLoggingIn" ) ) { return; }
- set( this, "loginStatus", 1 );
-
- const auth = get( this, "auth" );
-
- auth.signin()
- .then( () => {
- set( this, "loginStatus", 3 );
- this.retryTransition();
- })
- .catch( () => {
- set( this, "loginStatus", 2 );
- wait( 3000 )()
- .then( () => {
- set( this, "loginStatus", 0 );
- });
- });
- },
-
- // login via access token
- signinToken( success, failure ) {
- if ( get( this, "isLoggingIn" ) ) { return; }
- set( this, "loginStatus", 5 );
-
- const token = get( this, "token" );
- const auth = get( this, "auth" );
-
- // show the loading icon for a sec and wait
- wait( 1000 )()
- // login attempt
- .then( () => auth.login( token, false ) )
- // visualize result: update button and icon
- .then( () => {
- set( this, "loginStatus", 7 );
- return wait( 1000 )( true )
- .then( success );
- }, () => {
- set( this, "loginStatus", 6 );
- return wait( 3000 )( false )
- .then( failure )
- .catch( data => data );
- })
- // retry transition on success
- .then( result => {
- set( this, "loginStatus", 4 );
- if ( result ) {
- this.retryTransition();
- }
- });
- },
-
- // abort sign in with username + password
- abort() {
- get( this, "auth" ).abortSignin();
+ // retry transition on success
+ if ( successful ) {
+ this.retryTransition();
+ }
}
}
-});
+
+ // abort sign in with username + password
+ @action
+ abort() {
+ this.auth.abortSignin();
+ }
+}
diff --git a/src/app/ui/routes/user/auth/route.js b/src/app/ui/routes/user/auth/route.js
index e03011fd84..f9a1a5b47a 100644
--- a/src/app/ui/routes/user/auth/route.js
+++ b/src/app/ui/routes/user/auth/route.js
@@ -1,23 +1,28 @@
-import { get } from "@ember/object";
+import { action } from "@ember/object";
import Route from "@ember/routing/route";
import { inject as service } from "@ember/service";
-export default Route.extend({
- auth: service(),
+export default class UserAuthRoute extends Route {
+ /** @type {AuthService} */
+ @service auth;
+ /** @type {RouterService} */
+ @service router;
beforeModel( transition ) {
+ /** @type {Auth} */
+ const session = this.auth.session;
+
// check if user is successfully logged in
- if ( get( this, "auth.session.isLoggedIn" ) ) {
+ if ( session && session.isLoggedIn ) {
transition.abort();
- this.transitionTo( "user.index" );
+ this.router.transitionTo( "user.index" );
}
- },
+ }
- actions: {
- willTransition() {
- this.controller.send( "abort" );
- this.controller.resetProperties();
- }
+ @action
+ willTransition() {
+ this.controller.send( "abort" );
+ this.controller.resetProperties();
}
-});
+}
diff --git a/src/app/ui/routes/user/auth/template.hbs b/src/app/ui/routes/user/auth/template.hbs
index a14d4dc5c4..5303d027ee 100644
--- a/src/app/ui/routes/user/auth/template.hbs
+++ b/src/app/ui/routes/user/auth/template.hbs
@@ -20,7 +20,7 @@
{{/if}}