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} + * @returns {Promise} */ getChannelSettings() { - const store = get( this, "store" ); - const id = get( this, "name" ); + const store = this.store; + const name = this.name; // For a very weird reason, exceptions thrown by store.findRecord() are not being caught // when using an async function and a try/catch block with the await keyword. // So use a regular promise chain here instead. - return store.findRecord( "channelSettings", id ) - .catch( () => store.recordForId( "channelSettings", id ) ) + return store.findRecord( "channel-settings", name ) + .catch( () => store.recordForId( "channel-settings", name ) ) .then( record => { // get the record's data and then unload it const data = record.toJSON(); @@ -122,7 +142,4 @@ export default Model.extend({ return data; }); } - -}).reopenClass({ - toString() { return "kraken/channels"; } -}); +} diff --git a/src/app/data/models/twitch/channel/serializer.js b/src/app/data/models/twitch/channel/serializer.js index 1d5f9c4800..4e1c832b76 100644 --- a/src/app/data/models/twitch/channel/serializer.js +++ b/src/app/data/models/twitch/channel/serializer.js @@ -1,16 +1,14 @@ import TwitchSerializer from "data/models/twitch/serializer"; -export default TwitchSerializer.extend({ - modelNameFromPayloadKey() { - return "twitchChannel"; - }, +export default class TwitchChannelSerializer extends TwitchSerializer { + modelNameFromPayloadKey = () => "twitch-channel"; normalizeResponse( store, primaryModelClass, payload, id, requestType ) { payload = { - twitchChannel: 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/twitch/game-top/model.js b/src/app/data/models/twitch/game-top/model.js index 139a56b9cf..952cf5d00c 100644 --- a/src/app/data/models/twitch/game-top/model.js +++ b/src/app/data/models/twitch/game-top/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({ - channels: attr( "number" ), - game: belongsTo( "twitchGame", { async: false } ), - viewers: attr( "number" ) - -}).reopenClass({ - toString() { return "kraken/games/top"; } -}); +@name( "kraken/games/top" ) +export default class TwitchGameTop extends Model { + @attr( "number" ) + channels; + /** @type {TwitchGame} */ + @belongsTo( "twitch-game", { async: false } ) + game; + @attr( "number" ) + viewers; +} diff --git a/src/app/data/models/twitch/game-top/serializer.js b/src/app/data/models/twitch/game-top/serializer.js index f3971767b9..e2e8474940 100644 --- a/src/app/data/models/twitch/game-top/serializer.js +++ b/src/app/data/models/twitch/game-top/serializer.js @@ -1,21 +1,19 @@ import TwitchSerializer from "data/models/twitch/serializer"; -export default TwitchSerializer.extend({ - modelNameFromPayloadKey() { - return "twitchGameTop"; - }, +export default class TwitchGameTopSerializer extends TwitchSerializer { + modelNameFromPayloadKey = () => "twitch-game-top"; - attrs: { + attrs = { game: { deserialize: "records" } - }, + }; normalize( modelClass, resourceHash, prop ) { - const foreignKey = this.store.serializerFor( "twitchGame" ).primaryKey; + const foreignKey = this.store.serializerFor( "twitch-game" ).primaryKey; // get the id of the embedded TwitchGame record and apply it here resourceHash[ this.primaryKey ] = resourceHash.game[ foreignKey ]; - return this._super( modelClass, resourceHash, prop ); + return super.normalize( modelClass, resourceHash, prop ); } -}); +} diff --git a/src/app/data/models/twitch/game/model.js b/src/app/data/models/twitch/game/model.js index 38e7f33a2b..3b98d86b72 100644 --- a/src/app/data/models/twitch/game/model.js +++ b/src/app/data/models/twitch/game/model.js @@ -1,15 +1,21 @@ 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({ - box: belongsTo( "twitchImage", { async: false } ), - //giantbomb_id: attr( "number" ), - logo: belongsTo( "twitchImage", { async: false } ), - name: attr( "string" ) - //popularity: attr( "number" ) - -}).reopenClass({ - toString() { return "kraken/games"; } -}); +@name( "kraken/games" ) +export default class TwitchGame extends Model { + /** @type {TwitchImage} */ + @belongsTo( "twitch-image", { async: false } ) + box; + //@attr( "number" ) + //giantbomb_id; + /** @type {TwitchImage} */ + @belongsTo( "twitch-image", { async: false } ) + logo; + @attr( "string" ) + name; + //@attr( "number" ) + //popularity; +} diff --git a/src/app/data/models/twitch/game/serializer.js b/src/app/data/models/twitch/game/serializer.js index 0b3bde3e0a..27497293f7 100644 --- a/src/app/data/models/twitch/game/serializer.js +++ b/src/app/data/models/twitch/game/serializer.js @@ -1,21 +1,19 @@ import TwitchSerializer from "data/models/twitch/serializer"; -export default TwitchSerializer.extend({ - primaryKey: "name", +export default class TwitchGameSerializer extends TwitchSerializer { + primaryKey = "name"; - modelNameFromPayloadKey() { - return "twitchGame"; - }, + modelNameFromPayloadKey = () => "twitch-game"; - attrs: { + attrs = { box: { deserialize: "records" }, logo: { deserialize: "records" } - }, + }; normalize( modelClass, resourceHash, prop ) { const id = resourceHash[ this.primaryKey ]; - const foreignKey = this.store.serializerFor( "twitchImage" ).primaryKey; + const foreignKey = this.store.serializerFor( "twitch-image" ).primaryKey; // apply the id of this record to the embedded TwitchImage records (box and logo) if ( resourceHash.box ) { @@ -25,6 +23,6 @@ export default TwitchSerializer.extend({ resourceHash.logo[ foreignKey ] = `game/logo/${id}`; } - return this._super( modelClass, resourceHash, prop ); + return super.normalize( modelClass, resourceHash, prop ); } -}); +} diff --git a/src/app/data/models/twitch/image/model.js b/src/app/data/models/twitch/image/model.js index 5f07bce9a0..78c92eb851 100644 --- a/src/app/data/models/twitch/image/model.js +++ b/src/app/data/models/twitch/image/model.js @@ -1,4 +1,3 @@ -import { get, computed } from "@ember/object"; import attr from "ember-data/attr"; import Model from "ember-data/model"; import { vars } from "config"; @@ -14,29 +13,28 @@ function getURL( url, time ) { /** * Return the image URL with an already set expiration time. * Or create a new expiration time if it hasn't been set yet. - * @param {String} attr - * @returns {Ember.ComputedProperty} Volatile computed property + * @param {string} attr + * @returns {function(): PropertyDescriptor} */ -function buffered( attr ) { - return computed(function() { - let exp = this[ `expiration_${attr}` ]; +const buffered = attr => () => ({ + get() { + const exp = this[ `expiration_${attr}` ]; return exp - ? getURL( get( this, `image_${attr}` ), exp ) - : get( this, `${attr}Latest` ); - }).volatile(); -} + ? getURL( this[ `image_${attr}` ], exp ) + : this[ `${attr}Latest` ]; + } +}); /** * Return the image URL with an expiration parameter, so the latest version will be requested. * Update the expiration timer only once every X seconds. - * @param {String} attr - * @returns {Ember.ComputedProperty} Volatile computed property + * @param {string} attr + * @returns {function(): PropertyDescriptor} */ -function latest( attr ) { - // use a volatile property - return computed(function() { - const url = get( this, `image_${attr}` ); +const latest = attr => () => ({ + get() { + const url = this[ `image_${attr}` ]; // use the same timestamp for `time` seconds const key = `expiration_${attr}`; @@ -49,30 +47,39 @@ function latest( attr ) { } return getURL( url, exp ); - }).volatile(); -} + } +}); -export default Model.extend({ +export default class TwitchImage extends Model { // original attributes (renamed) - image_large: attr( "string" ), - image_medium: attr( "string" ), - image_small: attr( "string" ), + @attr( "string" ) + image_large; + @attr( "string" ) + image_medium; + @attr( "string" ) + image_small; // expiration times - expiration_large: null, - expiration_medium: null, - expiration_small: null, + expiration_large = null; + expiration_medium = null; + expiration_small = null; // "request latest image version, but only every X seconds" // should be used by a route's model hook - largeLatest: latest( "large" ), - mediumLatest: latest( "medium" ), - smallLatest: latest( "small" ), + @latest( "large" ) + largeLatest; + @latest( "medium" ) + mediumLatest; + @latest( "small" ) + smallLatest; // "use the previous expiration parameter" // should be used by all image src attributes in the DOM - large: buffered( "large" ), - medium: buffered( "medium" ), - small: buffered( "small" ) -}); + @buffered( "large" ) + large; + @buffered( "medium" ) + medium; + @buffered( "small" ) + small; +} diff --git a/src/app/data/models/twitch/image/serializer.js b/src/app/data/models/twitch/image/serializer.js index fe313266d2..7269b6ff92 100644 --- a/src/app/data/models/twitch/image/serializer.js +++ b/src/app/data/models/twitch/image/serializer.js @@ -1,12 +1,12 @@ import TwitchSerializer from "data/models/twitch/serializer"; -export default TwitchSerializer.extend({ +export default class TwitchImageSerializer extends TwitchSerializer { normalize( modelClass, resourceHash, prop ) { resourceHash.image_small = resourceHash.small; resourceHash.image_medium = resourceHash.medium; resourceHash.image_large = resourceHash.large; - return this._super( modelClass, resourceHash, prop ); + return super.normalize( modelClass, resourceHash, prop ); } -}); +} diff --git a/src/app/data/models/twitch/root/model.js b/src/app/data/models/twitch/root/model.js index 7a9021da7f..5d8bdafc51 100644 --- a/src/app/data/models/twitch/root/model.js +++ b/src/app/data/models/twitch/root/model.js @@ -1,20 +1,21 @@ import attr from "ember-data/attr"; import Model from "ember-data/model"; import { array } from "ember-data-model-fragments/attributes"; +import { name } from "utils/decorators"; -/** - * @class TwitchRoot - * @extends Model - */ -export default Model.extend({ - created_at: attr( "date" ), - scopes: array( "string" ), - updated_at: attr( "date" ), - user_id: attr( "number" ), - user_name: attr( "string" ), - valid: attr( "boolean" ) - -}).reopenClass({ - toString() { return "kraken/"; } -}); +@name( "kraken/" ) +export default class TwitchRoot extends Model { + @attr( "date" ) + created_at; + @array( "string" ) + scopes; + @attr( "date" ) + updated_at; + @attr( "number" ) + user_id; + @attr( "string" ) + user_name; + @attr( "boolean" ) + valid; +} diff --git a/src/app/data/models/twitch/root/serializer.js b/src/app/data/models/twitch/root/serializer.js index d27542972d..f327945c00 100644 --- a/src/app/data/models/twitch/root/serializer.js +++ b/src/app/data/models/twitch/root/serializer.js @@ -1,10 +1,8 @@ import TwitchSerializer from "data/models/twitch/serializer"; -export default TwitchSerializer.extend({ - modelNameFromPayloadKey() { - return "twitchRoot"; - }, +export default class TwitchRootSerializer extends TwitchSerializer { + modelNameFromPayloadKey = () => "twitch-root"; normalize( modelClass, resourceHash, prop ) { // add an ID to the record @@ -16,6 +14,6 @@ export default TwitchSerializer.extend({ resourceHash.created_at = created_at; resourceHash.updated_at = updated_at; - return this._super( modelClass, resourceHash, prop ); + return super.normalize( modelClass, resourceHash, prop ); } -}); +} diff --git a/src/app/data/models/twitch/search-channel/model.js b/src/app/data/models/twitch/search-channel/model.js index 91ab6ca044..e02151a5cf 100644 --- a/src/app/data/models/twitch/search-channel/model.js +++ b/src/app/data/models/twitch/search-channel/model.js @@ -1,10 +1,11 @@ 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 } ) - -}).reopenClass({ - toString() { return "kraken/search/channels"; } -}); +@name( "kraken/search/channels" ) +export default class TwitchSearchChannel extends Model { + /** @type {TwitchChannel} */ + @belongsTo( "twitch-channel", { async: false } ) + channel; +} diff --git a/src/app/data/models/twitch/search-channel/serializer.js b/src/app/data/models/twitch/search-channel/serializer.js index 48bc5b0e12..0b6a264800 100644 --- a/src/app/data/models/twitch/search-channel/serializer.js +++ b/src/app/data/models/twitch/search-channel/serializer.js @@ -1,28 +1,26 @@ import TwitchSerializer from "data/models/twitch/serializer"; -export default TwitchSerializer.extend({ - modelNameFromPayloadKey() { - return "twitchSearchChannel"; - }, +export default class TwitchSearchChannelSerializer extends TwitchSerializer { + modelNameFromPayloadKey = () => "twitch-search-channel"; - attrs: { + attrs = { channel: { deserialize: "records" } - }, + }; normalizeResponse( store, primaryModelClass, payload, id, requestType ) { // fix payload format payload.channels = ( payload.channels || [] ).map( channel => ({ channel }) ); - return this._super( store, primaryModelClass, payload, id, requestType ); - }, + return super.normalizeResponse( store, primaryModelClass, payload, id, requestType ); + } normalize( modelClass, resourceHash, prop ) { - const foreignKey = this.store.serializerFor( "twitchChannel" ).primaryKey; + const foreignKey = this.store.serializerFor( "twitch-channel" ).primaryKey; // get the id of the embedded TwitchChannel record and apply it here resourceHash[ this.primaryKey ] = resourceHash.channel[ foreignKey ]; - return this._super( modelClass, resourceHash, prop ); + return super.normalize( modelClass, resourceHash, prop ); } -}); +} diff --git a/src/app/data/models/twitch/search-game/model.js b/src/app/data/models/twitch/search-game/model.js index 46bdd512ea..42a7ff3002 100644 --- a/src/app/data/models/twitch/search-game/model.js +++ b/src/app/data/models/twitch/search-game/model.js @@ -1,10 +1,11 @@ import Model from "ember-data/model"; import { belongsTo } from "ember-data/relationships"; +import { name } from "utils/decorators"; -export default Model.extend({ - game: belongsTo( "twitchGame", { async: false } ) - -}).reopenClass({ - toString() { return "kraken/search/games"; } -}); +@name( "kraken/search/games" ) +export default class TwitchSearchGame extends Model { + /** @type {TwitchGame} */ + @belongsTo( "twitch-game", { async: false } ) + game; +} diff --git a/src/app/data/models/twitch/search-game/serializer.js b/src/app/data/models/twitch/search-game/serializer.js index ffc84432ba..a3fa5e939b 100644 --- a/src/app/data/models/twitch/search-game/serializer.js +++ b/src/app/data/models/twitch/search-game/serializer.js @@ -1,28 +1,26 @@ import TwitchSerializer from "data/models/twitch/serializer"; -export default TwitchSerializer.extend({ - modelNameFromPayloadKey() { - return "twitchSearchGame"; - }, +export default class TwitchSearchGameSerializer extends TwitchSerializer { + modelNameFromPayloadKey = () => "twitch-search-game"; - attrs: { + attrs = { game: { deserialize: "records" } - }, + }; normalizeResponse( store, primaryModelClass, payload, id, requestType ) { // fix payload format payload.games = ( payload.games || [] ).map( game => ({ game }) ); - return this._super( store, primaryModelClass, payload, id, requestType ); - }, + return super.normalizeResponse( store, primaryModelClass, payload, id, requestType ); + } normalize( modelClass, resourceHash, prop ) { - const foreignKey = this.store.serializerFor( "twitchGame" ).primaryKey; + const foreignKey = this.store.serializerFor( "twitch-game" ).primaryKey; // get the id of the embedded TwitchGame record and apply it here resourceHash[ this.primaryKey ] = resourceHash.game[ foreignKey ]; - return this._super( modelClass, resourceHash, prop ); + return super.normalize( modelClass, resourceHash, prop ); } -}); +} diff --git a/src/app/data/models/twitch/search-stream/model.js b/src/app/data/models/twitch/search-stream/model.js index 72ee9a1471..241f3ca5ab 100644 --- a/src/app/data/models/twitch/search-stream/model.js +++ b/src/app/data/models/twitch/search-stream/model.js @@ -1,10 +1,11 @@ import Model from "ember-data/model"; import { belongsTo } from "ember-data/relationships"; +import { name } from "utils/decorators"; -export default Model.extend({ - stream: belongsTo( "twitchStream", { async: false } ) - -}).reopenClass({ - toString() { return "kraken/search/streams"; } -}); +@name( "kraken/search/streams" ) +export default class TwitchSearchStream extends Model { + /** @type {TwitchStream} */ + @belongsTo( "twitch-stream", { async: false } ) + stream; +} diff --git a/src/app/data/models/twitch/search-stream/serializer.js b/src/app/data/models/twitch/search-stream/serializer.js index 810ce61b54..c3ecc17b2c 100644 --- a/src/app/data/models/twitch/search-stream/serializer.js +++ b/src/app/data/models/twitch/search-stream/serializer.js @@ -1,28 +1,26 @@ import TwitchSerializer from "data/models/twitch/serializer"; -export default TwitchSerializer.extend({ - modelNameFromPayloadKey() { - return "twitchSearchStream"; - }, +export default class TwitchSearchStreamSerializer extends TwitchSerializer { + modelNameFromPayloadKey = () => "twitch-search-stream"; - attrs: { + attrs = { stream: { deserialize: "records" } - }, + }; normalizeResponse( store, primaryModelClass, payload, id, requestType ) { // fix payload format payload.streams = ( payload.streams || [] ).map( stream => ({ stream }) ); - return this._super( store, primaryModelClass, payload, id, requestType ); - }, + return super.normalizeResponse( store, primaryModelClass, payload, id, requestType ); + } normalize( modelClass, resourceHash, prop ) { - const foreignKey = this.store.serializerFor( "twitchChannel" ).primaryKey; + const foreignKey = this.store.serializerFor( "twitch-channel" ).primaryKey; // get the id of the embedded TwitchChannel record and apply it here resourceHash[ this.primaryKey ] = resourceHash.stream.channel[ foreignKey ]; - return this._super( modelClass, resourceHash, prop ); + return super.normalize( modelClass, resourceHash, prop ); } -}); +} diff --git a/src/app/data/models/twitch/serializer.js b/src/app/data/models/twitch/serializer.js index 7fe94c8303..a1cd250c10 100644 --- a/src/app/data/models/twitch/serializer.js +++ b/src/app/data/models/twitch/serializer.js @@ -2,10 +2,10 @@ import EmbeddedRecordsMixin from "ember-data/serializers/embedded-records-mixin" import RESTSerializer from "ember-data/serializers/rest"; -export default RESTSerializer.extend( EmbeddedRecordsMixin, { - isNewSerializerAPI: true, +export default class TwitchSerializer extends RESTSerializer.extend( EmbeddedRecordsMixin ) { + isNewSerializerAPI = true; - primaryKey: "_id", + primaryKey = "_id"; /** * All underscored properties contain metadata (except the primaryKey) @@ -19,7 +19,7 @@ export default RESTSerializer.extend( EmbeddedRecordsMixin, { const primaryKey = this.primaryKey; const data = {}; - Object.keys( payload ).forEach(function( key ) { + Object.keys( payload ).forEach( key => { if ( key.charAt( 0 ) === "_" && key !== primaryKey ) { data[ key.substr( 1 ) ] = payload[ key ]; delete payload[ key ]; @@ -28,4 +28,4 @@ export default RESTSerializer.extend( EmbeddedRecordsMixin, { return data; } -}); +} diff --git a/src/app/data/models/twitch/stream-featured/model.js b/src/app/data/models/twitch/stream-featured/model.js index d450bae1c8..4bf40ad8b9 100644 --- a/src/app/data/models/twitch/stream-featured/model.js +++ b/src/app/data/models/twitch/stream-featured/model.js @@ -1,17 +1,24 @@ 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({ - image: attr( "string" ), - priority: attr( "number" ), - scheduled: attr( "boolean" ), - sponsored: attr( "boolean" ), - stream: belongsTo( "twitchStream", { async: false } ), - text: attr( "string" ), - title: attr( "string" ) - -}).reopenClass({ - toString() { return "kraken/streams/featured"; } -}); +@name( "kraken/streams/featured" ) +export default class TwitchStreamFeatured extends Model { + @attr( "string" ) + image; + @attr( "number" ) + priority; + @attr( "boolean" ) + scheduled; + @attr( "boolean" ) + sponsored; + /** @type {TwitchStream} */ + @belongsTo( "twitch-stream", { async: false } ) + stream; + @attr( "string" ) + text; + @attr( "string" ) + title; +} diff --git a/src/app/data/models/twitch/stream-featured/serializer.js b/src/app/data/models/twitch/stream-featured/serializer.js index e40ea0bfa9..6e225c85e1 100644 --- a/src/app/data/models/twitch/stream-featured/serializer.js +++ b/src/app/data/models/twitch/stream-featured/serializer.js @@ -1,21 +1,19 @@ import TwitchSerializer from "data/models/twitch/serializer"; -export default TwitchSerializer.extend({ - modelNameFromPayloadKey() { - return "twitchStreamFeatured"; - }, +export default class TwitchStreamFeaturedSerializer extends TwitchSerializer { + modelNameFromPayloadKey = () => "twitch-stream-featured"; - attrs: { + attrs = { stream: { deserialize: "records" } - }, + }; normalize( modelClass, resourceHash, prop ) { - const foreignKey = this.store.serializerFor( "twitchChannel" ).primaryKey; + const foreignKey = this.store.serializerFor( "twitch-channel" ).primaryKey; // get the id of the embedded TwitchChannel record and apply it here resourceHash[ this.primaryKey ] = resourceHash.stream.channel[ foreignKey ]; - return this._super( modelClass, resourceHash, prop ); + return super.normalize( modelClass, resourceHash, prop ); } -}); +} diff --git a/src/app/data/models/twitch/stream-followed/model.js b/src/app/data/models/twitch/stream-followed/model.js index 6ee90dbae9..fa605fe23a 100644 --- a/src/app/data/models/twitch/stream-followed/model.js +++ b/src/app/data/models/twitch/stream-followed/model.js @@ -1,10 +1,11 @@ import Model from "ember-data/model"; import { belongsTo } from "ember-data/relationships"; +import { name } from "utils/decorators"; -export default Model.extend({ - stream: belongsTo( "twitchStream", { async: false } ) - -}).reopenClass({ - toString() { return "kraken/streams/followed"; } -}); +@name( "kraken/streams/followed" ) +export default class TwitchStreamFollowed extends Model { + /** @type {TwitchStream} */ + @belongsTo( "twitch-stream", { async: false } ) + stream; +} diff --git a/src/app/data/models/twitch/stream-followed/serializer.js b/src/app/data/models/twitch/stream-followed/serializer.js index 7e8d99b2dc..c296ce28ad 100644 --- a/src/app/data/models/twitch/stream-followed/serializer.js +++ b/src/app/data/models/twitch/stream-followed/serializer.js @@ -1,28 +1,26 @@ import TwitchSerializer from "data/models/twitch/serializer"; -export default TwitchSerializer.extend({ - modelNameFromPayloadKey() { - return "twitchStreamFollowed"; - }, +export default class TwitchStreamFollowedSerializer extends TwitchSerializer { + modelNameFromPayloadKey = () => "twitch-stream-followed"; - attrs: { + attrs = { stream: { deserialize: "records" } - }, + }; normalizeResponse( store, primaryModelClass, payload, id, requestType ) { // fix payload format payload.streams = ( payload.streams || [] ).map( stream => ({ stream }) ); - return this._super( store, primaryModelClass, payload, id, requestType ); - }, + return super.normalizeResponse( store, primaryModelClass, payload, id, requestType ); + } normalize( modelClass, resourceHash, prop ) { - const foreignKey = this.store.serializerFor( "twitchChannel" ).primaryKey; + const foreignKey = this.store.serializerFor( "twitch-channel" ).primaryKey; // get the id of the embedded TwitchChannel record and apply it here resourceHash[ this.primaryKey ] = resourceHash.stream.channel[ foreignKey ]; - return this._super( modelClass, resourceHash, prop ); + return super.normalize( modelClass, resourceHash, prop ); } -}); +} diff --git a/src/app/data/models/twitch/stream-summary/model.js b/src/app/data/models/twitch/stream-summary/model.js index e45af39e3f..9b2a4f0af9 100644 --- a/src/app/data/models/twitch/stream-summary/model.js +++ b/src/app/data/models/twitch/stream-summary/model.js @@ -1,11 +1,12 @@ import attr from "ember-data/attr"; import Model from "ember-data/model"; +import { name } from "utils/decorators"; -export default Model.extend({ - channels: attr( "number" ), - viewers: attr( "number" ) - -}).reopenClass({ - toString() { return "kraken/streams/summary"; } -}); +@name( "kraken/streams/summary" ) +export default class TwitchStreamSummary extends Model { + @attr( "number" ) + channels; + @attr( "number" ) + viewers; +} diff --git a/src/app/data/models/twitch/stream-summary/serializer.js b/src/app/data/models/twitch/stream-summary/serializer.js index 04ab2c9e1a..ac7c99ebd6 100644 --- a/src/app/data/models/twitch/stream-summary/serializer.js +++ b/src/app/data/models/twitch/stream-summary/serializer.js @@ -1,10 +1,8 @@ import TwitchSerializer from "data/models/twitch/serializer"; -export default TwitchSerializer.extend({ - modelNameFromPayloadKey() { - return "twitchStreamSummary"; - }, +export default class TwitchStreamSummarySerializer extends TwitchSerializer { + modelNameFromPayloadKey = () => "twitch-stream-summary"; normalizeResponse( store, primaryModelClass, payload, id, requestType ) { // always use 1 as id @@ -12,9 +10,9 @@ export default TwitchSerializer.extend({ // fix payload format payload = { - twitchStreamSummary: 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/twitch/stream/adapter.js b/src/app/data/models/twitch/stream/adapter.js index b08b6ac38a..10890285bb 100644 --- a/src/app/data/models/twitch/stream/adapter.js +++ b/src/app/data/models/twitch/stream/adapter.js @@ -1,7 +1,7 @@ import TwitchAdapter from "data/models/twitch/adapter"; -export default TwitchAdapter.extend({ - coalesceFindRequests: true, - findManyIdString: "channel" -}); +export default class TwitchStreamAdapter extends TwitchAdapter { + coalesceFindRequests = true; + findManyIdString = "channel"; +} diff --git a/src/app/data/models/twitch/stream/model.js b/src/app/data/models/twitch/stream/model.js index 32534d6f54..ae0b90a20d 100644 --- a/src/app/data/models/twitch/stream/model.js +++ b/src/app/data/models/twitch/stream/model.js @@ -1,4 +1,4 @@ -import { get, computed } from "@ember/object"; +import { computed } from "@ember/object"; import { and } from "@ember/object/computed"; import { inject as service } from "@ember/service"; import attr from "ember-data/attr"; @@ -6,6 +6,7 @@ import Model from "ember-data/model"; import { belongsTo } from "ember-data/relationships"; import Moment from "moment"; import { DEFAULT_VODCAST_REGEXP } from "data/models/settings/streams/fragment"; +import { name } from "utils/decorators"; /** @@ -38,26 +39,43 @@ const fpsRanges = [ const reRerun = /rerun|watch_party/; -export default Model.extend({ - i18n: service(), - settings: service(), - - - average_fps: attr( "number" ), - broadcast_platform: attr( "string" ), - channel: belongsTo( "twitchChannel", { async: false } ), - created_at: attr( "date" ), - delay: attr( "number" ), - game: attr( "string" ), - //is_playlist: attr( "boolean" ), - preview: belongsTo( "twitchImage", { async: false } ), - stream_type: attr( "string" ), - video_height: attr( "number" ), - viewers: attr( "number" ), - - - reVodcast: computed( "settings.streams.vodcast_regexp", function() { - const vodcast_regexp = get( this, "settings.streams.vodcast_regexp" ); +@name( "kraken/streams" ) +export default class TwitchStream extends Model { + /** @type {I18nService} */ + @service i18n; + /** @type {SettingsService} */ + @service settings; + + + @attr( "number" ) + average_fps; + @attr( "string" ) + broadcast_platform; + /** @type {TwitchChannel} */ + @belongsTo( "twitch-channel", { async: false } ) + channel; + @attr( "date" ) + created_at; + @attr( "number" ) + delay; + @attr( "string" ) + game; + //@attr( "boolean" ) + //is_playlist; + /** @type {TwitchImage} */ + @belongsTo( "twitch-image", { async: false } ) + preview; + @attr( "string" ) + stream_type; + @attr( "number" ) + video_height; + @attr( "number" ) + viewers; + + + @computed( "settings.content.streams.vodcast_regexp" ) + get reVodcast() { + const vodcast_regexp = this.settings.content.streams.vodcast_regexp; if ( vodcast_regexp.length && !vodcast_regexp.trim().length ) { return null; } @@ -66,36 +84,38 @@ export default Model.extend({ } catch ( e ) { return null; } - }), + } // both properties are not documented in the v5 API - isVodcast: computed( + @computed( "broadcast_platform", "stream_type", "reVodcast", - "channel.status", - function() { - if ( - reRerun.test( get( this, "broadcast_platform" ) ) - || reRerun.test( get( this, "stream_type" ) ) - ) { - return true; - } - - const reVodcast = get( this, "reVodcast" ); - const status = get( this, "channel.status" ); - - return reVodcast && status - ? reVodcast.test( status ) - : false; + "channel.status" + ) + get isVodcast() { + if ( + reRerun.test( this.broadcast_platform ) + || reRerun.test( this.stream_type ) + ) { + return true; } - ), + const reVodcast = this.reVodcast; + const status = this.channel.status; + + return reVodcast && status + ? reVodcast.test( status ) + : false; + } - hasFormatInfo: and( "video_height", "average_fps" ), + @and( "video_height", "average_fps" ) + hasFormatInfo; - titleCreatedAt: computed( "i18n.locale", "created_at", function() { + + @computed( "i18n.locale", "created_at" ) + get titleCreatedAt() { const moment = new Moment( this.created_at ); const last24h = moment.diff( new Date(), "days" ) === 0; const format = last24h @@ -103,27 +123,26 @@ export default Model.extend({ : this.i18n.t( "models.twitch.stream.created-at.more-than-24h" ); return moment.format( format.toString() ); - }), - - titleViewers: computed( "i18n.locale", "viewers", function() { - const i18n = get( this, "i18n" ); - const count = get( this, "viewers" ); + } - return i18n.t( "models.twitch.stream.viewers", { count } ); - }), + @computed( "i18n.locale", "viewers" ) + get titleViewers() { + return this.i18n.t( "models.twitch.stream.viewers", { count: this.viewers } ); + } - resolution: computed( "video_height", function() { + @computed( "video_height" ) + get resolution() { // assume 16:9 - const video_height = get( this, "video_height" ); + const video_height = this.video_height; const width = Math.round( ( 16 / 9 ) * video_height ); const height = Math.round( video_height ); return `${width}x${height}`; - }), - - fps: computed( "average_fps", function() { - const average_fps = get( this, "average_fps" ); + } + @computed( "average_fps" ) + get fps() { + const average_fps = this.average_fps; if ( !average_fps ) { return null; } const fpsRange = fpsRanges.find( fpsRange => @@ -134,8 +153,5 @@ export default Model.extend({ return fpsRange ? fpsRange.target : Math.floor( average_fps ); - }) - -}).reopenClass({ - toString() { return "kraken/streams"; } -}); + } +} diff --git a/src/app/data/models/twitch/stream/serializer.js b/src/app/data/models/twitch/stream/serializer.js index 776c6ca736..f402e58429 100644 --- a/src/app/data/models/twitch/stream/serializer.js +++ b/src/app/data/models/twitch/stream/serializer.js @@ -1,19 +1,17 @@ import TwitchSerializer from "data/models/twitch/serializer"; -export default TwitchSerializer.extend({ - modelNameFromPayloadKey() { - return "twitchStream"; - }, +export default class TwitchStreamSerializer extends TwitchSerializer { + modelNameFromPayloadKey = () => "twitch-stream"; - attrs: { + attrs = { channel: { deserialize: "records" }, preview: { deserialize: "records" } - }, + }; normalize( modelClass, resourceHash, prop ) { - const foreignKeyChannel = this.store.serializerFor( "twitchChannel" ).primaryKey; - const foreignKeyImage = this.store.serializerFor( "twitchImage" ).primaryKey; + const foreignKeyChannel = this.store.serializerFor( "twitch-channel" ).primaryKey; + const foreignKeyImage = this.store.serializerFor( "twitch-image" ).primaryKey; // get the id of the embedded TwitchChannel record and apply it here // this is required for refreshing the record so that the correct id is being used @@ -25,6 +23,6 @@ export default TwitchSerializer.extend({ resourceHash.preview[ foreignKeyImage ] = `stream/preview/${id}`; } - return this._super( modelClass, resourceHash, prop ); + return super.normalize( modelClass, resourceHash, prop ); } -}); +} diff --git a/src/app/data/models/twitch/subscription/model.js b/src/app/data/models/twitch/subscription/model.js index 35b15c20af..de171d8e32 100644 --- a/src/app/data/models/twitch/subscription/model.js +++ b/src/app/data/models/twitch/subscription/model.js @@ -1,14 +1,18 @@ import attr from "ember-data/attr"; import Model from "ember-data/model"; +import { name } from "utils/decorators"; -export default Model.extend({ - //channel: belongsTo( "twitchChannel" ), - //is_gift: attr( "boolean" ), - //sender: attr( "number or string???" ), - //sub_plan: attr( "string" ), - created_at: attr( "date" ) - -}).reopenClass({ - toString() { return "kraken/users/:user_id/subscriptions"; } -}); +@name( "kraken/users/:user_id/subscriptions" ) +export default class TwitchSubscription extends Model { + //@belongsTo( "twitch-channel" ) + //channel; + //@attr( "boolean" ) + //is_gift; + //@attr( "number or string???" ) + //sender; + //@attr( "string" ) + //sub_plan; + @attr( "date" ) + created_at; +} diff --git a/src/app/data/models/twitch/subscription/serializer.js b/src/app/data/models/twitch/subscription/serializer.js index 641fc1c1ef..4457685420 100644 --- a/src/app/data/models/twitch/subscription/serializer.js +++ b/src/app/data/models/twitch/subscription/serializer.js @@ -1,22 +1,20 @@ import TwitchSerializer from "data/models/twitch/serializer"; -export default TwitchSerializer.extend({ - modelNameFromPayloadKey() { - return "twitchSubscription"; - }, +export default class TwitchSubscriptionSerializer extends TwitchSerializer { + modelNameFromPayloadKey = () => "twitch-subscription"; normalizeResponse( store, primaryModelClass, payload, id, requestType ) { - const foreignKey = this.store.serializerFor( "twitchChannel" ).primaryKey; + const foreignKey = this.store.serializerFor( "twitch-channel" ).primaryKey; // get the id of the embedded TwitchChannel record and apply it here payload[ this.primaryKey ] = payload.channel[ foreignKey ]; // fix payload format payload = { - twitchSubscription: 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/twitch/team/adapter.js b/src/app/data/models/twitch/team/adapter.js index cf52a49ad9..c1077fe2be 100644 --- a/src/app/data/models/twitch/team/adapter.js +++ b/src/app/data/models/twitch/team/adapter.js @@ -1,14 +1,14 @@ import TwitchAdapter from "data/models/twitch/adapter"; -export default TwitchAdapter.extend({ +export default class TwitchTeamAdapter extends TwitchAdapter { query( store, type, query ) { const url = this.buildURL( type, null, null, "query", query ); delete query.channel; query = this.sortQueryParams ? this.sortQueryParams( query ) : query; return this.ajax( url, "GET", { data: query } ); - }, + } urlForQuery( query ) { // use this approach until EmberData ships the new ds-improved-ajax feature @@ -17,6 +17,6 @@ export default TwitchAdapter.extend({ return this._buildURL( `kraken/channels/${query.channel}/teams` ); } - return this._super( ...arguments ); + return super.urlForQuery( ...arguments ); } -}); +} diff --git a/src/app/data/models/twitch/team/model.js b/src/app/data/models/twitch/team/model.js index 022c50504e..0de4eda5cc 100644 --- a/src/app/data/models/twitch/team/model.js +++ b/src/app/data/models/twitch/team/model.js @@ -1,26 +1,34 @@ -import { get, computed } from "@ember/object"; +import { computed } from "@ember/object"; import attr from "ember-data/attr"; import Model from "ember-data/model"; import { hasMany } from "ember-data/relationships"; +import { name } from "utils/decorators"; -export default Model.extend({ - background: attr( "string" ), - banner: attr( "string" ), - created_at: attr( "date" ), - display_name: attr( "string" ), - info: attr( "string" ), - logo: attr( "string" ), - name: attr( "string" ), - updated_at: attr( "date" ), - users: hasMany( "twitchChannel", { async: false } ), +@name( "kraken/teams" ) +export default class TwitchTeam extends Model { + @attr( "string" ) + background; + @attr( "string" ) + banner; + @attr( "date" ) + created_at; + @attr( "string" ) + display_name; + @attr( "string" ) + info; + @attr( "string" ) + logo; + @attr( "string" ) + name; + @attr( "date" ) + updated_at; + /** @type {TwitchChannel[]} */ + @hasMany( "twitch-channel", { async: false } ) + users; - - title: computed( "name", "display_name", function() { - return get( this, "display_name" ) - || get( this, "name" ); - }) - -}).reopenClass({ - toString() { return "kraken/teams"; } -}); + @computed( "name", "display_name" ) + get title() { + return this.display_name || this.name; + } +} diff --git a/src/app/data/models/twitch/team/serializer.js b/src/app/data/models/twitch/team/serializer.js index ef7f6e27a8..32c9dabc1e 100644 --- a/src/app/data/models/twitch/team/serializer.js +++ b/src/app/data/models/twitch/team/serializer.js @@ -1,16 +1,14 @@ import TwitchSerializer from "data/models/twitch/serializer"; -export default TwitchSerializer.extend({ - primaryKey: "name", +export default class TwitchTeamSerializer extends TwitchSerializer { + primaryKey = "name"; - modelNameFromPayloadKey() { - return "twitchTeam"; - }, + modelNameFromPayloadKey = () => "twitch-team"; - attrs: { + attrs = { users: { deserialize: "records" } - }, + }; normalizeSingleResponse( store, primaryModelClass, payload, id, requestType ) { // fix payload format @@ -18,6 +16,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/user/adapter.js b/src/app/data/models/twitch/user/adapter.js index 73a0464b48..6fabff10f7 100644 --- a/src/app/data/models/twitch/user/adapter.js +++ b/src/app/data/models/twitch/user/adapter.js @@ -1,11 +1,11 @@ import TwitchAdapter from "data/models/twitch/adapter"; -export default TwitchAdapter.extend({ +export default class TwitchUserAdapter extends TwitchAdapter { // automatically turns multiple findRecord calls into a single findMany call - coalesceFindRequests: true, + coalesceFindRequests = true; // and uses "login" as query string parameter for the IDs, as defined by the TwitchAdapter - findManyIdString: "login", + findManyIdString = "login"; /** * @param {DS.Store} store @@ -20,8 +20,8 @@ export default TwitchAdapter.extend({ login: id }; - return this.ajax( url, "GET", { data: data } ); - }, + return this.ajax( url, "GET", { data } ); + } /** * @param {string} id @@ -30,7 +30,7 @@ export default TwitchAdapter.extend({ */ urlForFindRecord( id, type ) { return this._buildURL( type ); - }, + } /** * @param {DS.Store} store @@ -46,7 +46,7 @@ export default TwitchAdapter.extend({ const url = this.buildURL( type, null, null, "queryRecord", query ); return this.ajax( url, "GET", { data: query } ); - }, + } /** * @param {Object} query @@ -56,4 +56,4 @@ export default TwitchAdapter.extend({ urlForQueryRecord( query, type ) { return this._buildURL( type ); } -}); +} diff --git a/src/app/data/models/twitch/user/model.js b/src/app/data/models/twitch/user/model.js index 7b36416a3a..a0cfa5c736 100644 --- a/src/app/data/models/twitch/user/model.js +++ b/src/app/data/models/twitch/user/model.js @@ -1,18 +1,39 @@ import Model from "ember-data/model"; import { belongsTo } from "ember-data/relationships"; +import { name } from "utils/decorators"; /** - * This model is only being used for mapping channel names to user IDs. - * Users are being looked up via GET /kraken/users?login=:name + * This model only gets used for mapping channel names to user IDs. + * Users are looked up via GET /kraken/users?login=:name * * The primary key is the user/channel name, so it can be looked up via store.findRecord. * The original primary key (_id) is used as a key for TwitchChannel/TwitchStream relationships. */ -export default Model.extend({ - channel: belongsTo( "twitchChannel", { async: true } ), - stream: belongsTo( "twitchStream", { async: true } ) +@name( "kraken/users" ) +export default class TwitchUser extends Model { + /** @type {PromiseObject} */ + @belongsTo( "twitch-channel", { async: true } ) + channel; + /** @type {PromiseObject} */ + @belongsTo( "twitch-stream", { async: true } ) + stream; -}).reopenClass({ - toString() { return "kraken/users"; } -}); + /** + * @returns {Promise} + */ + async loadChannel() { + await this.channel.promise; + + return this.channel.content; + } + + /** + * @returns {Promise} + */ + async loadStream() { + await this.stream.promise; + + return this.stream.content; + } +} diff --git a/src/app/data/models/twitch/user/serializer.js b/src/app/data/models/twitch/user/serializer.js index 1f07327705..59c3e4fed1 100644 --- a/src/app/data/models/twitch/user/serializer.js +++ b/src/app/data/models/twitch/user/serializer.js @@ -1,12 +1,10 @@ import TwitchSerializer from "data/models/twitch/serializer"; -export default TwitchSerializer.extend({ - primaryKey: "id", +export default class TwitchUserSerializer extends TwitchSerializer { + primaryKey = "id"; - modelNameFromPayloadKey() { - return "twitchUser"; - }, + modelNameFromPayloadKey = () => "twitch-user"; normalizeResponse( store, primaryModelClass, payload, id, requestType ) { payload = { @@ -18,17 +16,13 @@ export default TwitchSerializer.extend({ }) ) }; - return this._super( store, primaryModelClass, payload, id, requestType ); - }, + return super.normalizeResponse( store, primaryModelClass, payload, id, requestType ); + } - normalizeFindRecordResponse( store, primaryModelClass, payload, id, requestType ) { + normalizeSingleResponse( store, primaryModelClass, payload, id, requestType ) { const key = this.modelNameFromPayloadKey(); payload[ key ] = payload[ key ][0] /* istanbul ignore next */ || null; - return this._super( store, primaryModelClass, payload, id, requestType ); - }, - - normalizeQueryRecordResponse( ...args ) { - return this.normalizeFindRecordResponse( ...args ); + return super.normalizeSingleResponse( store, primaryModelClass, payload, id, requestType ); } -}); +} diff --git a/src/app/data/models/versioncheck/adapter.js b/src/app/data/models/versioncheck/adapter.js index 37d8b4beea..53fc527b54 100644 --- a/src/app/data/models/versioncheck/adapter.js +++ b/src/app/data/models/versioncheck/adapter.js @@ -1,6 +1,6 @@ import LocalStorageAdapter from "ember-localstorage-adapter/adapters/ls-adapter"; -export default LocalStorageAdapter.extend({ - namespace: "versioncheck" -}); +export default class VersioncheckAdapter extends LocalStorageAdapter { + namespace = "versioncheck"; +} diff --git a/src/app/data/models/versioncheck/model.js b/src/app/data/models/versioncheck/model.js index 9a8d8dc360..6361546a1d 100644 --- a/src/app/data/models/versioncheck/model.js +++ b/src/app/data/models/versioncheck/model.js @@ -1,12 +1,14 @@ import attr from "ember-data/attr"; import Model from "ember-data/model"; +import { name } from "utils/decorators"; -export default Model.extend({ - version: attr( "string", { defaultValue: "" } ), - checkagain: attr( "number", { defaultValue: 0 } ), - showdebugmessage: attr( "number", { defaultValue: 0 } ) - -}).reopenClass({ - toString() { return "Versioncheck"; } -}); +@name( "Versioncheck" ) +export default class Versioncheck extends Model { + @attr( "string", { defaultValue: "" } ) + version; + @attr( "number", { defaultValue: 0 } ) + checkagain; + @attr( "number", { defaultValue: 0 } ) + showdebugmessage; +} diff --git a/src/app/data/models/window/adapter.js b/src/app/data/models/window/adapter.js index 298c42bc35..d1d4b0108b 100644 --- a/src/app/data/models/window/adapter.js +++ b/src/app/data/models/window/adapter.js @@ -1,6 +1,6 @@ import LocalStorageAdapter from "ember-localstorage-adapter/adapters/ls-adapter"; -export default LocalStorageAdapter.extend({ - namespace: "window" -}); +export default class WindowAdapter extends LocalStorageAdapter { + namespace = "window"; +} diff --git a/src/app/data/models/window/model.js b/src/app/data/models/window/model.js index 72fd10b795..4c7eb4d176 100644 --- a/src/app/data/models/window/model.js +++ b/src/app/data/models/window/model.js @@ -1,14 +1,18 @@ import attr from "ember-data/attr"; import Model from "ember-data/model"; +import { name } from "utils/decorators"; -export default Model.extend({ - x: attr( "number", { defaultValue: null } ), - y: attr( "number", { defaultValue: null } ), - width: attr( "number", { defaultValue: null } ), - height: attr( "number", { defaultValue: null } ), - maximized: attr( "boolean", { defaultValue: false } ) - -}).reopenClass({ - toString() { return "Window"; } -}); +@name( "Window" ) +export default class Window extends Model { + @attr( "number", { defaultValue: null } ) + x; + @attr( "number", { defaultValue: null } ) + y; + @attr( "number", { defaultValue: null } ) + width; + @attr( "number", { defaultValue: null } ) + height; + @attr( "boolean", { defaultValue: false } ) + maximized; +} diff --git a/src/app/services/auth/service.js b/src/app/services/auth/service.js index f86632dee5..231c48779f 100644 --- a/src/app/services/auth/service.js +++ b/src/app/services/auth/service.js @@ -24,26 +24,27 @@ const { const reToken = /^[a-z\d]{30}$/i; -export default Service.extend( Evented, /** @class AuthService */ { +export default class AuthService extends Service.extend( Evented ) { /** @type {NwjsService} */ - nwjs: service(), + @service nwjs; /** @type {DS.Store} */ - store: service(), + @service store; /** @type {Auth} */ - session: null, + session = null; /** @type {HttpServer} */ - server: null, + server = null; - url: computed(function() { + @computed() + get url() { const redirect = redirecturi.replace( "{server-port}", String( serverport ) ); return baseuri .replace( "{client-id}", clientid ) .replace( "{redirect-uri}", encodeURIComponent( redirect ) ) .replace( "{scope}", expectedScopes.join( "+" ) ); - }), + } async autoLogin() { @@ -63,13 +64,13 @@ export default Service.extend( Evented, /** @class AuthService */ { // then perform auto-login afterwards await this.login( access_token, true ) .catch( /* istanbul ignore next */ () => {} ); - }, + } async _loadSession() { /** @type {Auth} */ const session = await this.store.findOrCreateRecord( "auth" ); set( this, "session", session ); - }, + } /** @@ -79,7 +80,7 @@ export default Service.extend( Evented, /** @class AuthService */ { async signout() { this._updateAdapter( null ); await this._sessionReset(); - }, + } /** * Open OAuth url in browser @@ -128,14 +129,14 @@ export default Service.extend( Evented, /** @class AuthService */ { this.abortSignin(); this.nwjs.focus( true ); }); - }, + } abortSignin() { const { server } = this; if ( !server ) { return; } server.close(); set( this, "server", null ); - }, + } /** * Validate the OAuth response after a login attempt @@ -150,7 +151,7 @@ export default Service.extend( Evented, /** @class AuthService */ { } return await this.login( token, false ); - }, + } /** * Update the adapter and try to authenticate with the given access token @@ -190,7 +191,7 @@ export default Service.extend( Evented, /** @class AuthService */ { set( session, "isPending", false ); this.trigger( "login", success ); } - }, + } /** * Adapter was updated. Now check if the access token is valid. @@ -206,7 +207,7 @@ export default Service.extend( Evented, /** @class AuthService */ { } return twitchRoot; - }, + } /** * Received and expected scopes need to be identical @@ -216,7 +217,7 @@ export default Service.extend( Evented, /** @class AuthService */ { _validateScope( returnedScopes ) { return Array.isArray( returnedScopes ) && expectedScopes.every( item => returnedScopes.includes( item ) ); - }, + } /** @@ -232,7 +233,7 @@ export default Service.extend( Evented, /** @class AuthService */ { setProperties( session, { access_token, scope, date } ); await session.save(); - }, + } /** * Clear auth record and save it @@ -249,7 +250,7 @@ export default Service.extend( Evented, /** @class AuthService */ { }); await session.save(); - }, + } /** * @param {string} token @@ -258,4 +259,4 @@ export default Service.extend( Evented, /** @class AuthService */ { const adapter = this.store.adapterFor( "twitch" ); set( adapter, "access_token", token ); } -}); +} diff --git a/src/app/services/chat/service.js b/src/app/services/chat/service.js index 4cc6df289e..3dc332705b 100644 --- a/src/app/services/chat/service.js +++ b/src/app/services/chat/service.js @@ -1,6 +1,5 @@ -import { get, getProperties } from "@ember/object"; -import { on } from "@ember/object/evented"; import { default as Service, inject as service } from "@ember/service"; +import { on } from "@ember-decorators/object"; import { chat as chatConfig } from "config"; import providers from "./providers"; import { logDebug, logError } from "./logger"; @@ -15,48 +14,51 @@ export const providerInstanceMap = new Map(); export const providerSetupMap = new Map(); -export default Service.extend({ - auth: service(), - settings: service(), +export default class ChatService extends Service { + /** @type {AuthService} */ + @service auth; + /** @type {SettingsService} */ + @service settings; - _resetProviders: on( "init", function() { - const settingsService = get( this, "settings" ); - settingsService.on( "didUpdate", () => { + @on( "init" ) + _resetProviders() { + this.settings.on( "didUpdate", () => { providerInstanceMap.clear(); providerSetupMap.clear(); }); - }), + } + /** + * @param {TwitchChannel} twitchChannel + * @returns {Promise} + */ async openChat( twitchChannel ) { - /** @type {{name: string}} */ const channelData = twitchChannel.toJSON(); - const session = get( this, "auth.session" ); - /** @type {Object} */ - const sessionData = getProperties( session, "access_token", "user_name", "isLoggedIn" ); + const { access_token, user_name, isLoggedIn } = this.auth.session.toJSON(); await logDebug( "Preparing to launch chat", { channel: channelData.name, - user: sessionData.user_name + user: user_name }); try { /** @type {ChatProvider} */ const provider = await this._getChatProvider(); - await provider.launch( channelData, sessionData ); + await provider.launch( channelData, { access_token, user_name, isLoggedIn } ); } catch ( error ) { await logError( error ); throw error; } - }, + } async _getChatProvider() { - const provider = get( this, "settings.chat.provider" ); + const provider = this.settings.content.chat.provider; if ( !hasOwnProperty.call( providers, provider ) ) { throw new Error( `Invalid provider: ${provider}` ); } - const providersUserData = get( this, "settings.chat.providers" ).toJSON(); + const providersUserData = this.settings.content.chat.providers.toJSON(); if ( !hasOwnProperty.call( providersUserData, provider ) ) { throw new Error( `Missing chat provider settings: ${provider}` ); } @@ -92,4 +94,4 @@ export default Service.extend({ return inst; } -}); +} diff --git a/src/app/services/hotkey.js b/src/app/services/hotkey.js index d64ccff282..ef391ced42 100644 --- a/src/app/services/hotkey.js +++ b/src/app/services/hotkey.js @@ -219,20 +219,24 @@ class HotkeyRegistry { } -/** - * @class HotkeyService - */ -export default Service.extend({ +export default class HotkeyService extends Service { /** @type {I18nService} */ - i18n: service(), + @service i18n; /** @type {SettingsService} */ - settings: service(), + @service settings; + + /** @type {HotkeysMap} */ + hotkeys = new Map(); + + /** @type {HotkeyRegistry[]} */ + registries = []; + + /** @type {KeyboardLayoutMap} */ + layoutMap = new Map(); init() { - this._super( ...arguments ); + super.init( ...arguments ); - this.registries = []; - this.hotkeys = new Map(); hotkeysMapBuild( this.hotkeys ); const hotkeysMapUpdate = () => { const userData = this.settings.content && this.settings.content.hotkeys; @@ -264,16 +268,7 @@ export default Service.extend({ const resetLayoutMap = async () => this.layoutMap = await navigator.keyboard.getLayoutMap(); //navigator.keyboard.addEventListener( "layoutchange", resetLayoutMap ); this.settings.on( "didUpdate", resetLayoutMap ); - }, - - /** @type {HotkeysMap} */ - hotkeys: null, - - /** @type {HotkeyRegistry[]} */ - registries: null, - - /** @type {KeyboardLayoutMap} */ - layoutMap: null, + } /** * Register hotkeys of a component @@ -304,7 +299,7 @@ export default Service.extend({ } this.registries.unshift( ...registries ); - }, + } /** * Remove all hotkeys registered by a component @@ -319,7 +314,7 @@ export default Service.extend({ l--; } } - }, + } /** * Find a registered hotkey that matches and execute the action of the one added last @@ -345,7 +340,7 @@ export default Service.extend({ break; } } - }, + } /** * @param {string} namespace @@ -358,7 +353,7 @@ export default Service.extend({ return hotkeys.has( id ) && hotkeys.get( id ).find( hotkey => !hotkey.disabled && hotkey.code !== null ); - }, + } /** * @param {HotkeyComponent} context @@ -372,7 +367,7 @@ export default Service.extend({ return hotkey; } } - }, + } /** * @param {Hotkey} hotkey @@ -406,4 +401,4 @@ export default Service.extend({ ? `[${str}] ${title}` : str; } -}); +} diff --git a/src/app/services/i18n/service.js b/src/app/services/i18n/service.js index f3208617ab..469de8c717 100644 --- a/src/app/services/i18n/service.js +++ b/src/app/services/i18n/service.js @@ -1,6 +1,7 @@ -import { get, set, observer } from "@ember/object"; +import { set } from "@ember/object"; import { inject as service } from "@ember/service"; -import { Service } from "ember-i18n/addon"; +import { observes, on } from "@ember-decorators/object"; +import { Service as OriginalI18nService } from "ember-i18n/addon"; import { locales as localesConfig } from "config"; import systemLocale from "./system-locale"; @@ -9,22 +10,32 @@ const { locales } = localesConfig; const { hasOwnProperty } = {}; -export default Service.extend({ - settings: service(), +export default class I18nService extends OriginalI18nService { + /** @type {SettingsService} */ + @service settings; - _settingsObserver: observer( "settings.content.gui.language", function() { - let locale = get( this, "settings.content.gui.language" ); + + @on( "init" ) + _initSettings() { + return this.settings; + } + + @observes( "settings.content.gui.language" ) + _languageObserver() { + let locale = this.settings.content.gui.language; if ( locale === "auto" || !locales || !hasOwnProperty.call( locales, locale ) ) { locale = systemLocale /* istanbul ignore next */ || "en"; } set( this, "locale", locale ); - }), - - init() { - this._super( ...arguments ); + } - // the observer doesn't trigger without reading the settings property first - get( this, "settings" ); + /** + * @param {string} key + * @param {Object?} data + * @returns {Handlebars.SafeString} + */ + t( key, data = {} ) { + return super.t( key, data ); } -}); +} diff --git a/src/app/services/modal.js b/src/app/services/modal.js index 3ac7f828cc..5147b9a33c 100644 --- a/src/app/services/modal.js +++ b/src/app/services/modal.js @@ -1,6 +1,6 @@ import { A } from "@ember/array"; import { getOwner } from "@ember/application"; -import { set, setProperties } from "@ember/object"; +import { setProperties } from "@ember/object"; import { notEmpty } from "@ember/object/computed"; import Evented from "@ember/object/evented"; import Service from "@ember/service"; @@ -14,18 +14,12 @@ import Service from "@ember/service"; */ -/** */ -export default Service.extend( Evented, /** @class ModalService */ { +export default class ModalService extends Service.extend( Evented ) { /** @type {ModalServiceEntry[]} */ - modals: null, - - isModalOpened: notEmpty( "modals" ), - - init() { - this._super( ...arguments ); - set( this, "modals", A() ); - }, + modals = A(); + @notEmpty( "modals" ) + isModalOpened; /** * @param {string} name @@ -77,7 +71,7 @@ export default Service.extend( Evented, /** @class ModalService */ { } return context; - }, + } /** * @param {(Object|null)} context @@ -98,7 +92,7 @@ export default Service.extend( Evented, /** @class ModalService */ { this.trigger( "close", n, c ); } } - }, + } promiseModal( name, context, ...args ) { return new Promise( resolve => { @@ -110,7 +104,7 @@ export default Service.extend( Evented, /** @class ModalService */ { this.on( "close", onClose ); context = this.openModal( name, context, ...args ); }); - }, + } /** * @param {string?} name @@ -125,4 +119,4 @@ export default Service.extend( Evented, /** @class ModalService */ { || !context && n === name ); } -}); +} diff --git a/src/app/services/nwjs.js b/src/app/services/nwjs.js index 017828dacd..86237d8ad6 100644 --- a/src/app/services/nwjs.js +++ b/src/app/services/nwjs.js @@ -1,5 +1,5 @@ import { getOwner } from "@ember/application"; -import { get, computed } from "@ember/object"; +import { computed } from "@ember/object"; import { default as Service, inject as service } from "@ember/service"; import { quit } from "nwjs/App"; import { Clipboard, Shell } from "nwjs/nwGui"; @@ -18,29 +18,34 @@ const { hasOwnProperty } = {}; const reVariable = /{(\w+)}/g; -export default Service.extend( /** @class NwjsService */ { - modal: service(), - settings: service(), - streaming: service(), +export default class NwjsService extends Service { + /** @type {ModalService} */ + @service modal; + /** @type {SettingsService} */ + @service settings; + /** @type {StreamingService} */ + @service streaming; /** @type {NWJS_Helpers.clip} */ - clipboard: computed(function() { + @computed() + get clipboard() { return Clipboard.get(); - }), + } - tray: computed(function() { + @computed() + get tray() { return getOwner( this ).lookup( "nwjs:tray" ); - }), + } reload() { nwWindow.reloadIgnoringCache(); - }, + } devTools() { nwWindow.showDevTools(); - }, + } /** * @param {string} url @@ -62,7 +67,7 @@ export default Service.extend( /** @class NwjsService */ { }); Shell.openExternal( url ); - }, + } minimize() { const { integration, minimizetotray } = this.settings.content.gui; @@ -78,31 +83,32 @@ export default Service.extend( /** @class NwjsService */ { } else { toggleMinimized(); } - }, + } maximize() { toggleMaximized(); - }, + } focus( focus = true ) { setFocused( focus ); - }, + } close() { - const streams = get( this, "streaming.model" ).toArray(); - if ( streams.length && streams.some( stream => !get( stream, "hasEnded" ) ) ) { - get( this, "modal" ).openModal( "quit", this ); + /** @type {Stream[]} */ + const streams = this.streaming.model.toArray(); + if ( streams.length && streams.some( stream => !stream.hasEnded ) ) { + this.modal.openModal( "quit", this ); } else { this.quit(); } - }, + } quit() { quit(); - }, + } setShowInTray( visible, removeOnClick ) { - const tray = get( this, "tray" ); + const tray = this.tray; if ( visible ) { tray._createTray(); if ( removeOnClick ) { @@ -111,7 +117,7 @@ export default Service.extend( /** @class NwjsService */ { } else { tray._removeTray(); } - }, + } contextMenu( event, items ) { event.preventDefault(); @@ -120,19 +126,19 @@ export default Service.extend( /** @class NwjsService */ { const menu = getOwner( this ).lookup( "nwjs:menu" ); menu.items.pushObjects( items ); menu.menu.popup( event.x, event.y ); - }, + } addTrayMenuItem( item, position ) { - const tray = get( this, "tray" ); + const tray = this.tray; if ( position === undefined ) { tray.menu.items.unshiftObject( item ); } else { tray.menu.items.insertAt( position, item ); } - }, + } removeTrayMenuItem( item ) { - const tray = get( this, "tray" ); + const tray = this.tray; tray.menu.items.removeObject( item ); } -}); +} diff --git a/src/app/services/settings.js b/src/app/services/settings.js index 362df442e3..cd8e14867b 100644 --- a/src/app/services/settings.js +++ b/src/app/services/settings.js @@ -1,26 +1,27 @@ -import { get, set } from "@ember/object"; +import { set } from "@ember/object"; import Evented from "@ember/object/evented"; import ObjectProxy from "@ember/object/proxy"; import { inject as service } from "@ember/service"; +import { on } from "@ember-decorators/object"; // A service object is just a regular object, so we can use an ObjectProxy as well -export default ObjectProxy.extend( Evented, { - store: service(), +export default class SettingsService extends ObjectProxy.extend( Evented ) { + static isServiceFactory = true; - content: null, + /** @type {DS.Store} */ + @service store; - init() { - const store = get( this, "store" ); - // don't use async functions here and use Ember RSVP promises instead - store.findOrCreateRecord( "settings" ) - .then( settings => { - set( this, "content", settings ); - settings.on( "didUpdate", ( ...args ) => this.trigger( "didUpdate", ...args ) ); - this.trigger( "initialized" ); - }); - } + /** @type {Settings} */ + content = null; + + @on( "init" ) + async _initContent() { + /** @type {Settings} */ + const settings = await this.store.findOrCreateRecord( "settings" ); + set( this, "content", settings ); -}).reopenClass({ - isServiceFactory: true -}); + settings.on( "didUpdate", ( ...args ) => this.trigger( "didUpdate", ...args ) ); + this.trigger( "initialized" ); + } +} diff --git a/src/app/services/streaming/errors.js b/src/app/services/streaming/errors.js index 00fc092c6e..cf33d5ea7e 100644 --- a/src/app/services/streaming/errors.js +++ b/src/app/services/streaming/errors.js @@ -28,28 +28,36 @@ export class ProviderError extends Error { } } -export class PlayerError extends Error {} -PlayerError.regex = [ - /^error: Failed to start player: /, - /^error: The default player \(.+\) does not seem to be installed\./ -]; +export class PlayerError extends Error { + static regex = [ + /^error: Failed to start player: /, + /^error: The default player \(.+\) does not seem to be installed\./ + ]; +} -export class UnableToOpenError extends Error {} -UnableToOpenError.regex = [ - /^error: Unable to open URL: / -]; +export class UnableToOpenError extends Error { + static regex = [ + /^error: Unable to open URL: / + ]; +} -export class NoStreamsFoundError extends Error {} -NoStreamsFoundError.regex = [ - /^error: No streams found on this URL: / -]; +export class NoStreamsFoundError extends Error { + static regex = [ + /^error: No streams found on this URL: / + ]; +} -export class TimeoutError extends Error {} -TimeoutError.regex = [ - /^error: Error when reading from stream: Read timeout, exiting$/ -]; +export class TimeoutError extends Error { + static regex = [ + /^error: Error when reading from stream: Read timeout, exiting$/ + ]; +} export class HostingError extends Error { + static regex = [ + /^\S+ is hosting (\S+)$/ + ]; + constructor( message, channel ) { super( message ); if ( channel ) { @@ -57,11 +65,9 @@ export class HostingError extends Error { } } } -HostingError.regex = [ - /^\S+ is hosting (\S+)$/ -]; -export class Warning extends Error {} -Warning.regex = [ - /InsecurePlatformWarning: A true SSLContext object is not available\./ -]; +export class Warning extends Error { + static regex = [ + /InsecurePlatformWarning: A true SSLContext object is not available\./ + ]; +} diff --git a/src/app/services/streaming/is-aborted.js b/src/app/services/streaming/is-aborted.js index cd97089969..c936a209e2 100644 --- a/src/app/services/streaming/is-aborted.js +++ b/src/app/services/streaming/is-aborted.js @@ -1,11 +1,10 @@ -import { get } from "@ember/object"; import { Aborted } from "./errors"; export default function( stream ) { - if ( get( stream, "isAborted" ) ) { + if ( stream.isAborted ) { // remove the record from the store - if ( !get( stream, "isDeleted" ) ) { + if ( !stream.isDeleted ) { stream.destroyRecord(); } diff --git a/src/app/services/streaming/service.js b/src/app/services/streaming/service.js index 5a3559157b..cd2db692d4 100644 --- a/src/app/services/streaming/service.js +++ b/src/app/services/streaming/service.js @@ -25,30 +25,33 @@ function setIfNotNull( objA, keyA, objB, keyB ) { } -export default Service.extend( /** @class StreamingService */ { +export default class StreamingService extends Service { /** @type {ChatService} */ - chat: service(), + @service chat; /** @type {ModalService} */ - modal: service(), + @service modal; /** @type {SettingsService} */ - settings: service(), + @service settings; /** @type {DS.Store} */ - store: service(), + @service store; + /**@type {(Stream|null)} */ + active = null; - /** @type {DS.RecordArray} */ - model: computed(function() { + /** @returns {DS.RecordArray} */ + @computed() + get model() { return this.store.peekAll( "stream" ); - }), + } init() { - this._super( ...arguments ); + super.init( ...arguments ); // invalidate cache: listen for all settings changes // changed properties of model relationships and nested attributes don't trigger isDirty this.settings.on( "didUpdate", clearCache ); - }, + } /** @@ -69,7 +72,7 @@ export default Service.extend( /** @class StreamingService */ { } return true; - }, + } /** * @param {TwitchStream} twitchStream @@ -77,8 +80,8 @@ export default Service.extend( /** @class StreamingService */ { * @return {Promise} */ async startStream( twitchStream, quality ) { - const { /** @type {DS.Store} */ store } = this; - const { /** @type {TwitchChannel} */ channel } = twitchStream; + const { store } = this; + const { channel } = twitchStream; const { name: id } = channel; /** @type {Stream} */ let stream; @@ -121,7 +124,7 @@ export default Service.extend( /** @class StreamingService */ { await this.getChannelSettings( stream, quality ); await this.launchStream( stream, true ); - }, + } /** * @param {Stream} stream @@ -166,7 +169,7 @@ export default Service.extend( /** @class StreamingService */ { } finally { await this.onStreamEnd( stream ); } - }, + } /** @@ -204,7 +207,7 @@ export default Service.extend( /** @class StreamingService */ { // hide the GUI this.minimize( false ); - }, + } /** * @param {Stream} stream @@ -222,7 +225,7 @@ export default Service.extend( /** @class StreamingService */ { // show error in modal set( stream, "error", error ); - }, + } /** * @param {Stream} stream @@ -245,7 +248,7 @@ export default Service.extend( /** @class StreamingService */ { // restore the GUI this.minimize( true ); - }, + } /** @@ -268,12 +271,12 @@ export default Service.extend( /** @class StreamingService */ { setIfNotNull( stream, "low_latency", channelSettings, "streaming_low_latency" ); setIfNotNull( stream, "disable_ads", channelSettings, "streaming_disable_ads" ); setIfNotNull( stream, "chat_open", channelSettings, "streams_chat_open" ); - }, + } killAll() { this.model.slice().forEach( stream => stream.kill() ); - }, + } minimize( restore ) { switch ( this.settings.content.gui.minimize ) { @@ -289,7 +292,7 @@ export default Service.extend( /** @class StreamingService */ { } break; } - }, + } /** * @param {Stream} stream @@ -312,4 +315,4 @@ export default Service.extend( /** @class StreamingService */ { setTimeout( () => this.refreshStream( stream ), streamReloadInterval ); }); } -}); +} diff --git a/src/app/services/theme.js b/src/app/services/theme.js index 6edc0b33e6..ff6361f586 100644 --- a/src/app/services/theme.js +++ b/src/app/services/theme.js @@ -1,21 +1,24 @@ -import { set, observer } from "@ember/object"; +import { set } from "@ember/object"; import { default as Service, inject as service } from "@ember/service"; +import { observes } from "@ember-decorators/object"; import { themes as themesConfig } from "config"; const { themes, system: systemThemes, prefix } = themesConfig; -export default Service.extend({ - document: service( "-document" ), - settings: service(), +export default class ThemeService extends Service { + /** @type {Document} */ + @service( "-document" ) document; + /** @type {SettingsService} */ + @service settings; - systemTheme: null, + systemTheme = null; initialize() { // calling this in `init` won't trigger the observer of systemTheme when it gets set this._checkSystemColorScheme(); - }, + } /** * Query registered color schemes until one matches and update the systemTheme property. @@ -38,12 +41,13 @@ export default Service.extend({ } // use the default theme if none actually matches (probably unnecessary) set( this, "systemTheme", systemThemes[ "no-preference" ] ); - }, + } /** * Apply theme class name to the documentElement */ - _applyTheme: observer( "settings.content.gui.theme", "systemTheme", function() { + @observes( "settings.content.gui.theme", "systemTheme" ) + _applyTheme() { let theme = this.settings.content.gui.theme; if ( !theme || !themes.includes( theme ) ) { @@ -60,5 +64,5 @@ export default Service.extend({ } }); classList.add( `${prefix}${theme}` ); - }) -}); + } +} diff --git a/src/app/services/versioncheck.js b/src/app/services/versioncheck.js index 75f80f3f4a..9a31f44a12 100644 --- a/src/app/services/versioncheck.js +++ b/src/app/services/versioncheck.js @@ -13,32 +13,32 @@ const { "check-again": checkAgain, "show-debug-message": showDebugMessage } = up const { version } = manifest; -export default Service.extend( /** @class VersioncheckService */ { +export default class VersioncheckService extends Service { /** @type {ModalService} */ - modal: service(), + @service modal; /** @type {DS.Store} */ - store: service(), + @service store; - version, + version = version; /** @type {Versioncheck} */ - model: null, + model = null; /** @type {GithubReleases} */ - release: null, + release = null; async check() { const existinguser = await this._getRecord(); await this._showDebugMessage(); await this._check( existinguser ); - }, + } async ignoreRelease() { const { model } = this; set( model, "checkagain", Date.now() + checkAgain ); await model.save(); - }, + } async _getRecord() { @@ -68,7 +68,7 @@ export default Service.extend( /** @class VersioncheckService */ { set( this, "model", record ); return existinguser; - }, + } async _showDebugMessage() { if ( !isDebug || isDevelopment ) { return; } @@ -79,7 +79,7 @@ export default Service.extend( /** @class VersioncheckService */ { await this.modal.promiseModal( "debug", { buildVersion, displayName }, null, 1000 ); set( model, "showdebugmessage", Date.now() + showDebugMessage ); await model.save(); - }, + } /** * @param {boolean} existinguser @@ -109,12 +109,12 @@ export default Service.extend( /** @class VersioncheckService */ { // go on with new version check if no modal was opened await this._checkForNewRelease(); - }, + } _openModalAndCheckForNewRelease( name ) { this.modal.promiseModal( name, this ) .then( () => this._checkForNewRelease() ); - }, + } async _checkForNewRelease() { // don't check for new releases if disabled or re-check threshold not yet reached @@ -130,4 +130,4 @@ export default Service.extend( /** @class VersioncheckService */ { this.modal.openModal( "newrelease", this ); } -}); +} diff --git a/src/app/ui/components/flag-icon/component.js b/src/app/ui/components/flag-icon/component.js index b346ce3d63..6e421afa55 100644 --- a/src/app/ui/components/flag-icon/component.js +++ b/src/app/ui/components/flag-icon/component.js @@ -1,32 +1,37 @@ import Component from "@ember/component"; import { computed } from "@ember/object"; import { inject as service } from "@ember/service"; -import { langs } from "config"; +import { attribute, className, classNames, tagName } from "@ember-decorators/component"; +import { langs as langsConfig } from "config"; import "./styles.less"; -export default Component.extend({ - i18n: service(), +@tagName( "i" ) +@classNames( "flag-icon-component" ) +export default class FlagIconComponent extends Component { + /** @type {I18nService} */ + @service i18n; - tagName: "i", - classNames: [ "flag-icon-component" ], - classNameBindings: [ "flag", "withCursor::no-cursor" ], - attributeBindings: [ "title" ], + lang; + type; + withTitle = true; - lang: null, - type: null, - withTitle : true, - withCursor: true, + @className( "", "no-cursor" ) + withCursor = true; - flag: computed( "lang", function() { - const code = langs[ this.lang ]; + @className + @computed( "lang" ) + get flag() { + const code = langsConfig[ this.lang ]; return code ? `flag-${code.flag}` : null; - }), + } - title: computed( "withTitle", "lang", function() { + @attribute + @computed( "withTitle", "lang", "i18n.locale" ) + get title() { if ( !this.withTitle ) { return ""; } @@ -42,5 +47,5 @@ export default Component.extend({ return type === "channel" ? i18n.t( "components.flag-icon.channel", { lang } ).toString() : i18n.t( "components.flag-icon.broadcaster", { lang } ).toString(); - }) -}); + } +} diff --git a/src/app/ui/components/form/-input-btn/component.js b/src/app/ui/components/form/-input-btn/component.js index ba4d01958a..fc1609d993 100644 --- a/src/app/ui/components/form/-input-btn/component.js +++ b/src/app/ui/components/form/-input-btn/component.js @@ -2,21 +2,35 @@ import Component from "@ember/component"; import { set, computed } from "@ember/object"; import { or } from "@ember/object/computed"; import { scheduleOnce } from "@ember/runloop"; +import { attribute, className, classNames, layout, tagName } from "@ember-decorators/component"; import isFocused from "utils/is-focused"; -import layout from "./template.hbs"; +import template from "./template.hbs"; import "./styles.less"; -export default Component.extend({ - layout, +@layout( template ) +@tagName( "label" ) +@classNames( "input-btn-component" ) +export default class InputBtnComponent extends Component { + static positionalParams = [ "label" ]; - tagName: "label", - classNames: [ "input-btn-component" ], - classNameBindings: [ "checked", "disabled", "_blockOrLabel::no-label" ], - attributeBindings: [ "tabindex", "title" ], - tabindex: 0, + label; - _blockOrLabel: or( "hasBlock", "label" ), + @attribute + tabindex = 0; + + @attribute + title; + + @className + checked = false; + + @className + disabled = false; + + @or( "hasBlock", "label" ) + @className( "", "no-label" ) + _blockOrLabel; /** * Super dirty hack!!! @@ -25,11 +39,12 @@ export default Component.extend({ * use this computed property which (lazily) sets a (different) hasBlock property here * this computed property will be called in the template's hasBlock block */ - _setHasBlock: computed(function() { + @computed(function() { scheduleOnce( "afterRender", () => { set( this, "hasBlock", true ); }); - }), + }) + _setHasBlock; /** * @param {KeyboardEvent} event @@ -51,7 +66,4 @@ export default Component.extend({ return; } } - -}).reopenClass({ - positionalParams: [ "label" ] -}); +} diff --git a/src/app/ui/components/form/-selectable/component.js b/src/app/ui/components/form/-selectable/component.js index 6ed79850e5..60f87a0aa0 100644 --- a/src/app/ui/components/form/-selectable/component.js +++ b/src/app/ui/components/form/-selectable/component.js @@ -1,41 +1,43 @@ import Component from "@ember/component"; -import { get, set, observer } from "@ember/object"; +import { set } from "@ember/object"; import { addObserver, removeObserver } from "@ember/object/observers"; +import { observes, on } from "@ember-decorators/object"; -export default Component.extend({ - content: null, - selection: null, - value: null, - optionValuePath: "id", - optionLabelPath: "label", +const { hasOwnProperty } = {}; - _ignoreNextValueChange: false, - _selection: null, - _selectionValuePath: null, - _selectionValueObserver: null, - /** - * Find initial selection by the given value attribute - */ - init() { - this._super( ...arguments ); - this._setSelectionByValue(); - }, +export default class SelectableComponent extends Component { + /** @type {Object[]} */ + content = null; + /** @type {Object} */ + selection = null; + value = null; + optionValuePath = "id"; + optionLabelPath = "label"; + + _ignoreNextValueChange = false; + _selection = null; + _selectionValuePath = null; + /** @type {Function} */ + _selectionValueObserver = null; + /** * Clean up observers and caches on destruction + * Can't use an @on decorator here */ willDestroy() { - this._super( ...arguments ); + super.willDestroy( ...arguments ); this._removeSelectionValueObserver(); - }, + } /** * Watch value attribute and try to find a new selection */ - _valueObserver: observer( "value", function() { + @observes( "value" ) + _valueObserver() { // don't find a new selection if disabled if ( this._ignoreNextValueChange ) { this._ignoreNextValueChange = false; @@ -43,24 +45,25 @@ export default Component.extend({ } this._setSelectionByValue(); - }), + } /** * Reset selection value observer if a new selection has been set */ - _selectionObserver: observer( "selection", function() { + @observes( "selection" ) + _selectionObserver() { this._removeSelectionValueObserver(); this._addSelectionValueObserver(); - }), + } /** * Watch changes being made to the content: * - Try to find a selection if there currently is none * - Unset the selection and value attributes if the selection has been removed from the content */ - _contentObserver: observer( "content.[]", function() { - const content = get( this, "content" ); - const selection = get( this, "selection" ); + @observes( "content.[]" ) + _contentObserver() { + const selection = this.selection; if ( !selection ) { // no selection: check for matching value (in case a new item has been added) @@ -68,29 +71,32 @@ export default Component.extend({ } else { // has the current selection been removed from the content list? - if ( !content.includes( selection ) ) { + if ( !this.content.includes( selection ) ) { set( this, "selection", null ); this._ignoreNextValueChange = true; set( this, "value", null ); } } - }), + } /** * Find selection by value and update selection attribute * Will trigger the selection observer if a new selection has been found */ + @on( "init" ) _setSelectionByValue() { - const content = get( this, "content" ); - const value = get( this, "value" ); - const optionValuePath = get( this, "optionValuePath" ); - const selection = content.findBy( optionValuePath, value ); + const value = this.value; + const optionValuePath = this.optionValuePath; + const selection = this.content.find( item => + hasOwnProperty.call( item, optionValuePath ) + && item[ optionValuePath ] === value + ); if ( selection !== undefined ) { set( this, "selection", selection ); } - }, + } /** * Remove old selection value observer @@ -105,18 +111,18 @@ export default Component.extend({ this._selectionValueObserver ); this._selection = this._selectionValuePath = this._selectionValueObserver = null; - }, + } /** * Add new selection value observer and execute it to update value attribute */ _addSelectionValueObserver() { - const selection = get( this, "selection" ); + const selection = this.selection; if ( !selection ) { return; } - const optionValuePath = get( this, "optionValuePath" ); + const { optionValuePath } = this; const selectionValueObserver = () => { - const value = get( selection, optionValuePath ); + const value = selection[ optionValuePath ]; set( this, "value", value ); }; @@ -133,4 +139,4 @@ export default Component.extend({ this._selectionValueObserver(); } -}); +} diff --git a/src/app/ui/components/form/check-box/component.js b/src/app/ui/components/form/check-box/component.js index eddad89a30..ef523dcda4 100644 --- a/src/app/ui/components/form/check-box/component.js +++ b/src/app/ui/components/form/check-box/component.js @@ -1,12 +1,11 @@ -import { get } from "@ember/object"; +import { classNames } from "@ember-decorators/component"; import InputBtnComponent from "../-input-btn/component"; -export default InputBtnComponent.extend({ - classNames: [ "check-box-component" ], - +@classNames( "check-box-component" ) +export default class CheckBoxComponent extends InputBtnComponent { click() { - if ( get( this, "disabled" ) ) { return; } + if ( this.disabled ) { return; } this.toggleProperty( "checked" ); } -}); +} diff --git a/src/app/ui/components/form/drop-down-list/component.js b/src/app/ui/components/form/drop-down-list/component.js index 61fb5f2856..04eaac393b 100644 --- a/src/app/ui/components/form/drop-down-list/component.js +++ b/src/app/ui/components/form/drop-down-list/component.js @@ -1,35 +1,29 @@ import Component from "@ember/component"; -import { get, set, setProperties, observer } from "@ember/object"; +import { set, setProperties, action } from "@ember/object"; import { scheduleOnce } from "@ember/runloop"; -import layout from "./template.hbs"; +import { className, classNames, layout, tagName } from "@ember-decorators/component"; +import { observes, on } from "@ember-decorators/object"; +import template from "./template.hbs"; -export default Component.extend({ - layout, +@layout( template ) +@tagName( "ui" ) +@classNames( "drop-down-list-component" ) +export default class DropDownListComponent extends Component { + @className + class = ""; + @className + expanded = false; + @className( "expanded-upwards" ) + upwards = false; - tagName: "ul", - classNames: [ "drop-down-list-component" ], - classNameBindings: [ - "expanded:expanded", - "upwards:expanded-upwards", - "class" - ], - expanded: false, - upwards: false, - - - willDestroyElement() { - this._removeClickListener(); - this._super( ...arguments ); - }, - - - _expandedObserver: observer( "expanded", function() { + @observes( "expanded" ) + _expandedObserver() { // always remove click listener this._removeClickListener(); - if ( !get( this, "expanded" ) ) { + if ( !this.expanded ) { return; } @@ -47,15 +41,16 @@ export default Component.extend({ } }; this.element.ownerDocument.body.addEventListener( "click", this._clickListener ); - }), + } + @on( "willDestroyElement" ) _removeClickListener() { // unregister click event listener if ( this._clickListener ) { this.element.ownerDocument.body.removeEventListener( "click", this._clickListener ); this._clickListener = null; } - }, + } _calcExpansionDirection() { const element = this.element; @@ -66,16 +61,15 @@ export default Component.extend({ const listHeight = element.offsetHeight + parseInt( marginTop ) + parseInt( marginBottom ); const isOverflowing = parentHeight - positionTop < listHeight; set( this, "upwards", isOverflowing ); - }, + } - actions: { - change( item ) { - if ( get( this, "disabled" ) ) { return; } - setProperties( this, { - expanded: false, - selection: item - }); - } + @action + change( item ) { + if ( this.disabled ) { return; } + setProperties( this, { + expanded: false, + selection: item + }); } -}); +} diff --git a/src/app/ui/components/form/drop-down-selection/component.js b/src/app/ui/components/form/drop-down-selection/component.js index d6c8650ae5..7b56d3e422 100644 --- a/src/app/ui/components/form/drop-down-selection/component.js +++ b/src/app/ui/components/form/drop-down-selection/component.js @@ -1,23 +1,35 @@ import Component from "@ember/component"; -import { get } from "@ember/object"; import { inject as service } from "@ember/service"; -import { translationMacro as t } from "ember-i18n/addon"; -import layout from "./template.hbs"; +import { className, classNames, layout, tagName } from "@ember-decorators/component"; +import { t } from "ember-i18n/decorator"; +import template from "./template.hbs"; -export default Component.extend({ - i18n: service(), +@layout( template ) +@tagName( "div" ) +@classNames( "drop-down-selection-component" ) +export default class DropDownSelectionComponent extends Component { + /** @type {I18nService} */ + @service i18n; - layout, + @className + class = ""; - tagName: "div", - classNames: [ "drop-down-selection-component" ], - classNameBindings: [ "class" ], + @t( "components.drop-down-selection.placeholder" ) + _defaultPlaceholder; - placeholder: t( "components.drop-down-selection.placeholder" ), + _placeholder = null; + + get placeholder() { + return this._placeholder || this._defaultPlaceholder; + } + set placeholder( value ) { + this._placeholder = value; + } click() { - get( this, "action" )(); + this.action(); + return false; } -}); +} diff --git a/src/app/ui/components/form/drop-down/component.js b/src/app/ui/components/form/drop-down/component.js index 87e50b1ca7..3948abe8be 100644 --- a/src/app/ui/components/form/drop-down/component.js +++ b/src/app/ui/components/form/drop-down/component.js @@ -1,25 +1,24 @@ -import { get, set } from "@ember/object"; -import Selectable from "../-selectable/component"; +import { set, action } from "@ember/object"; +import { attribute, className, classNames, layout, tagName } from "@ember-decorators/component"; +import SelectableComponent from "../-selectable/component"; import isFocused from "utils/is-focused"; -import layout from "./template.hbs"; +import template from "./template.hbs"; import "./styles.less"; -export default Selectable.extend({ - layout, +@layout( template ) +@tagName( "div" ) +@classNames( "drop-down-component" ) +export default class DropDownComponent extends SelectableComponent { + @className + class = ""; + @className + disabled = false; - tagName: "div", - classNames: [ "drop-down-component" ], - classNameBindings: [ - "disabled:disabled", - "class" - ], - attributeBindings: [ "tabindex" ], - tabindex: 0, - - disabled: false, - expanded: false, + @attribute + tabindex = 0; + expanded = false; /** * @param {KeyboardEvent} event @@ -28,7 +27,7 @@ export default Selectable.extend({ switch ( event.key ) { case "Escape": case "Backspace": - if ( get( this, "expanded" ) ) { + if ( this.expanded ) { set( this, "expanded", false ); return false; } @@ -51,16 +50,15 @@ export default Selectable.extend({ case "ArrowDown": return this._switchSelectionOnArrowKey( 1 ); } - }, + } _switchSelectionOnArrowKey( change ) { - if ( !get( this, "expanded" ) || !isFocused( this.element ) ) { + if ( !this.expanded || !isFocused( this.element ) ) { return; } - const content = get( this, "content" ); - const selection = get( this, "selection" ); - const selIndex = content.indexOf( selection ); + const content = this.content; + const selIndex = content.indexOf( this.selection ); if ( selIndex === -1 ) { return; } @@ -70,16 +68,15 @@ export default Selectable.extend({ set( this, "selection", newSelection ); return false; - }, + } - actions: { - toggle() { - if ( get( this, "disabled" ) ) { - set( this, "expanded", false ); - } else { - this.toggleProperty( "expanded" ); - } + @action + toggle() { + if ( this.disabled ) { + set( this, "expanded", false ); + } else { + this.toggleProperty( "expanded" ); } } -}); +} diff --git a/src/app/ui/components/form/file-select/component.js b/src/app/ui/components/form/file-select/component.js index e42273ffa9..244f107952 100644 --- a/src/app/ui/components/form/file-select/component.js +++ b/src/app/ui/components/form/file-select/component.js @@ -1,45 +1,54 @@ import Component from "@ember/component"; -import { get, set, computed } from "@ember/object"; -import { on } from "@ember/object/evented"; +import { set, action } from "@ember/object"; import { inject as service } from "@ember/service"; +import { classNames, layout, tagName } from "@ember-decorators/component"; +import { on } from "@ember-decorators/object"; +import { t } from "ember-i18n/decorator"; import { platform } from "utils/node/platform"; -import layout from "./template.hbs"; +import template from "./template.hbs"; const { hasOwnProperty } = {}; const { isArray } = Array; -export default Component.extend({ - i18n: service(), +@layout( template ) +@tagName( "div" ) +@classNames( "file-select-component", "input-group" ) +export default class FileSelectComponent extends Component { + /** {I18nService} */ + @service i18n; - layout, + value = ""; + disabled = false; - tagName: "div", - classNames: [ "input-group" ], + /** @type {HTMLInputElement} */ + _input = null; - value: "", - disabled: false, + @t( "components.file-select.placeholder" ) + _defaultPlaceholder; - placeholder: computed({ - set( key, value ) { - if ( typeof value === "string" ) { - return value; - } + _placeholder = null; - if ( typeof value !== "object" || !hasOwnProperty.call( value, platform ) ) { - return get( this, "i18n" ).t( "components.file-select.placeholder" ).toString(); - } + get placeholder() { + return this._placeholder || this._defaultPlaceholder; + } + set placeholder( value ) { + if ( typeof value === "string" ) { + this._placeholder = value; + } else if ( typeof value === "object" && hasOwnProperty.call( value, platform ) ) { value = value[ platform ]; - return isArray( value ) + this._placeholder = isArray( value ) ? value[ 0 ] : value; } - }), + } - _createInput: on( "didInsertElement", function() { + + @on( "didInsertElement" ) + _createInputElement() { const input = this.element.ownerDocument.createElement( "input" ); input.classList.add( "hidden" ); input.setAttribute( "type", "file" ); @@ -50,13 +59,18 @@ export default Component.extend({ input.files.clear(); }); this._input = input; - }), + } + + @on( "willDestroyElement" ) + _destroyInputElement() { + this._input = null; + } + - actions: { - selectfile() { - if ( !get( this, "disabled" ) ) { - this._input.dispatchEvent( new MouseEvent( "click", { bubbles: true } ) ); - } + @action + selectfile() { + if ( !this.disabled ) { + this._input.dispatchEvent( new MouseEvent( "click", { bubbles: true } ) ); } } -}); +} diff --git a/src/app/ui/components/form/number-field/component.js b/src/app/ui/components/form/number-field/component.js index d8b3459a12..8351cd02e8 100644 --- a/src/app/ui/components/form/number-field/component.js +++ b/src/app/ui/components/form/number-field/component.js @@ -1,7 +1,8 @@ import Component from "@ember/component"; -import { get, set } from "@ember/object"; -import { on } from "@ember/object/evented"; -import layout from "./template.hbs"; +import { set, action } from "@ember/object"; +import { classNames, layout, tagName } from "@ember-decorators/component"; +import { on } from "@ember-decorators/object"; +import template from "./template.hbs"; import "./styles.less"; @@ -9,33 +10,34 @@ const { min, max } = Math; const { isNaN, isInteger, MIN_SAFE_INTEGER, MAX_SAFE_INTEGER } = Number; -export default Component.extend({ - layout, +@layout( template ) +@tagName( "div" ) +@classNames( "number-field-component" ) +export default class NumberFieldComponent extends Component { + /** @type {number|null} */ + value = null; + /** @type {number|null} */ + defaultValue = null; + disabled = false; + min = MIN_SAFE_INTEGER; + max = MAX_SAFE_INTEGER; - tagName: "div", - classNames: [ "number-field-component" ], + _prevValue = null; + _value = null; - value: null, - defaultValue: null, - disabled: false, - min: MIN_SAFE_INTEGER, - max: MAX_SAFE_INTEGER, - _prevValue: null, - _value: null, - - _update: on( "init", "didReceiveAttrs", function() { - this._super( ...arguments ); - const value = get( this, "value" ); + @on( "init", "didReceiveAttrs" ) + _update() { + const value = this.value; const parsedValue = this._parse( value ); this._prevValue = value; set( this, "_value", String( parsedValue ) ); - }), + } - didInsertElement() { - this._super( ...arguments ); + @on( "didInsertElement" ) + _getInputElement() { this._input = this.element.querySelector( "input" ); - }, + } _parse( value ) { let numValue = Number( value ); @@ -43,19 +45,18 @@ export default Component.extend({ // is the new value not a number? if ( isNaN( numValue ) ) { // use previous value if it exists - const prevValue = this._prevValue; + const { _prevValue: prevValue } = this; if ( prevValue !== null && !isNaN( Number( prevValue ) ) ) { return prevValue; } // otherwise, get the default value - const defaultValue = get( this, "defaultValue" ); + const { defaultValue } = this; if ( defaultValue !== null && !isNaN( Number( defaultValue ) ) ) { return defaultValue; } - const min = get( this, "min" ); - const max = get( this, "max" ); + const { min, max } = this; // or the average of min and max values if no default value exists either return ( ( min / 2 ) + ( max / 2 ) ) >> 0; @@ -66,41 +67,37 @@ export default Component.extend({ numValue = numValue >> 0; } - const min = get( this, "min" ); - const max = get( this, "max" ); + const { min, max } = this; // maximum and minimum return numValue <= max ? numValue >= min - ? numValue - : min + ? numValue + : min : max; - }, - - - actions: { - increase() { - if ( get( this, "disabled" ) ) { return; } - const maxValue = get( this, "max" ); - const currentValue = get( this, "value" ); - const value = min( currentValue + 1, maxValue ); - this.attrs.value.update( value ); - }, - - decrease() { - if ( get( this, "disabled" ) ) { return; } - const minValue = get( this, "min" ); - const currentValue = get( this, "value" ); - const value = max( currentValue - 1, minValue ); - this.attrs.value.update( value ); - }, - - blur() { - if ( get( this, "disabled" ) ) { return; } - const inputValue = this._input.value; - const value = this._parse( inputValue ); - this._input.value = String( value ); - this.attrs.value.update( value ); - } } -}); + + + @action + increase() { + if ( this.disabled ) { return; } + const newValue = min( this.value + 1, this.max ); + set( this, "value", newValue ); + } + + @action + decrease() { + if ( this.disabled ) { return; } + const newValue = max( this.value - 1, this.min ); + set( this, "value", newValue ); + } + + @action + blur() { + if ( this.disabled ) { return; } + const inputValue = this._input.value; + const newValue = this._parse( inputValue ); + this._input.value = String( newValue ); + set( this, "value", newValue ); + } +} diff --git a/src/app/ui/components/form/radio-buttons-item/component.js b/src/app/ui/components/form/radio-buttons-item/component.js index 6eb83d104e..5588fa27b4 100644 --- a/src/app/ui/components/form/radio-buttons-item/component.js +++ b/src/app/ui/components/form/radio-buttons-item/component.js @@ -1,12 +1,11 @@ -import { get } from "@ember/object"; +import { classNames } from "@ember-decorators/component"; import InputBtnComponent from "../-input-btn/component"; -export default InputBtnComponent.extend({ - classNames: [ "radio-buttons-item-component" ], - +@classNames( "radio-buttons-item-component" ) +export default class RadioButtonsItemComponent extends InputBtnComponent { click() { - if ( get( this, "disabled" ) ) { return; } - get( this, "action" )(); + if ( this.disabled ) { return; } + this.action(); } -}); +} diff --git a/src/app/ui/components/form/radio-buttons/component.js b/src/app/ui/components/form/radio-buttons/component.js index a2ba75cab8..bdbd1a687a 100644 --- a/src/app/ui/components/form/radio-buttons/component.js +++ b/src/app/ui/components/form/radio-buttons/component.js @@ -1,18 +1,16 @@ -import { set } from "@ember/object"; -import Selectable from "../-selectable/component"; -import layout from "./template.hbs"; +import { set, action } from "@ember/object"; +import { classNames, layout, tagName } from "@ember-decorators/component"; +import SelectableComponent from "../-selectable/component"; +import template from "./template.hbs"; import "./styles.less"; -export default Selectable.extend({ - layout, - - tagName: "div", - classNames: [ "radio-buttons-component" ], - - actions: { - change( item ) { - set( this, "selection", item ); - } +@layout( template ) +@tagName( "div" ) +@classNames( "radio-buttons-component" ) +export default class RadioButtonsComponent extends SelectableComponent { + @action + change( item ) { + set( this, "selection", item ); } -}); +} diff --git a/src/app/ui/components/form/text-field/component.js b/src/app/ui/components/form/text-field/component.js index 8709e8a4f7..8280373e82 100644 --- a/src/app/ui/components/form/text-field/component.js +++ b/src/app/ui/components/form/text-field/component.js @@ -1,18 +1,21 @@ import TextField from "@ember/component/text-field"; import { inject as service } from "@ember/service"; +import { attribute } from "@ember-decorators/component"; import t from "translation-key"; -export default TextField.extend({ +export default class TextFieldComponent extends TextField { /** @type {NwjsService} */ - nwjs: service(), + @service nwjs; - attributeBindings: [ "autoselect:data-selectable" ], + @attribute( "data-selectable" ) + autoselect = false; - autoselect: false, + autofocus = false; + noContextmenu = false; contextMenu( event ) { - if ( this.attrs.noContextmenu ) { return; } + if ( this.noContextmenu ) { return; } const element = this.element; const start = element.selectionStart; @@ -42,13 +45,13 @@ export default TextField.extend({ } } ]); - }, + } focusIn() { - if ( !this.attrs.autofocus || !this.attrs.autoselect ) { return; } + if ( !this.autofocus || !this.autoselect ) { return; } this.element.setSelectionRange( 0, this.element.value.length ); - }, + } /** * @param {KeyboardEvent} event @@ -59,6 +62,6 @@ export default TextField.extend({ return; } - return this._super( ...arguments ); + return super.keyDown( ...arguments ); } -}); +} diff --git a/src/app/ui/components/helper/-from-now.js b/src/app/ui/components/helper/-from-now.js index 34f2f58f71..3e66413d66 100644 --- a/src/app/ui/components/helper/-from-now.js +++ b/src/app/ui/components/helper/-from-now.js @@ -2,14 +2,14 @@ import Helper from "@ember/component/helper"; import { run } from "@ember/runloop"; -export const helper = Helper.extend({ +export const helper = class FromNowHelper extends Helper { compute( params, hash ) { if ( hash.interval ) { this._interval = setTimeout( () => run( () => this.recompute() ), hash.interval ); } return this._compute( ...arguments ); - }, + } destroy() { if ( this._interval ) { @@ -19,4 +19,4 @@ export const helper = Helper.extend({ this._super( ...arguments ); } -}); +}; diff --git a/src/app/ui/components/helper/hotkey-title.js b/src/app/ui/components/helper/hotkey-title.js index 9812c39273..ba48f738c9 100644 --- a/src/app/ui/components/helper/hotkey-title.js +++ b/src/app/ui/components/helper/hotkey-title.js @@ -2,18 +2,18 @@ import Helper from "@ember/component/helper"; import { inject as service } from "@ember/service"; -export const helper = Helper.extend({ +export const helper = class HotkeyTitleHelper extends Helper { /** @type {HotkeyService} */ - hotkey: service(), + @service hotkey; /** @type {I18nService} */ - i18n: service(), + @service i18n; init() { - this._super( ...arguments ); + super.init( ...arguments ); // initialize computed property of injected service to make the observer work this.get( "i18n" ); this.addObserver( "i18n.locale", this, "recompute" ); - }, + } compute( positional, { hotkey, context, namespace, action, title } ) { if ( action ) { @@ -30,4 +30,4 @@ export const helper = Helper.extend({ ? this.hotkey.formatTitle( hotkey, title ) : title; } -}); +}; diff --git a/src/app/ui/components/helper/hours-from-now.js b/src/app/ui/components/helper/hours-from-now.js index 056ee29f22..d5d040c678 100644 --- a/src/app/ui/components/helper/hours-from-now.js +++ b/src/app/ui/components/helper/hours-from-now.js @@ -8,9 +8,11 @@ const hour = 60 * minute; const day = 24 * hour; -export const helper = FromNowHelper.extend({ - i18n: service(), - settings: service(), +export const helper = class HoursFromNowHelper extends FromNowHelper { + /** @type {I18nService} */ + @service i18n; + /** @type {SettingsService} */ + @service settings; _compute( [ time = 0 ] ) { const diff = Date.now() - time; @@ -60,4 +62,4 @@ export const helper = FromNowHelper.extend({ } } } -}); +}; diff --git a/src/app/ui/components/helper/time-from-now.js b/src/app/ui/components/helper/time-from-now.js index f1b4496252..8d68e99fde 100644 --- a/src/app/ui/components/helper/time-from-now.js +++ b/src/app/ui/components/helper/time-from-now.js @@ -2,8 +2,8 @@ import { helper as FromNowHelper } from "./-from-now"; import Moment from "moment"; -export const helper = FromNowHelper.extend({ +export const helper = class TimeFromNowHelper extends FromNowHelper { _compute( params, hash ) { return new Moment( params[0] ).fromNow( hash.suffix || params[1] ); } -}); +}; diff --git a/src/app/ui/components/link/documentation-link/component.js b/src/app/ui/components/link/documentation-link/component.js index 2abc5d4012..07627d1b9d 100644 --- a/src/app/ui/components/link/documentation-link/component.js +++ b/src/app/ui/components/link/documentation-link/component.js @@ -1,54 +1,57 @@ -import { get, computed } from "@ember/object"; +import { computed } from "@ember/object"; import { inject as service } from "@ember/service"; +import { attribute, className, classNames, layout, tagName } from "@ember-decorators/component"; import { streaming as streamingConfig } from "config"; import ExternalLinkComponent from "../external-link/component"; -import layout from "./template.hbs"; +import template from "./template.hbs"; import "./styles.less"; const { "docs-url": docsUrl } = streamingConfig; -export default ExternalLinkComponent.extend({ - i18n: service(), - settings: service(), - - layout, - - // default baseUrl - baseUrl: computed( "settings.streaming.providerType", function() { - const type = get( this, "settings.streaming.providerType" ); - - return docsUrl[ type ]; - }), - - tagName: "span", - classNameBindings: [ - ":documentation-link-component", - "url:with-url", - "class" - ], - attributeBindings: [ - "title" - ], - - class: "", - title: computed( "i18n.locale", "baseUrl", function() { - return get( this, "baseUrl" ) - ? get( this, "i18n" ).t( "components.documentation-link.title" ) +@layout( template ) +@tagName( "span" ) +@classNames( "documentation-link-component" ) +export default class DocumentationLinkComponent extends ExternalLinkComponent { + /** @type {I18nService} */ + @service i18n; + /** @type {SettingsService} */ + @service settings; + + @className + class = ""; + + _baseUrl = null; + + @computed( "settings.content.streaming.providerType" ) + get baseUrl() { + return this._baseUrl !== null + ? this._baseUrl + : docsUrl[ this.settings.content.streaming.providerType ]; + } + set baseUrl( value ) { + this._baseUrl = value; + } + + @attribute + @computed( "i18n.locale", "baseUrl" ) + get title() { + return this.baseUrl + ? this.i18n.t( "components.documentation-link.title" ) : ""; - }), + } - url: computed( "baseUrl", "item", function() { - const baseUrl = get( this, "baseUrl" ); - const item = get( this, "item" ); - let itemUrl = encodeURIComponent( item ); + @className( "with-url" ) + @computed( "baseUrl", "item" ) + get url() { + let itemUrl = encodeURIComponent( this.item ); // remove leading double dash from Streamlink documentation links - if ( get( this, "settings.streaming.isStreamlink" ) ) { + if ( !this._baseUrl && this.settings.content.streaming.isStreamlink ) { itemUrl = itemUrl.replace( /^-/, "" ); } - return baseUrl.replace( "{item}", itemUrl ); - }) -}); + return this.baseUrl.replace( "{item}", itemUrl ); + } +} diff --git a/src/app/ui/components/link/embedded-html-links/component.js b/src/app/ui/components/link/embedded-html-links/component.js index ff17b93138..c0481578fa 100644 --- a/src/app/ui/components/link/embedded-html-links/component.js +++ b/src/app/ui/components/link/embedded-html-links/component.js @@ -1,5 +1,6 @@ import Component from "@ember/component"; import { inject as service } from "@ember/service"; +import { on } from "@ember-decorators/object"; import getStreamFromUrl from "utils/getStreamFromUrl"; import t from "translation-key"; @@ -7,13 +8,14 @@ import t from "translation-key"; const DISABLED_EVENTS = "mousedown mouseup keyup keydown keypress".split( " " ); -export default Component.extend({ +export default class EmbeddedHtmlLinksComponent extends Component { /** @type {NwjsService} */ - nwjs: service(), + @service nwjs; /** @type {RouterService} */ - router: service(), + @service router; - didInsertElement() { + @on( "didInsertElement" ) + _onDidInsertElement() { /** @type {HTMLAnchorElement[]} */ const anchors = Array.from( this.element.querySelectorAll( "a" ) ); for ( const anchor of anchors ) { @@ -63,7 +65,5 @@ export default Component.extend({ }); } } - - return this._super( ...arguments ); } -}); +} diff --git a/src/app/ui/components/link/embedded-links/component.js b/src/app/ui/components/link/embedded-links/component.js index 071f1f5aa4..0d3829e859 100644 --- a/src/app/ui/components/link/embedded-links/component.js +++ b/src/app/ui/components/link/embedded-links/component.js @@ -1,21 +1,20 @@ import Component from "@ember/component"; -import { get, computed } from "@ember/object"; +import { computed } from "@ember/object"; +import { classNames, layout } from "@ember-decorators/component"; import { parseString } from "utils/linkparser"; -import layout from "./template.hbs"; +import template from "./template.hbs"; -export default Component.extend({ - layout, - - classNames: [ "embedded-links-component" ], - - content: computed( "text", function() { - const text = get( this, "text" ); - const parsed = parseString( text ); +@layout( template ) +@classNames( "embedded-links-component" ) +export default class EmbeddedLinksComponent extends Component { + @computed( "text" ) + get content() { + const parsed = parseString( this.text ); const links = parsed.links; // merge texts and links - return parsed.texts.reduce(function( output, textItem, index ) { + return parsed.texts.reduce( ( output, textItem, index ) => { if ( textItem.length ) { output.push({ text: textItem }); } @@ -24,5 +23,5 @@ export default Component.extend({ } return output; }, [] ); - }) -}); + } +} diff --git a/src/app/ui/components/link/external-link/component.js b/src/app/ui/components/link/external-link/component.js index 72666bf1ba..4fba7b1fa9 100644 --- a/src/app/ui/components/link/external-link/component.js +++ b/src/app/ui/components/link/external-link/component.js @@ -1,57 +1,57 @@ import Component from "@ember/component"; -import { get, computed } from "@ember/object"; +import { computed } from "@ember/object"; import { inject as service } from "@ember/service"; +import { attribute, className, classNames, tagName } from "@ember-decorators/component"; import getStreamFromUrl from "utils/getStreamFromUrl"; import t from "translation-key"; -export default Component.extend({ +@tagName( "a" ) +@classNames( "external-link-component" ) +export default class ExternalLinkComponent extends Component { /** @type {NwjsService} */ - nwjs: service(), + @service nwjs; /** @type {RouterService} */ - router: service(), + @service router; - tagName: "a", - classNameBindings: [ - ":external-link-component", - "channel::external-link" - ], - attributeBindings: [ - "href", - "title", - "tabindex" - ], + @attribute + href = "#"; + @attribute + tabindex = -1; - href: "#", - tabindex: -1, - - channel: computed( "url", function() { - const url = get( this, "url" ); - return getStreamFromUrl( url ); - }), + @className( "", "external-link" ) + @computed( "url" ) + get channel() { + return getStreamFromUrl( this.url ); + } - title: computed( "url", "channel", function() { - return get( this, "channel" ) + @attribute + @computed( "url", "channel" ) + get title() { + return this.channel ? null - : get( this, "url" ); - }), + : this.url; + } + /** + * @param {MouseEvent} event + */ click( event ) { event.preventDefault(); event.stopImmediatePropagation(); - const channel = get( this, "channel" ); - if ( channel ) { - this.router.transitionTo( "channel", channel ); - } else { + if ( this.channel ) { + this.router.transitionTo( "channel", this.channel ); + } else if ( this.url ) { this.nwjs.openBrowser( this.url ); } - }, + } + /** + * @param {MouseEvent} event + */ contextMenu( event ) { - if ( get( this, "channel" ) ) { - return; - } + if ( this.channel ) { return; } event.preventDefault(); event.stopImmediatePropagation(); @@ -68,4 +68,4 @@ export default Component.extend({ } ]); } -}); +} diff --git a/src/app/ui/components/link/link-to/component.js b/src/app/ui/components/link/link-to/component.js index 662629de5b..a64e01b089 100644 --- a/src/app/ui/components/link/link-to/component.js +++ b/src/app/ui/components/link/link-to/component.js @@ -1,30 +1,29 @@ import { computed } from "@ember/object"; import LinkComponent from "@ember/routing/link-component"; import { inject as service } from "@ember/service"; +import { attribute } from "@ember-decorators/component"; -export default LinkComponent.extend({ +export default class LinkToComponent extends LinkComponent { /** @type {RouterService} */ - router: service(), + @service router; - tabindex: -1, + @attribute + tabindex = -1; - _active: computed( - "_routing.currentState", - "_routing.currentRouteName", - "attrs.params", - function() { - const currentState = this._routing.currentState; + @computed( "_routing.currentState", "_routing.currentRouteName", "attrs.params" ) + get _active() { + const currentState = this._routing.currentState; - return currentState - && this._isActive( currentState ) - && this._routing.currentRouteName !== "error"; - } - ), + return currentState + && this._isActive( currentState ) + && this._routing.currentRouteName !== "error"; + } - active: computed( "activeClass", "_active", "_routing.currentRouteName", function() { + @computed( "activeClass", "_active", "_routing.currentRouteName" ) + get active() { return this._active ? this.activeClass : false; - }), + } /** * Checks whether the link is active because of an active child route. @@ -48,8 +47,12 @@ export default LinkComponent.extend({ return routeInfosLeafName !== routeName && routeInfosLeafName !== `${routeName}.index` && routeInfosAncestors.find( ({ name }) => name === routeName ); - }, + } + /** + * @param {MouseEvent} e + * @returns {boolean} + */ _invoke( e ) { // prevent new windows from being opened if ( e.buttons & 6 || e.shiftKey || e.ctrlKey || e.altKey || e.metaKey ) { @@ -60,13 +63,14 @@ export default LinkComponent.extend({ // perform default action if link is inactive // or if it is active, but only because of an active child route if ( !this._active || this._isActiveAncestor() ) { - return this._super( ...arguments ); + return super._invoke( ...arguments ); } // enable route refreshing by clicking on the same link again e.preventDefault(); e.stopImmediatePropagation(); this.router.refresh(); + return false; } -}); +} diff --git a/src/app/ui/components/list/stream-item/component.js b/src/app/ui/components/list/stream-item/component.js index 7993ea8231..fa1228db74 100644 --- a/src/app/ui/components/list/stream-item/component.js +++ b/src/app/ui/components/list/stream-item/component.js @@ -32,6 +32,7 @@ export default ListItemComponent.extend({ expanded: false, locked : false, timer : null, + ignoreLanguageFading: false, showGame: notEmpty( "channel.game" ), @@ -44,11 +45,16 @@ export default ListItemComponent.extend({ fadedVodcast: and( "content.isVodcast", "settings.content.streams.filter_vodcast" ), faded: computed( + "ignoreLanguageFading", "settings.content.streams.filter_languages", "settings.content.streams.language", "channel.language", "channel.broadcaster_language", function() { + if ( this.ignoreLanguageFading ) { + return false; + } + const { filter_languages, language } = this.settings.content.streams; if ( filter_languages !== ATTR_FILTER_LANGUAGES_FADE ) { diff --git a/src/app/ui/components/loading-spinner/component.js b/src/app/ui/components/loading-spinner/component.js index 8b888ac0fa..6f90e9b307 100644 --- a/src/app/ui/components/loading-spinner/component.js +++ b/src/app/ui/components/loading-spinner/component.js @@ -1,22 +1,22 @@ import Component from "@ember/component"; -import { on } from "@ember/object/evented"; -import layout from "./template.hbs"; +import { attribute, classNames, layout, tagName } from "@ember-decorators/component"; +import { on } from "@ember-decorators/object"; +import template from "./template.hbs"; import "./styles.less"; -export default Component.extend({ - layout, +@layout( template ) +@tagName( "svg" ) +@classNames( "loading-spinner-component" ) +export default class LoadingSpinnerComponent extends Component { + @attribute + viewBox = "0 0 1 1"; - tagName: "svg", - attributeBindings: [ "viewBox" ], - classNames: [ "loading-spinner-component" ], - - viewBox: "0 0 1 1", - - _setRadiusAttribute: on( "didInsertElement", function() { + @on( "didInsertElement" ) + _setRadiusAttribute() { let circle = this.element.querySelector( "circle" ); let strokeWidth = window.getComputedStyle( circle ).strokeWidth; let radius = 50 - parseFloat( strokeWidth ); circle.setAttribute( "r", `${radius}%` ); - }) -}); + } +} diff --git a/src/app/ui/components/main-menu/component.js b/src/app/ui/components/main-menu/component.js index 208310e0a5..0b378830f2 100644 --- a/src/app/ui/components/main-menu/component.js +++ b/src/app/ui/components/main-menu/component.js @@ -1,8 +1,10 @@ import Component from "@ember/component"; import { inject as service } from "@ember/service"; +import { classNames, layout, tagName } from "@ember-decorators/component"; import HotkeyMixin from "ui/components/-mixins/hotkey"; +import { hotkey, hotkeysNamespace } from "utils/decorators"; import { isDarwin } from "utils/node/platform"; -import layout from "./template.hbs"; +import template from "./template.hbs"; import "./styles.less"; @@ -19,41 +21,41 @@ const hotkeyActionRouteMap = { }; -export default Component.extend( HotkeyMixin, /** @class MainMenuComponent */ { +@layout( template ) +@tagName( "aside" ) +@classNames( "main-menu-component" ) +@hotkeysNamespace( "navigation" ) +export default class MainMenuComponent extends Component.extend( HotkeyMixin ) { /** @type {RouterService} */ - router: service(), - - layout, - - classNames: [ "main-menu-component" ], - tagName: "aside", - - hotkeysNamespace: "navigation", - hotkeys: { - /** @this {MainMenuComponent} */ - refresh() { - // macOS has a menu bar with its own refresh hotkey - if ( isDarwin ) { return; } - this.router.refresh(); - }, - /** @this {MainMenuComponent} */ - historyBack() { - this.router.history( -1 ); - }, - /** @this {MainMenuComponent} */ - historyForward() { - this.router.history( +1 ); - }, - /** @this {MainMenuComponent} */ - homepage() { - this.router.homepage(); - }, - ...Object.entries( hotkeyActionRouteMap ) - .reduce( ( obj, [ action, route ]) => Object.assign( obj, { - /** @this {MainMenuComponent} */ - [ action ]() { - this.router.transitionTo( route ); - } - }), {} ) + @service router; + + @hotkey + refresh() { + // macOS has a menu bar with its own refresh hotkey + if ( isDarwin ) { return; } + this.router.refresh(); + } + + @hotkey + historyBack() { + this.router.history( -1 ); + } + + @hotkey + historyForward() { + this.router.history( +1 ); } -}); + + @hotkey + homepage() { + this.router.homepage(); + } +} + + +for ( const [ action, route ] of Object.entries( hotkeyActionRouteMap ) ) { + /** @this {MainMenuComponent} */ + MainMenuComponent.prototype.hotkeys[ action ] = function() { + this.router.transitionTo( route ); + }; +} diff --git a/src/app/ui/components/modal/modal-body/component.js b/src/app/ui/components/modal/modal-body/component.js index bbb01ee8b2..fc60536a9f 100644 --- a/src/app/ui/components/modal/modal-body/component.js +++ b/src/app/ui/components/modal/modal-body/component.js @@ -1,7 +1,10 @@ import Component from "@ember/component"; +import { className, classNames, tagName } from "@ember-decorators/component"; -export default Component.extend({ - tagName: "section", - classNames: [ "modal-body-component" ] -}); +@tagName( "section" ) +@classNames( "modal-body-component" ) +export default class ModalBodyComponent extends Component { + @className + class; +} diff --git a/src/app/ui/components/modal/modal-changelog/component.js b/src/app/ui/components/modal/modal-changelog/component.js index d6fc9a7607..5398732a4f 100644 --- a/src/app/ui/components/modal/modal-changelog/component.js +++ b/src/app/ui/components/modal/modal-changelog/component.js @@ -1,34 +1,34 @@ +import { action } from "@ember/object"; import { inject as service } from "@ember/service"; +import { classNames, layout } from "@ember-decorators/component"; import { main as mainConfig } from "config"; import ModalDialogComponent from "../modal-dialog/component"; -import layout from "./template.hbs"; +import template from "./template.hbs"; const { urls: { "release": releaseUrl } } = mainConfig; -export default ModalDialogComponent.extend( /** @class ModalChangelogComponent */ { +@layout( template ) +@classNames( "modal-changelog-component" ) +export default class ModalChangelogComponent extends ModalDialogComponent { /** @type {NwjsService} */ - nwjs: service(), - + @service nwjs; /** @type {VersioncheckService} */ - modalContext: null, - - layout, - classNames: [ "modal-changelog-component" ], + @service versioncheck; - - actions: { - /** @this {ModalChangelogComponent} */ - async showChangelog( success, failure ) { - try { - const { version } = this.modalContext; - this.nwjs.openBrowser( releaseUrl, { version } ); - await success(); - this.send( "close" ); - } catch ( err ) /* istanbul ignore next */ { - await failure( err ); - } + /** @type {VersioncheckService} */ + modalContext; + + @action + async showChangelog( success, failure ) { + try { + const { version } = this.modalContext; + this.nwjs.openBrowser( releaseUrl, { version } ); + await success(); + this.send( "close" ); + } catch ( err ) /* istanbul ignore next */ { + await failure( err ); } } -}); +} diff --git a/src/app/ui/components/modal/modal-dialog/component.js b/src/app/ui/components/modal/modal-dialog/component.js index f8cba3152d..d63823e396 100644 --- a/src/app/ui/components/modal/modal-dialog/component.js +++ b/src/app/ui/components/modal/modal-dialog/component.js @@ -1,29 +1,29 @@ import Component from "@ember/component"; +import { action } from "@ember/object"; import { inject as service } from "@ember/service"; +import { className, classNames, layout, tagName } from "@ember-decorators/component"; +import { on } from "@ember-decorators/object"; import HotkeyMixin from "ui/components/-mixins/hotkey"; -import layout from "./template.hbs"; +import { hotkey, hotkeysNamespace } from "utils/decorators"; +import template from "./template.hbs"; import "./styles.less"; -export default Component.extend( HotkeyMixin, { - modal: service(), +@layout( template ) +@tagName( "section" ) +@classNames( "modal-dialog-component" ) +@hotkeysNamespace( "modaldialog" ) +export default class ModalDialogComponent extends Component.extend( HotkeyMixin ) { + /** @type {ModalService} */ + @service modal; - layout, - - tagName: "section", - classNameBindings: [ ":modal-dialog-component", "class" ], - - "class": "", - - hotkeysNamespace: "modaldialog", - hotkeys: { - close: "close" - }, + @className + class = ""; /** @type {string} Set by the modal-service-component on component init */ - modalName: "", + modalName = ""; /** @type {Object} Set by the modal-service-component on component init */ - modalContext: null, + modalContext = null; /* * Since Ember will try to re-use the same DOM element when only the modalContext changes and @@ -36,13 +36,14 @@ export default Component.extend( HotkeyMixin, { const { element } = this; element.parentNode.replaceChild( element, element ); }); - }, + } /* * This will be called synchronously, so we need to copy the element and animate it instead */ - willDestroyElement() { - const { element } = this; + @on( "willDestroyElement" ) + _fadeOut() { + const element = this.element; let clone = element.cloneNode( true ); clone.classList.add( "fadeOut" ); element.parentNode.appendChild( clone ); @@ -50,12 +51,17 @@ export default Component.extend( HotkeyMixin, { clone.parentNode.removeChild( clone ); clone = null; }, { once: true } ); - }, + } + + + @hotkey( "close" ) + hotkeyClose() { + this.send( "close" ); + } - actions: { - close() { - this.modal.closeModal( this.modalContext, this.modalName ); - } + @action + close() { + this.modal.closeModal( this.modalContext, this.modalName ); } -}); +} diff --git a/src/app/ui/components/modal/modal-firstrun/component.js b/src/app/ui/components/modal/modal-firstrun/component.js index 32ce3f2dcd..fa58a847ab 100644 --- a/src/app/ui/components/modal/modal-firstrun/component.js +++ b/src/app/ui/components/modal/modal-firstrun/component.js @@ -1,24 +1,24 @@ +import { action } from "@ember/object"; import { inject as service } from "@ember/service"; +import { classNames, layout } from "@ember-decorators/component"; import { main as mainConfig } from "config"; import ModalDialogComponent from "../modal-dialog/component"; -import layout from "./template.hbs"; +import template from "./template.hbs"; -export default ModalDialogComponent.extend( /** @class ModalFirstrunComponent */ { +@layout( template ) +@classNames( "modal-firstrun-component" ) +export default class ModalFirstrunComponent extends ModalDialogComponent { /** @type {RouterService} */ - router: service(), + @service router; + /** @type {VersioncheckService} */ + @service versioncheck; - layout, - classNames: [ "modal-firstrun-component" ], + name = mainConfig[ "display-name" ]; - name: mainConfig[ "display-name" ], - - - actions: { - /** @this {ModalFirstrunComponent} */ - settings() { - this.router.transitionTo( "settings" ); - this.send( "close" ); - } + @action + settings() { + this.router.transitionTo( "settings" ); + this.send( "close" ); } -}); +} diff --git a/src/app/ui/components/modal/modal-footer/component.js b/src/app/ui/components/modal/modal-footer/component.js index 42008e22be..0774a168a4 100644 --- a/src/app/ui/components/modal/modal-footer/component.js +++ b/src/app/ui/components/modal/modal-footer/component.js @@ -1,7 +1,10 @@ import Component from "@ember/component"; +import { className, classNames, tagName } from "@ember-decorators/component"; -export default Component.extend({ - tagName: "footer", - classNames: [ "modal-footer-component" ] -}); +@tagName( "footer" ) +@classNames( "modal-footer-component" ) +export default class ModalFooterComponent extends Component { + @className + class; +} diff --git a/src/app/ui/components/modal/modal-header/component.js b/src/app/ui/components/modal/modal-header/component.js index 29811cc9e2..d8b97f24cb 100644 --- a/src/app/ui/components/modal/modal-header/component.js +++ b/src/app/ui/components/modal/modal-header/component.js @@ -1,7 +1,10 @@ import Component from "@ember/component"; +import { className, classNames, tagName } from "@ember-decorators/component"; -export default Component.extend({ - tagName: "header", - classNames: [ "modal-header-component" ] -}); +@tagName( "header" ) +@classNames( "modal-header-component" ) +export default class ModalHeaderComponent extends Component { + @className + class; +} diff --git a/src/app/ui/components/modal/modal-log/component.js b/src/app/ui/components/modal/modal-log/component.js index 639d6dccbe..8566b1b396 100644 --- a/src/app/ui/components/modal/modal-log/component.js +++ b/src/app/ui/components/modal/modal-log/component.js @@ -1,27 +1,26 @@ +import { A } from "@ember/array"; import Component from "@ember/component"; -import { computed, observer } from "@ember/object"; -import { on } from "@ember/object/evented"; +import { classNames, layout, tagName } from "@ember-decorators/component"; +import { observes, on } from "@ember-decorators/object"; import { scheduleOnce } from "@ember/runloop"; -import layout from "./template.hbs"; +import template from "./template.hbs"; -export default Component.extend({ - layout, +@layout( template ) +@tagName( "section" ) +@classNames( "modal-log-component" ) +export default class ModalLogComponent extends Component { + log = A(); - tagName: "section", - classNames: [ "modal-log-component" ], - - log: computed(function() { - return []; - }), - - _logObserver: observer( "log.[]", function() { + @observes( "log.[]" ) + _logObserver() { scheduleOnce( "afterRender", () => this.scrollToBottom() ); - }), + } - scrollToBottom: on( "didInsertElement", function() { + @on( "didInsertElement" ) + scrollToBottom() { const elem = this.element; if ( !elem ) { return; } elem.scrollTop = Math.max( 0, elem.scrollHeight - elem.clientHeight ); - }) -}); + } +} diff --git a/src/app/ui/components/modal/modal-newrelease/component.js b/src/app/ui/components/modal/modal-newrelease/component.js index 85803a582f..e86d41abbb 100644 --- a/src/app/ui/components/modal/modal-newrelease/component.js +++ b/src/app/ui/components/modal/modal-newrelease/component.js @@ -1,35 +1,35 @@ +import { action } from "@ember/object"; import { inject as service } from "@ember/service"; +import { classNames, layout } from "@ember-decorators/component"; import ModalDialogComponent from "../modal-dialog/component"; -import layout from "./template.hbs"; +import template from "./template.hbs"; -export default ModalDialogComponent.extend( /** @class ModalNewreleaseComponent */ { +@layout( template ) +@classNames( "modal-newrelease-component" ) +export default class ModalNewreleaseComponent extends ModalDialogComponent { /** @type {NwjsService} */ - nwjs: service(), - + @service nwjs; /** @type {VersioncheckService} */ - modalContext: null, - - layout, - classNames: [ "modal-newrelease-component" ], - + @service versioncheck; - actions: { - /** @this {ModalNewreleaseComponent} */ - async download( success, failure ) { - try { - this.nwjs.openBrowser( this.modalContext.release.html_url ); - await success(); - this.send( "ignore" ); - } catch ( err ) /* istanbul ignore next */ { - await failure( err ); - } - }, + /** @type {VersioncheckService} */ + modalContext; - /** @this {ModalNewreleaseComponent} */ - ignore() { - this.modalContext.ignoreRelease(); - this.send( "close" ); + @action + async download( success, failure ) { + try { + this.nwjs.openBrowser( this.modalContext.release.html_url ); + await success(); + this.send( "ignore" ); + } catch ( err ) /* istanbul ignore next */ { + await failure( err ); } } -}); + + @action + ignore() { + this.modalContext.ignoreRelease(); + this.send( "close" ); + } +} diff --git a/src/app/ui/components/modal/modal-service/component.js b/src/app/ui/components/modal/modal-service/component.js index 85bcd0098c..de19911c72 100644 --- a/src/app/ui/components/modal/modal-service/component.js +++ b/src/app/ui/components/modal/modal-service/component.js @@ -1,14 +1,18 @@ import Component from "@ember/component"; +import { readOnly } from "@ember/object/computed"; +import { className, classNames, layout } from "@ember-decorators/component"; import { inject as service } from "@ember/service"; -import layout from "./template.hbs"; +import template from "./template.hbs"; import "./styles.less"; -export default Component.extend({ +@layout( template ) +@classNames( "modal-service-component" ) +export default class ModalServiceComponent extends Component { /** @type {ModalService} */ - modal: service(), + @service modal; - layout, - classNames: "modal-service-component", - classNameBindings: [ "modal.isModalOpened:active" ] -}); + @className( "active" ) + @readOnly( "modal.isModalOpened" ) + isActive +} diff --git a/src/app/ui/components/modal/modal-streaming/component.js b/src/app/ui/components/modal/modal-streaming/component.js index 9a1103dee3..0e7deec270 100644 --- a/src/app/ui/components/modal/modal-streaming/component.js +++ b/src/app/ui/components/modal/modal-streaming/component.js @@ -1,6 +1,7 @@ -import { get, set, computed } from "@ember/object"; +import { action, get, set, computed } from "@ember/object"; import { readOnly } from "@ember/object/computed"; import { inject as service } from "@ember/service"; +import { classNames, layout } from "@ember-decorators/component"; import { streaming as streamingConfig } from "config"; import ModalDialogComponent from "../modal-dialog/component"; import { qualities } from "data/models/stream/model"; @@ -14,7 +15,8 @@ import { TimeoutError, HostingError } from "services/streaming/errors"; -import layout from "./template.hbs"; +import { hotkey, hotkeysNamespace } from "utils/decorators"; +import template from "./template.hbs"; import "./styles.less"; @@ -33,143 +35,156 @@ function computedError( Class ) { } -export default ModalDialogComponent.extend( /** @class ModalStreamingComponent */ { +@layout( template ) +@classNames( "modal-streaming-component" ) +@hotkeysNamespace( "modalstreaming" ) +export default class ModalStreamingComponent extends ModalDialogComponent { /** @type {NwjsService} */ - nwjs: service(), + @service nwjs; /** @type {StreamingService} */ - streaming: service(), + @service streaming; /** @type {SettingsService} */ - settings: service(), + @service settings; /** @type {DS.Store} */ - store: service(), + @service store; - layout, - - classNames: [ "modal-streaming-component" ], /** @type {Stream} */ - modalContext: null, + modalContext; + + /** @alias modalContext.error */ + @readOnly( "modalContext.error" ) + error; + + @computedError( LogError ) + isLogError; + @computedError( ProviderError ) + isProviderError; + @computedError( PlayerError ) + isPlayerError; + @computedError( VersionError ) + isVersionError; + @computedError( UnableToOpenError ) + isUnableToOpenError; + @computedError( NoStreamsFoundError ) + isNoStreamsFoundError; + @computedError( TimeoutError ) + isTimeoutError; + @computedError( HostingError ) + isHostingError; + + qualities = qualities; + @computed( "settings.content.streaming.providerType" ) + get versionMin() { + const type = this.settings.content.streaming.providerType; - error: readOnly( "modalContext.error" ), + return validationProviders[ type ][ "version" ]; + } - isLogError: computedError( LogError ), - isProviderError: computedError( ProviderError ), - isPlayerError: computedError( PlayerError ), - isVersionError: computedError( VersionError ), - isUnableToOpenError: computedError( UnableToOpenError ), - isNoStreamsFoundError: computedError( NoStreamsFoundError ), - isTimeoutError: computedError( TimeoutError ), - isHostingError: computedError( HostingError ), + @readOnly( "settings.content.streaming.providerName" ) + providerName; - qualities, - versionMin: computed( "settings.content.streaming.providerType", function() { - const type = this.settings.content.streaming.providerType; - return validationProviders[ type ][ "version" ]; - }), + @hotkey( "close" ) + hotkeyClose() { + if ( this.modalContext.isPreparing ) { + this.send( "abort" ); + } else { + this.send( "close" ); + } + } - providerName: readOnly( "settings.streaming.providerName" ), + @hotkey( "confirm" ) + hotkeyConfirm() { + if ( this.isHostingError ) { + this.send( "startHosted" ); + } else if ( this.modalContext.hasEnded ) { + this.send( "restart" ); + } + } + @hotkey( "shutdown" ) + hotkeyShutdown() { + if ( this.modalContext.isPreparing ) { + this.send( "abort" ); + } else { + this.send( "shutdown" ); + } + } - hotkeysNamespace: "modalstreaming", - hotkeys: { - /** @this {ModalStreamingComponent} */ - close() { - if ( this.modalContext.isPreparing ) { - this.send( "abort" ); - } else { - this.send( "close" ); - } - }, - /** @this {ModalStreamingComponent} */ - confirm() { - if ( this.isHostingError ) { - this.send( "startHosted" ); - } else if ( this.modalContext.hasEnded ) { - this.send( "restart" ); - } - }, - /** @this {ModalStreamingComponent} */ - shutdown() { - if ( this.modalContext.isPreparing ) { - this.send( "abort" ); - } else { - this.send( "shutdown" ); - } - }, - log: "toggleLog" - }, + @hotkey( "log" ) + hotkeyLog() { + this.send( "toggleLog" ); + } - actions: { - /** @this {ModalStreamingComponent} */ - async download( success, failure ) { - try { - const providerType = this.settings.content.streaming.providerType; - this.nwjs.openBrowser( downloadUrl[ providerType ] ); + @action + async download( success, failure ) { + try { + const { providerType } = this.settings.content.streaming; + this.nwjs.openBrowser( downloadUrl[ providerType ] ); + await success(); + this.send( "close" ); + } catch ( err ) { + await failure( err ); + } + } + + @action + abort() { + const { modalContext } = this; + if ( !modalContext.isDestroyed ) { + set( modalContext, "isAborted", true ); + modalContext.destroyRecord(); + } + this.send( "close" ); + } + + @action + async shutdown() { + this.modalContext.kill(); + this.send( "close" ); + } + + @action + async restart( success ) { + const { modalContext } = this; + if ( !modalContext.isDestroyed ) { + if ( success ) { await success(); - this.send( "close" ); - } catch ( err ) { - await failure( err ); } - }, - - /** @this {ModalStreamingComponent} */ - abort() { - const { modalContext } = this; - if ( !modalContext.isDestroyed ) { - set( modalContext, "isAborted", true ); - modalContext.destroyRecord(); - } - this.send( "close" ); - }, + this.streaming.launchStream( modalContext ) + .catch( () => {} ); + } + } - /** @this {ModalStreamingComponent} */ - async shutdown() { - this.modalContext.kill(); - this.send( "close" ); - }, - - /** @this {ModalStreamingComponent} */ - async restart( success ) { - const { modalContext } = this; - if ( !modalContext.isDestroyed ) { - if ( success ) { - await success(); - } - this.streaming.launchStream( modalContext ) - .catch( () => {} ); + @action + async startHosted( success, failure ) { + const { modalContext } = this; + if ( modalContext.isDestroyed ) { return; } + const channel = get( modalContext, "error.channel" ); + if ( !channel ) { return; } + try { + const user = await this.store.queryRecord( "twitchUser", channel ); + const stream = await get( user, "stream" ); + if ( success ) { + await success(); } - }, - - /** @this {ModalStreamingComponent} */ - async startHosted( success, failure ) { - const { modalContext } = this; - if ( modalContext.isDestroyed ) { return; } - const channel = get( modalContext, "error.channel" ); - if ( !channel ) { return; } - try { - const user = await this.store.queryRecord( "twitchUser", channel ); - const stream = await get( user, "stream" ); - if ( success ) { - await success(); - } - this.send( "close" ); - this.streaming.startStream( stream ) - .catch( () => {} ); - } catch ( e ) { - if ( failure ) { - await failure(); - } + this.send( "close" ); + this.streaming.startStream( stream ) + .catch( () => {} ); + } catch ( e ) { + if ( failure ) { + await failure(); } - }, + } + } - /** @this {ModalStreamingComponent} */ - toggleLog() { - const { modalContext } = this; - if ( !modalContext.isDestroyed ) { - modalContext.toggleProperty( "showLog" ); - } + @action + toggleLog() { + const { modalContext } = this; + if ( !modalContext.isDestroyed ) { + modalContext.toggleProperty( "showLog" ); } } -}); +} diff --git a/src/app/ui/components/preview-image/component.js b/src/app/ui/components/preview-image/component.js index 38f7f5c529..bb9f2dbbc6 100644 --- a/src/app/ui/components/preview-image/component.js +++ b/src/app/ui/components/preview-image/component.js @@ -1,28 +1,28 @@ import Component from "@ember/component"; -import { get, set } from "@ember/object"; +import { set } from "@ember/object"; import { next, scheduleOnce } from "@ember/runloop"; -import layout from "./template.hbs"; +import { layout } from "@ember-decorators/component"; +import { on } from "@ember-decorators/object"; +import template from "./template.hbs"; import "./styles.less"; -export default Component.extend({ - layout, - - error: false, +@layout( template ) +export default class PreviewImageComponent extends Component { + error = false; /* istanbul ignore next */ - onLoad() {}, + onLoad() {} /* istanbul ignore next */ - onError() {}, - - didInsertElement() { - this._super( ...arguments ); + onError() {} + @on( "didInsertElement" ) + onDidInsertElement() { const setError = () => scheduleOnce( "afterRender", () => { set( this, "error", true ); }); - if ( !get( this, "src" ) ) { + if ( !this.src ) { return setError(); } @@ -47,4 +47,4 @@ export default Component.extend({ img.addEventListener( "error", onError, false ); img.addEventListener( "load", onLoad, false ); } -}); +} diff --git a/src/app/ui/components/quick/quick-bar-homepage/component.js b/src/app/ui/components/quick/quick-bar-homepage/component.js index 5d71fd947c..712f346f8f 100644 --- a/src/app/ui/components/quick/quick-bar-homepage/component.js +++ b/src/app/ui/components/quick/quick-bar-homepage/component.js @@ -1,5 +1,5 @@ -import { getOwner } from "@ember/application"; -import { get, set, computed } from "@ember/object"; +import { set } from "@ember/object"; +import { equal } from "@ember/object/computed"; import { inject as service } from "@ember/service"; import { translationMacro as t } from "ember-i18n/addon"; import FormButtonComponent from "ui/components/button/form-button/component"; @@ -7,6 +7,7 @@ import FormButtonComponent from "ui/components/button/form-button/component"; export default FormButtonComponent.extend({ i18n: service(), + router: service(), settings: service(), classNames: "btn-neutral", @@ -17,26 +18,15 @@ export default FormButtonComponent.extend({ icon: "fa-home", iconanim: true, - url: computed(function() { - let router = getOwner( this ).lookup( "router:main" ); - let location = get( router, "location" ); - - return location.getURL(); - }).volatile(), - - isHomepage: computed( "url", "settings.gui.homepage", function() { - return get( this, "url" ) === get( this, "settings.gui.homepage" ); - }), + isHomepage: equal( "router.currentURL", "settings.content.gui.homepage" ), action() { - let settings = get( this, "settings.content" ); - let value = get( this, "url" ); - if ( !settings || !value ) { - return Promise.reject(); - } - + /** @type {Settings} */ + const settings = this.settings.content; + const value = this.router.currentURL; set( settings, "gui.homepage", value ); + return settings.save(); } }); diff --git a/src/app/ui/components/search-bar/component.js b/src/app/ui/components/search-bar/component.js index 91f684fe5b..1bd0bb914c 100644 --- a/src/app/ui/components/search-bar/component.js +++ b/src/app/ui/components/search-bar/component.js @@ -1,14 +1,16 @@ import Component from "@ember/component"; -import { get, set, getWithDefault } from "@ember/object"; +import { set, action } from "@ember/object"; import { sort } from "@ember/object/computed"; -import { on } from "@ember/object/evented"; import { run } from "@ember/runloop"; import { inject as service } from "@ember/service"; +import { classNames, layout, tagName } from "@ember-decorators/component"; +import { on } from "@ember-decorators/object"; import { vars as varsConfig } from "config"; import HotkeyMixin from "ui/components/-mixins/hotkey"; import Search from "data/models/search/model"; +import { hotkey, hotkeysNamespace } from "utils/decorators"; import getStreamFromUrl from "utils/getStreamFromUrl"; -import layout from "./template.hbs"; +import template from "./template.hbs"; import "./styles.less"; @@ -17,95 +19,87 @@ const { filters } = Search; // TODO: rewrite SearchBarComponent and Search model -export default Component.extend( HotkeyMixin, { +@layout( template ) +@tagName( "nav" ) +@classNames( "search-bar-component" ) +@hotkeysNamespace( "searchbar" ) +export default class SearchBarComponent extends Component.extend( HotkeyMixin ) { /** @type {RouterService} */ - router: service(), - store: service(), + @service router; + /** @type {DS.Store} */ + @service store; - layout, - tagName: "nav", - classNames: [ "search-bar-component" ], + /** @type {DS.RecordArray} */ + model = null; - // the record array (will be set by init()) - model: null, - // needed by SortableMixin's arrangedContent - content: sort( "model", "sortBy" ), - sortBy: [ "date:desc" ], + @sort( "model", "sortBy" ) + content; + sortBy = [ "date:desc" ]; - showDropdown: false, + showDropdown = false; - query: "", - reQuery: /^\S+/, + query = ""; + reQuery = /^\S+/; - filters, - filter: "all", + filters = filters; + filter = "all"; - hotkeysNamespace: "searchbar", - hotkeys: { - focus: "focus" - }, - - - init() { - this._super( ...arguments ); - - const store = get( this, "store" ); - store.findAll( "search" ) - .then( records => { - set( this, "model", records ); - }); - }, + @on( "init" ) + async _setModel() { + const records = await this.store.findAll( "search" ); + set( this, "model", records ); + } async addRecord( query, filter ) { - const store = get( this, "store" ); - const model = get( this, "model" ); + const model = this.model; const match = model.filter( record => - query === get( record, "query" ) - && filter === get( record, "filter" ) + query === record.query + && filter === record.filter ); let record; // found a matching record? just update the date property, save the record and return - if ( get( match, "length" ) === 1 ) { + if ( match.length === 1 ) { set( match[0], "date", new Date() ); await match[0].save(); return; } // we don't want to store more than X records - if ( get( model, "length" ) >= searchHistorySize ) { + if ( model.length >= searchHistorySize ) { const oldestRecord = model.sortBy( "date" ).shiftObject(); await run( () => oldestRecord.destroyRecord() ); } // create a new record - const id = 1 + Number( getWithDefault( model, "lastObject.id", 0 ) ); + const id = 1 + Number( model.lastObject.id || 0 ); const date = new Date(); - record = store.createRecord( "search", { id, query, filter, date } ); + record = this.store.createRecord( "search", { id, query, filter, date } ); await record.save(); model.addObject( record ); - }, + } async deleteAllRecords() { // delete all records at once and then clear the record array - const model = get( this, "model" ); + const model = this.model; model.forEach( record => record.deleteRecord() ); await model.save(); model.clear(); this.store.unloadAll( "search" ); - }, + } doSearch( query, filter ) { set( this, "showDropdown", false ); this.addRecord( query, filter ); this.router.transitionTo( "search", { queryParams: { filter, query } } ); - }, + } - _prepareDropdown: on( "didInsertElement", function() { + @on( "didInsertElement" ) + _prepareDropdown() { // dropdown const element = this.element; const dropdown = element.querySelector( ".searchbar-dropdown" ); @@ -125,58 +119,68 @@ export default Component.extend( HotkeyMixin, { set( this, "showDropdown", false ); } }); - }), + } + + @hotkey( "focus" ) + hotkeyFocus() { + this.send( "focus" ); + } - actions: { - back() { - this.router.history( -1 ); - }, - forward() { - this.router.history( +1 ); - }, + @action + back() { + this.router.history( -1 ); + } - refresh() { - this.router.refresh(); - }, + @action + forward() { + this.router.history( +1 ); + } - focus() { - this.element.querySelector( "input[type='search']" ).focus(); - }, + @action + refresh() { + this.router.refresh(); + } - toggleDropdown() { - const showDropdown = get( this, "showDropdown" ); - set( this, "showDropdown", !showDropdown ); - }, + @action + focus() { + this.element.querySelector( "input[type='search']" ).focus(); + } - clear() { - set( this, "query", "" ); - }, + @action + toggleDropdown() { + set( this, "showDropdown", !this.showDropdown ); + } - submit() { - let query = get( this, "query" ).trim(); - let filter = get( this, "filter" ); + @action + clear() { + set( this, "query", "" ); + } - const stream = getStreamFromUrl( query ); - if ( stream ) { - query = stream; - filter = "channels"; - } + @action + submit() { + let query = this.query.trim(); + let filter = this.filter; - if ( this.reQuery.test( query ) ) { - this.doSearch( query, filter ); - } - }, + const stream = getStreamFromUrl( query ); + if ( stream ) { + query = stream; + filter = "channels"; + } - searchHistory( record ) { - const query = get( record, "query" ); - const filter = get( record, "filter" ); + if ( this.reQuery.test( query ) ) { this.doSearch( query, filter ); - }, - - clearHistory() { - this.deleteAllRecords(); } } -}); + + @action + searchHistory( record ) { + this.doSearch( record.query, record.filter ); + } + + @action + clearHistory() { + this.deleteAllRecords(); + } +} diff --git a/src/app/ui/components/selectable-text/component.js b/src/app/ui/components/selectable-text/component.js index 7ce9f5da7d..679e674f5f 100644 --- a/src/app/ui/components/selectable-text/component.js +++ b/src/app/ui/components/selectable-text/component.js @@ -1,19 +1,19 @@ import Component from "@ember/component"; import { inject as service } from "@ember/service"; +import { attribute, className, tagName } from "@ember-decorators/component"; import t from "translation-key"; -export default Component.extend({ +@tagName( "div" ) +export default class SelectableTextComponent extends Component { /** @type {NwjsService} */ - nwjs: service(), + @service nwjs; - tagName: "div", + @className + class = ""; - classNameBindings: [ "class" ], - attributeBindings: [ "selectable:data-selectable" ], - - "class" : "", - selectable: true, + @attribute( "data-selectable" ) + selectable = true; contextMenu( event ) { if ( this.attrs.noContextmenu ) { return; } @@ -31,4 +31,4 @@ export default Component.extend({ } }] ); } -}); +} diff --git a/src/app/ui/components/settings-row/component.js b/src/app/ui/components/settings-row/component.js index d3c7767d2f..e9f18676c8 100644 --- a/src/app/ui/components/settings-row/component.js +++ b/src/app/ui/components/settings-row/component.js @@ -1,42 +1,32 @@ import Component from "@ember/component"; -import { get, computed } from "@ember/object"; -import layout from "./template.hbs"; +import { computed } from "@ember/object"; +import { classNames, layout } from "@ember-decorators/component"; +import template from "./template.hbs"; import "./styles.less"; -/** - * @param {Substitution[]} substitutions - * @returns {Array} - */ -function getSubstitutionsList( substitutions ) { - if ( !( substitutions instanceof Array ) ) { return []; } +@layout( template ) +@classNames( "settings-row-component" ) +export default class SettingsRowComponent extends Component { + static positionalParams = [ "title", "description" ]; - return substitutions.map(function( substitution ) { - const vars = substitution.vars.map( name => `{${name}}` ); + strNewLine = "\n"; + substitutionsExpanded = false; - return { - variable : vars[0], - description: substitution.description - }; - }); -} - - -export default Component.extend({ - layout, + documentationUrl = null; - classNames: [ "settings-row-component" ], + @computed( "substitutions" ) + get _substitutions() { + /** @type {Substitution[]} substitutions */ + const substitutions = this.substitutions; - strNewLine: "\n", - substitutionsExpanded: false, + if ( !Array.isArray( substitutions ) ) { + return []; + } - documentationUrl: null, - - _substitutions: computed( "substitutions", function() { - const substitutions = get( this, "substitutions" ); - return getSubstitutionsList( substitutions ); - }) - -}).reopenClass({ - positionalParams: [ "title", "description" ] -}); + return substitutions.map( substitution => ({ + variable: `{${substitution.vars[0]}}`, + description: substitution.description + }) ); + } +} diff --git a/src/app/ui/components/settings-row/template.hbs b/src/app/ui/components/settings-row/template.hbs index 2078df57cf..caee000119 100644 --- a/src/app/ui/components/settings-row/template.hbs +++ b/src/app/ui/components/settings-row/template.hbs @@ -30,7 +30,7 @@ {{#unless (is-null documentationUrl)}} {{documentation-link item=documentation baseUrl=documentationUrl class=(if documentationUrl "hoverable" "")}} {{else}} - {{documentation-link item=documentation classNames="hoverable"}} + {{documentation-link item=documentation class="hoverable"}} {{/unless}}

{{/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}}
{{#if settings.advanced}} -{{#if showTokenForm}} +{{#if isTokenFormVisible}}
{{input type="text" @@ -54,7 +54,7 @@ {{/form-button}} {{/if}} {{/if}} -{{#if showFailMessage}} +{{#if isFailMessageVisible}}

{{t "routes.user.auth.fail"}}

{{/if}}
diff --git a/src/app/ui/routes/user/followed-channels/controller.js b/src/app/ui/routes/user/followed-channels/controller.js index fd00b05be6..3fada196b0 100644 --- a/src/app/ui/routes/user/followed-channels/controller.js +++ b/src/app/ui/routes/user/followed-channels/controller.js @@ -1,20 +1,20 @@ import Controller from "@ember/controller"; -import { set } from "@ember/object"; +import { set, action } from "@ember/object"; -export default Controller.extend({ - queryParams: [ "sortby", "direction" ], +export default class UserFollowedChannelsRoute extends Controller { + queryParams = [ "sortby", "direction" ]; - sortby : "created_at", - direction: "desc", + sortby = "created_at"; + direction = "desc"; - actions: { - sortMethod( sortby ) { - set( this, "sortby", sortby ); - }, + @action + sortMethod( sortby ) { + set( this, "sortby", sortby ); + } - sortOrder( direction ) { - set( this, "direction", direction ); - } + @action + sortOrder( direction ) { + set( this, "direction", direction ); } -}); +} diff --git a/src/app/ui/routes/user/followed-channels/route.js b/src/app/ui/routes/user/followed-channels/route.js index 89dfa76bc2..bfb23981e9 100644 --- a/src/app/ui/routes/user/followed-channels/route.js +++ b/src/app/ui/routes/user/followed-channels/route.js @@ -1,32 +1,32 @@ -import { getProperties } from "@ember/object"; import UserIndexRoute from "../index/route"; import InfiniteScrollOffsetMixin from "ui/routes/-mixins/routes/infinite-scroll/offset"; import RefreshRouteMixin from "ui/routes/-mixins/routes/refresh"; -export default UserIndexRoute.extend( InfiniteScrollOffsetMixin, RefreshRouteMixin, { - itemSelector: ".channel-item-component", - modelName: "twitchChannelFollowed", - modelMapBy: "channel", - modelPreload: "logo", +export default class UserFollowedChannelsRoute +extends UserIndexRoute.extend( InfiniteScrollOffsetMixin, RefreshRouteMixin ) { + itemSelector = ".channel-item-component"; + modelName = "twitch-channel-followed"; + modelMapBy = "channel"; + modelPreload = "logo"; - queryParams: { + queryParams = { sortby: { refreshModel: true }, direction: { refreshModel: true } - }, + }; model({ sortby = "created_at", direction = "desc" }) { - return this._super({ sortby, direction }); - }, + return super.model({ sortby, direction }); + } fetchContent() { - const params = getProperties( this.controller, "sortby", "direction" ); + const { sortby, direction } = this.controller; - return this.model( params ); + return this.model({ sortby, direction }); } -}); +} diff --git a/src/app/ui/routes/user/followed-streams/route.js b/src/app/ui/routes/user/followed-streams/route.js index c41423aa9c..f20f11f140 100644 --- a/src/app/ui/routes/user/followed-streams/route.js +++ b/src/app/ui/routes/user/followed-streams/route.js @@ -6,13 +6,14 @@ import { mapBy } from "utils/ember/recordArrayMethods"; import { preload } from "utils/preload"; -export default UserIndexRoute.extend( InfiniteScrollMixin, RefreshRouteMixin, { - itemSelector: ".stream-item-component", +export default class UserFollowedStreamsRoute +extends UserIndexRoute.extend( InfiniteScrollMixin, RefreshRouteMixin ) { + itemSelector = ".stream-item-component"; // Guard against infinite API queries in case Twitch's query offset implemention breaks. // Assume that nobody has followed so many channels that more than 500 (5*maxLimit) streams // are online at the same time. - maxQueries: 5, + maxQueries = 5; /** * Twitch doesn't sort the followed streams response anymore: @@ -43,7 +44,7 @@ export default UserIndexRoute.extend( InfiniteScrollMixin, RefreshRouteMixin, { // return the first infinite scroll result as initial model data return await this.fetchContent(); - }, + } /** * @returns {Promise} @@ -57,10 +58,10 @@ export default UserIndexRoute.extend( InfiniteScrollMixin, RefreshRouteMixin, { }; return await preload( streams, "preview.mediumLatest" ); - }, + } deactivate() { this._super( ...arguments ); this.all = null; } -}); +} diff --git a/src/app/ui/routes/user/followed-streams/template.hbs b/src/app/ui/routes/user/followed-streams/template.hbs index ce5c8e3f9d..ad9bb75902 100644 --- a/src/app/ui/routes/user/followed-streams/template.hbs +++ b/src/app/ui/routes/user/followed-streams/template.hbs @@ -8,7 +8,7 @@ {{/quick-bar}} {{#content-list model as |item isNewItem isDuplicateItem|}} -{{stream-item content=item isNewItem=isNewItem isDuplicateItem=isDuplicateItem faded=false}} +{{stream-item content=item isNewItem=isNewItem isDuplicateItem=isDuplicateItem ignoreLanguageFading=true}} {{else}}

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