diff --git a/medusa/__main__.py b/medusa/__main__.py index 1104ec6483..f44a2b1586 100755 --- a/medusa/__main__.py +++ b/medusa/__main__.py @@ -2134,6 +2134,7 @@ def load_shows_from_db(): for sql_show in sql_results: try: cur_show = Series(sql_show['indexer'], sql_show['indexer_id']) + cur_show.prev_episode() cur_show.next_episode() app.showList.append(cur_show) except Exception as error: diff --git a/medusa/server/api/v2/config.py b/medusa/server/api/v2/config.py index 64b46cb28a..43515dc0b3 100644 --- a/medusa/server/api/v2/config.py +++ b/medusa/server/api/v2/config.py @@ -80,7 +80,6 @@ class ConfigHandler(BaseRequestHandler): #: patch mapping patches = { # Main - 'selectedRootIndex': IntegerField(app, 'SELECTED_ROOT'), 'rootDirs': ListField(app, 'ROOT_DIRS'), 'showDefaults.status': EnumField(app, 'STATUS_DEFAULT', (SKIPPED, WANTED, IGNORED), int), @@ -474,6 +473,7 @@ class ConfigHandler(BaseRequestHandler): 'layout.backlogOverview.status': StringField(app, 'BACKLOG_STATUS'), 'layout.timeStyle': StringField(app, 'TIME_PRESET_W_SECONDS'), 'layout.dateStyle': StringField(app, 'DATE_PRESET'), + 'layout.selectedRootIndex': IntegerField(app, 'SELECTED_ROOT'), } def get(self, identifier, path_param=None): @@ -615,8 +615,6 @@ def data_main(): section_data['logs']['subliminalLog'] = bool(app.SUBLIMINAL_LOG) section_data['logs']['privacyLevel'] = app.PRIVACY_LEVEL - section_data['selectedRootIndex'] = int_default(app.SELECTED_ROOT, -1) # All paths - # Added for config - main, needs refactoring in the structure. section_data['launchBrowser'] = bool(app.LAUNCH_BROWSER) section_data['defaultPage'] = app.DEFAULT_PAGE @@ -1174,7 +1172,7 @@ def data_layout(): section_data['wide'] = bool(app.LAYOUT_WIDE) - section_data['posterSortdir'] = int(app.POSTER_SORTDIR) + section_data['posterSortdir'] = int(app.POSTER_SORTDIR or 0) section_data['themeName'] = app.THEME_NAME section_data['animeSplitHomeInTabs'] = bool(app.ANIME_SPLIT_HOME_IN_TABS) section_data['animeSplitHome'] = bool(app.ANIME_SPLIT_HOME) @@ -1192,11 +1190,13 @@ def data_layout(): section_data['comingEps'] = {} section_data['comingEps']['displayPaused'] = bool(app.COMING_EPS_DISPLAY_PAUSED) section_data['comingEps']['sort'] = app.COMING_EPS_SORT - section_data['comingEps']['missedRange'] = int(app.COMING_EPS_MISSED_RANGE) + section_data['comingEps']['missedRange'] = int(app.COMING_EPS_MISSED_RANGE or 0) section_data['comingEps']['layout'] = app.COMING_EPS_LAYOUT section_data['backlogOverview'] = {} section_data['backlogOverview']['status'] = app.BACKLOG_STATUS section_data['backlogOverview']['period'] = app.BACKLOG_PERIOD + section_data['selectedRootIndex'] = int_default(app.SELECTED_ROOT, -1) # All paths + return section_data diff --git a/medusa/server/api/v2/series.py b/medusa/server/api/v2/series.py index 4151b960f6..bbd5c673df 100644 --- a/medusa/server/api/v2/series.py +++ b/medusa/server/api/v2/series.py @@ -55,6 +55,7 @@ def filter_series(current): s.to_json(detailed=detailed, episodes=episodes) for s in Series.find_series(predicate=filter_series) ] + return self._paginate(data, sort='title') identifier = SeriesIdentifier.from_slug(series_slug) diff --git a/medusa/server/api/v2/stats.py b/medusa/server/api/v2/stats.py index df3cc1347e..8ca4cfd763 100644 --- a/medusa/server/api/v2/stats.py +++ b/medusa/server/api/v2/stats.py @@ -17,6 +17,7 @@ UNAIRED, WANTED ) +from medusa.network_timezones import parse_date_time from medusa.server.api.v2.base import BaseRequestHandler from medusa.show.show import Show @@ -66,46 +67,49 @@ def query_in(items): return '({0})'.format(','.join(map(str, items))) query = dedent("""\ - SELECT indexer AS indexerId, showid AS seriesId, + SELECT tv_eps.indexer AS indexerId, tv_eps.showid AS seriesId, SUM( season > 0 AND episode > 0 AND airdate > 1 AND - status IN {status_quality} + tv_eps.status IN {status_quality} ) AS epSnatched, SUM( season > 0 AND episode > 0 AND airdate > 1 AND - status IN {status_download} + tv_eps.status IN {status_download} ) AS epDownloaded, SUM( season > 0 AND episode > 0 AND airdate > 1 AND ( - (airdate <= {today} AND status IN {status_pre_today}) OR - status IN {status_both} + (airdate <= {today} AND tv_eps.status IN {status_pre_today}) OR + tv_eps.status IN {status_both} ) ) AS epTotal, (SELECT airdate FROM tv_episodes - WHERE showid=tv_eps.showid AND - indexer=tv_eps.indexer AND + WHERE tv_episodes.showid=tv_eps.showid AND + tv_episodes.indexer=tv_eps.indexer AND airdate >= {today} AND - (status = {unaired} OR status = {wanted}) + (tv_eps.status = {unaired} OR tv_eps.status = {wanted}) ORDER BY airdate ASC LIMIT 1 ) AS epAirsNext, (SELECT airdate FROM tv_episodes - WHERE showid=tv_eps.showid AND - indexer=tv_eps.indexer AND - airdate > 1 AND - status <> {unaired} + WHERE tv_episodes.showid=tv_eps.showid AND + tv_episodes.indexer=tv_eps.indexer AND + airdate > {today} AND + tv_eps.status <> {unaired} ORDER BY airdate DESC LIMIT 1 ) AS epAirsPrev, - SUM(file_size) AS seriesSize - FROM tv_episodes tv_eps - GROUP BY showid, indexer + SUM(file_size) AS seriesSize, + tv_shows.airs as airs, + tv_shows.network as network + FROM tv_episodes tv_eps, tv_shows + WHERE tv_eps.showid = tv_shows.indexer_id AND tv_eps.indexer = tv_shows.indexer + GROUP BY tv_eps.showid, tv_eps.indexer; """).format( status_quality=query_in(snatched), status_download=query_in(downloaded), @@ -121,12 +125,16 @@ def query_in(items): sql_result = main_db_con.select(query) stats_data = {} - stats_data['seriesStat'] = [] + stats_data['stats'] = [] stats_data['maxDownloadCount'] = 1000 for cur_result in sql_result: - stats_data['seriesStat'].append(cur_result) + stats_data['stats'].append(cur_result) if cur_result['epTotal'] > stats_data['maxDownloadCount']: stats_data['maxDownloadCount'] = cur_result['epTotal'] + if cur_result['epAirsNext']: + cur_result['epAirsNext'] = parse_date_time(cur_result['epAirsNext'], cur_result['airs'], cur_result['network']) + if cur_result['epAirsPrev']: + cur_result['epAirsPrev'] = parse_date_time(cur_result['epAirsPrev'], cur_result['airs'], cur_result['network']) stats_data['maxDownloadCount'] *= 100 return stats_data diff --git a/medusa/server/web/home/handler.py b/medusa/server/web/home/handler.py index a847d66749..5c195cd1c1 100644 --- a/medusa/server/web/home/handler.py +++ b/medusa/server/web/home/handler.py @@ -121,41 +121,13 @@ def _genericMessage(self, subject, message): return t.render(message=message, subject=subject, title='') def index(self): - t = PageTemplate(rh=self, filename='home.mako') - selected_root = int(app.SELECTED_ROOT) - shows_dir = None - if selected_root is not None and app.ROOT_DIRS: - backend_pieces = app.ROOT_DIRS - backend_dirs = backend_pieces[1:] - try: - shows_dir = backend_dirs[selected_root] if selected_root != -1 else None - except IndexError: - # If user have a root selected in /home and remove the root folder a IndexError is raised - shows_dir = None - app.SELECTED_ROOT = -1 - - series = [] - if app.ANIME_SPLIT_HOME: - anime = [] - for show in app.showList: - if shows_dir and not show._location.startswith(shows_dir): - continue - if show.is_anime: - anime.append(show) - else: - series.append(show) - - show_lists = [[order, {'Series': series, 'Anime': anime}[order]] for order in app.SHOW_LIST_ORDER] - else: - for show in app.showList: - if shows_dir and not show._location.startswith(shows_dir): - continue - series.append(show) - show_lists = [['Series', series]] + """ + Render the home page. - stats = self.show_statistics() - return t.render(show_lists=show_lists, show_stat=stats[0], - max_download_count=stats[1], controller='home', action='index') + [Converted to VueRouter] + """ + t = PageTemplate(rh=self, filename='index.mako') + return t.render() @staticmethod def show_statistics(): diff --git a/medusa/tv/series.py b/medusa/tv/series.py index ec5c4ad046..bd76acbbb2 100644 --- a/medusa/tv/series.py +++ b/medusa/tv/series.py @@ -235,6 +235,7 @@ def __init__(self, indexer, indexerid, lang='', quality=None, self.default_ep_status = SKIPPED self._location = '' self.episodes = {} + self.prev_aired = '' self.next_aired = '' self.release_groups = None self.exceptions = set() @@ -555,9 +556,17 @@ def last_update_indexer(self): epoch_date = update_date - datetime.date.fromtimestamp(0) return int(epoch_date.total_seconds()) + @property + def prev_airdate(self): + """Return last aired episode airdate.""" + return ( + sbdatetime.convert_to_setting(network_timezones.parse_date_time(self.prev_aired, self.airs, self.network)) + if try_int(self.prev_aired, 1) > MILLIS_YEAR_1900 else None + ) + @property def next_airdate(self): - """Return next airdate.""" + """Return next aired episode airdate.""" return ( sbdatetime.convert_to_setting(network_timezones.parse_date_time(self.next_aired, self.airs, self.network)) if try_int(self.next_aired, 1) > MILLIS_YEAR_1900 else None @@ -623,7 +632,7 @@ def aliases_to_json(self): @property def xem_numbering(self): """Return series episode xem numbering.""" - return get_xem_numbering_for_show(self) + return get_xem_numbering_for_show(self, refresh_data=False) @property def xem_absolute_numbering(self): @@ -1612,6 +1621,46 @@ def load_imdb_info(self): log.debug(u'{id}: Obtained info from IMDb: {imdb_info}', {'id': self.series_id, 'imdb_info': self.imdb_info}) + def prev_episode(self): + """Return the last aired episode air date. + + :return: + :rtype: datetime.date + """ + log.debug(u'{id}: Finding the episode which aired last', {'id': self.series_id}) + + main_db_con = db.DBConnection() + sql_results = main_db_con.select( + 'SELECT ' + ' airdate,' + ' season,' + ' episode ' + 'FROM ' + ' tv_episodes ' + 'WHERE ' + ' indexer = ?' + ' AND showid = ? ' + ' AND airdate < ? ' + ' AND status <> ? ' + 'ORDER BY' + ' airdate ' + 'DESC LIMIT 1', + [self.indexer, self.series_id, datetime.date.today().toordinal(), UNAIRED]) + + if sql_results is None or len(sql_results) == 0: + log.debug(u'{id}: Could not find a previous aired episode', {'id': self.series_id}) + self.prev_aired = u'' + else: + log.debug( + u'{id}: Found previous aired episode number {ep}', { + 'id': self.series_id, + 'ep': episode_num(sql_results[0]['season'], sql_results[0]['episode']) + } + ) + self.prev_aired = sql_results[0]['airdate'] + + return self.prev_aired + def next_episode(self): """Return the next episode air date. @@ -1635,14 +1684,13 @@ def next_episode(self): ' indexer = ?' ' AND showid = ? ' ' AND airdate >= ? ' - ' AND status IN (?,?) ' 'ORDER BY' ' airdate ' 'ASC LIMIT 1', - [self.indexer, self.series_id, datetime.date.today().toordinal(), UNAIRED, WANTED]) + [self.indexer, self.series_id, datetime.date.today().toordinal()]) if sql_results is None or len(sql_results) == 0: - log.debug(u'{id}: No episode found... need to implement a show status', + log.debug(u'{id}: Could not find a next episode', {'id': self.series_id}) self.next_aired = u'' else: @@ -2056,6 +2104,7 @@ def to_json(self, detailed=False, episodes=False): data['imdbInfo'] = {to_camel_case(k): v for k, v in viewitems(self.imdb_info)} data['year'] = {} data['year']['start'] = self.imdb_year or self.start_year + data['prevAirDate'] = self.prev_airdate.isoformat() if self.prev_airdate else None data['nextAirDate'] = self.next_airdate.isoformat() if self.next_airdate else None data['lastUpdate'] = datetime.date.fromordinal(self._last_update_indexer).isoformat() data['runtime'] = self.imdb_runtime or self.runtime @@ -2097,6 +2146,9 @@ def to_json(self, detailed=False, episodes=False): data['config']['release']['requiredWordsExclude'] = bool(self.rls_require_exclude) data['config']['airdateOffset'] = self.airdate_offset + # Moved from detailed, as the home page, needs it to display the Xem icon. + data['xemNumbering'] = numbering_tuple_to_dict(self.xem_numbering) + # These are for now considered anime-only options if self.is_anime: bw_list = self.release_groups or BlackAndWhiteList(self) @@ -2112,7 +2164,6 @@ def to_json(self, detailed=False, episodes=False): if detailed: data['size'] = self.size data['showQueueStatus'] = self.show_queue_status - data['xemNumbering'] = numbering_tuple_to_dict(self.xem_numbering) data['sceneAbsoluteNumbering'] = dict_to_array(self.scene_absolute_numbering, key='absolute', value='sceneAbsolute') if self.is_scene: data['xemAbsoluteNumbering'] = dict_to_array(self.xem_absolute_numbering, key='absolute', value='sceneAbsolute') diff --git a/tests/apiv2/__init__.py b/tests/apiv2/__init__.py index ce233ba128..d24cdb4e0a 100644 --- a/tests/apiv2/__init__.py +++ b/tests/apiv2/__init__.py @@ -1,9 +1,23 @@ # coding=utf-8 """Api v2 tests.""" from __future__ import unicode_literals + import os import sys +import six + +# Start event loop in python3 +if six.PY3: + import asyncio + + # We need to set the WindowsSelectorEventLoop event loop on python 3 (3.8 and higher) running on windows + if sys.platform == 'win32': + try: + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + except AttributeError: # Only available since Python 3.7.0 + pass + sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../lib'))) sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../ext'))) sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))) diff --git a/tests/apiv2/test_config.py b/tests/apiv2/test_config.py index 2f2442b9cc..598b3e58e0 100644 --- a/tests/apiv2/test_config.py +++ b/tests/apiv2/test_config.py @@ -33,6 +33,7 @@ def config_main(monkeypatch, app_config): app_config('NAMING_ANIME', 3) section_data = {} + # Can't get rid of this because of the usage of themeName in MEDUSA.config.themeName. section_data['themeName'] = app.THEME_NAME section_data['anonRedirect'] = app.ANON_REDIRECT @@ -72,8 +73,6 @@ def config_main(monkeypatch, app_config): section_data['logs']['subliminalLog'] = bool(app.SUBLIMINAL_LOG) section_data['logs']['privacyLevel'] = app.PRIVACY_LEVEL - section_data['selectedRootIndex'] = int_default(app.SELECTED_ROOT, -1) # All paths - # Added for config - main, needs refactoring in the structure. section_data['launchBrowser'] = bool(app.LAUNCH_BROWSER) section_data['defaultPage'] = app.DEFAULT_PAGE @@ -97,7 +96,7 @@ def config_main(monkeypatch, app_config): section_data['availableThemes'] = [{'name': theme.name, 'version': theme.version, 'author': theme.author} - for theme in app.AVAILABLE_THEMES] + for theme in app.AVAILABLE_THEMES] section_data['timePresets'] = list(time_presets) section_data['datePresets'] = list(date_presets) @@ -755,3 +754,66 @@ def test_config_get_search(http_client, create_url, auth_headers, config_search) # then assert response.code == 200 assert expected == json.loads(response.body) + + +@pytest.fixture +def config_layout(): + section_data = {} + + section_data['schedule'] = app.COMING_EPS_LAYOUT + section_data['history'] = app.HISTORY_LAYOUT + section_data['historyLimit'] = app.HISTORY_LIMIT + + section_data['home'] = app.HOME_LAYOUT + + section_data['show'] = {} + section_data['show']['specials'] = bool(app.DISPLAY_SHOW_SPECIALS) + section_data['show']['showListOrder'] = app.SHOW_LIST_ORDER + section_data['show']['pagination'] = {} + section_data['show']['pagination']['enable'] = bool(app.SHOW_USE_PAGINATION) + + section_data['wide'] = bool(app.LAYOUT_WIDE) + + section_data['posterSortdir'] = int(app.POSTER_SORTDIR or 0) + section_data['themeName'] = app.THEME_NAME + section_data['animeSplitHomeInTabs'] = bool(app.ANIME_SPLIT_HOME_IN_TABS) + section_data['animeSplitHome'] = bool(app.ANIME_SPLIT_HOME) + section_data['fanartBackground'] = bool(app.FANART_BACKGROUND) + section_data['fanartBackgroundOpacity'] = float(app.FANART_BACKGROUND_OPACITY or 0) + section_data['timezoneDisplay'] = app.TIMEZONE_DISPLAY + section_data['dateStyle'] = app.DATE_PRESET + section_data['timeStyle'] = app.TIME_PRESET_W_SECONDS + + section_data['trimZero'] = bool(app.TRIM_ZERO) + section_data['sortArticle'] = bool(app.SORT_ARTICLE) + section_data['fuzzyDating'] = bool(app.FUZZY_DATING) + section_data['posterSortby'] = app.POSTER_SORTBY + + section_data['comingEps'] = {} + section_data['comingEps']['displayPaused'] = bool(app.COMING_EPS_DISPLAY_PAUSED) + section_data['comingEps']['sort'] = app.COMING_EPS_SORT + section_data['comingEps']['missedRange'] = int(app.COMING_EPS_MISSED_RANGE or 0) + section_data['comingEps']['layout'] = app.COMING_EPS_LAYOUT + + section_data['backlogOverview'] = {} + section_data['backlogOverview']['status'] = app.BACKLOG_STATUS + section_data['backlogOverview']['period'] = app.BACKLOG_PERIOD + + section_data['selectedRootIndex'] = int_default(app.SELECTED_ROOT, -1) # All paths + + return section_data + + +@pytest.mark.gen_test +def test_config_get_layout(http_client, create_url, auth_headers, config_layout): + # given + expected = config_layout + + url = create_url('/config/layout') + + # when + response = yield http_client.fetch(url, **auth_headers) + + # then + assert response.code == 200 + assert expected == json.loads(response.body) diff --git a/themes-default/slim/package.json b/themes-default/slim/package.json index 020606badb..7e08d2f017 100644 --- a/themes-default/slim/package.json +++ b/themes-default/slim/package.json @@ -41,20 +41,25 @@ "lodash": "4.17.15", "tablesorter": "2.31.3", "v-tooltip": "2.0.3", + "vanilla-lazyload": "15.2.0", "vue": "2.6.11", "vue-async-computed": "3.8.2", "vue-cookies": "1.7.0", - "vue-good-table": "git+https://github.com/p0psicles/vue-good-table#25a6f282231426fbbdb44d8a6d1927e8abf21e4d", + "vue-good-table": "git+https://github.com/p0psicles/vue-good-table#5cf396f70a5cee003d2541af381f87d4797a7a92", + "vue-images-loaded": "1.1.2", "vue-js-modal": "1.3.35", "vue-js-toggle-button": "1.3.3", "vue-meta": "2.3.3", "vue-multiselect": "2.1.6", "vue-native-websocket": "2.0.14", + "vue-nav-tabs": "0.5.7", "vue-router": "3.1.6", "vue-scrollto": "2.18.1", "vue-snotify": "3.2.1", "vue-template-compiler": "2.6.11", "vue-truncate-collapsed": "2.1.0", + "vuedraggable": "2.23.2", + "vueisotope": "3.1.2", "vuex": "3.4.0" }, "devDependencies": { @@ -64,6 +69,7 @@ "@babel/runtime": "7.9.6", "@mapbox/stylelint-processor-arbitrary-tags": "0.3.0", "@sharkykh/eslint-plugin-vue-extra": "0.1.1", + "@testing-library/jest-dom": "5.7.0", "@vue/test-utils": "1.0.2", "babel-core": "7.0.0-bridge.0", "babel-eslint": "10.1.0", @@ -91,6 +97,7 @@ "imagemin-pngquant": "8.0.0", "jest": "26.0.1", "jest-serializer-vue": "2.0.2", + "jest-vue-matcher": "1.1.0", "mini-css-extract-plugin": "0.9.0", "nyc": "15.0.1", "optimize-css-assets-webpack-plugin": "5.0.3", @@ -156,7 +163,8 @@ ], "coverageReporters": [ "json" - ] + ], + "setupFilesAfterEnv": ["/test/jest/setup.js"] }, "nyc": { "sourceMap": false, diff --git a/themes-default/slim/src/app.js b/themes-default/slim/src/app.js index c14e4d1c47..8168bc6f85 100644 --- a/themes-default/slim/src/app.js +++ b/themes-default/slim/src/app.js @@ -3,6 +3,7 @@ import Vue from 'vue'; import { registerGlobalComponents, registerPlugins } from './global-vue-shim'; import router from './router'; import store from './store'; +import { mapActions, mapMutations, mapState } from 'vuex'; import { isDevelopment } from './utils/core'; Vue.config.devtools = true; @@ -23,7 +24,14 @@ const app = new Vue({ pageComponent: false }; }, + computed: { + ...mapState({ + showsLoading: state => state.shows.loading + }) + }, mounted() { + const { getShows, setLoadingDisplay, setLoadingFinished } = this; + if (isDevelopment) { console.log('App Mounted!'); } @@ -46,6 +54,25 @@ const app = new Vue({ alert('Unable to connect to Medusa!'); // eslint-disable-line no-alert }); } + + // Let's bootstrap the app with essential data. + getShows() + .then(() => { + console.log('Finished loading all shows.'); + setTimeout(() => { + setLoadingFinished(true); + setLoadingDisplay(false); + }, 2000); + }); + }, + methods: { + ...mapActions({ + getShows: 'getShows' + }), + ...mapMutations([ + 'setLoadingDisplay', + 'setLoadingFinished' + ]) } }).$mount('#vue-wrap'); diff --git a/themes-default/slim/src/components/config-notifications.vue b/themes-default/slim/src/components/config-notifications.vue index f5578822bd..c7d684a445 100644 --- a/themes-default/slim/src/components/config-notifications.vue +++ b/themes-default/slim/src/components/config-notifications.vue @@ -972,11 +972,7 @@ export default { }); } }, - created() { - const { getShows } = this; - // Needed for the show-selector component - getShows(); - }, + beforeMount() { // Wait for the next tick, so the component is rendered this.$nextTick(() => { diff --git a/themes-default/slim/src/components/display-show.vue b/themes-default/slim/src/components/display-show.vue index 820b418f7e..05ef1bde97 100644 --- a/themes-default/slim/src/components/display-show.vue +++ b/themes-default/slim/src/components/display-show.vue @@ -7,6 +7,7 @@ import debounce from 'lodash/debounce'; -import formatDate from 'date-fns/format'; -import parseISO from 'date-fns/parseISO'; import { mapState, mapGetters, mapActions } from 'vuex'; import { AppLink, PlotInfo } from './helpers'; -import { humanFileSize, convertDateFormat } from '../utils/core'; +import { humanFileSize } from '../utils/core'; +import { manageCookieMixin } from '../mixins/manage-cookie'; import { addQTip, updateSearchIcons } from '../utils/jquery'; import { VueGoodTable } from 'vue-good-table'; import Backstretch from './backstretch.vue'; import ShowHeader from './show-header.vue'; import SubtitleSearch from './subtitle-search.vue'; -import TimeAgo from 'javascript-time-ago'; -import timeAgoLocalEN from 'javascript-time-ago/locale/en'; import QualityPill from './helpers/quality-pill.vue'; -// Add locale-specific relative date/time formatting rules. -TimeAgo.addLocale(timeAgoLocalEN); - export default { name: 'show', components: { @@ -383,6 +378,9 @@ export default { ShowHeader, QualityPill }, + mixins: [ + manageCookieMixin('displayShow') + ], metaInfo() { if (!this.show || !this.show.title) { return { @@ -414,7 +412,7 @@ export default { const { getCookie } = this; const perPageDropdown = [25, 50, 100, 250, 500]; const getPaginationPerPage = () => { - const rows = getCookie('displayShow-pagination-perPage'); + const rows = getCookie('pagination-perPage'); if (!rows) { return 50; } @@ -433,23 +431,23 @@ export default { field: 'content.hasNfo', type: 'boolean', sortable: false, - hidden: getCookie('displayShow-hide-field-NFO') + hidden: getCookie('NFO') }, { label: 'TBN', field: 'content.hasTbn', type: 'boolean', sortable: false, - hidden: getCookie('displayShow-hide-field-TBN') + hidden: getCookie('TBN') }, { label: 'Episode', field: 'episode', type: 'number', - hidden: getCookie('displayShow-hide-field-Episode') + hidden: getCookie('Episode') }, { label: 'Abs. #', field: 'absoluteNumber', type: 'number', - hidden: getCookie('displayShow-hide-field-Abs. #') + hidden: getCookie('Abs. #') }, { label: 'Scene', field: row => { @@ -457,7 +455,7 @@ export default { return getSceneNumbering(row); }, sortable: false, - hidden: getCookie('displayShow-hide-field-Scene') + hidden: getCookie('Scene') }, { label: 'Scene Abs. #', field: row => { @@ -474,47 +472,47 @@ export default { sortFn(x, y) { return (x < y ? -1 : (x > y ? 1 : 0)); }, - hidden: getCookie('displayShow-hide-field-Scene Abs. #') + hidden: getCookie('Scene Abs. #') }, { label: 'Title', field: 'title', - hidden: getCookie('displayShow-hide-field-Title') + hidden: getCookie('Title') }, { label: 'File', field: 'file.location', - hidden: getCookie('displayShow-hide-field-File') + hidden: getCookie('File') }, { label: 'Size', field: 'file.size', type: 'number', formatFn: humanFileSize, - hidden: getCookie('displayShow-hide-field-Size') + hidden: getCookie('Size') }, { // For now i'm using a custom function the parse it. As the type: date, isn't working for us. // But the goal is to have this user formatted (as configured in backend) label: 'Air date', field: this.parseDateFn, sortable: false, - hidden: getCookie('displayShow-hide-field-Air date') + hidden: getCookie('Air date') }, { label: 'Download', field: 'download', sortable: false, - hidden: getCookie('displayShow-hide-field-Download') + hidden: getCookie('Download') }, { label: 'Subtitles', field: 'subtitles', sortable: false, - hidden: getCookie('displayShow-hide-field-Subtitles') + hidden: getCookie('Subtitles') }, { label: 'Status', field: 'status', - hidden: getCookie('displayShow-hide-field-Status') + hidden: getCookie('Status') }, { label: 'Search', field: 'search', sortable: false, - hidden: getCookie('displayShow-hide-field-Search') + hidden: getCookie('Search') }], perPageDropdown, paginationPerPage: getPaginationPerPage(), @@ -522,8 +520,7 @@ export default { // We need to keep track of which episode where trying to search, for the vue-modal failedSearchEpisode: null, backlogSearchEpisodes: [], - filterByOverviewStatus: false, - timeAgo: new TimeAgo('en-US') + filterByOverviewStatus: false }; }, computed: { @@ -536,7 +533,8 @@ export default { }), ...mapGetters({ show: 'getCurrentShow', - getOverviewStatus: 'getOverviewStatus' + getOverviewStatus: 'getOverviewStatus', + fuzzyParseDateTime: 'fuzzyParseDateTime' }), indexer() { return this.showIndexer || this.$route.query.indexername; @@ -589,12 +587,7 @@ export default { return show.seasons.filter(season => season.season === 0); } }, - created() { - const { getShows } = this; - // Without getting any specific show data, we pick the show needed from the shows array. - // We need to get the complete list of shows anyway, as this is also needed for the show-selector component - getShows(); - }, + mounted() { const { id, @@ -695,7 +688,6 @@ export default { humanFileSize, ...mapActions({ getShow: 'getShow', // Map `this.getShow()` to `this.$store.dispatch('getShow')` - getShows: 'getShows', getEpisodes: 'getEpisodes' }), statusQualityUpdate(event) { @@ -750,24 +742,8 @@ export default { } }, parseDateFn(row) { - const { layout, timeAgo } = this; - const { dateStyle, timeStyle } = layout; - const { fuzzyDating } = layout; - - if (!row.airDate) { - return ''; - } - - if (fuzzyDating) { - return timeAgo.format(new Date(row.airDate)); - } - - if (dateStyle === '%x') { - return new Date(row.airDate).toLocaleString(); - } - - const fdate = parseISO(row.airDate); - return formatDate(fdate, convertDateFormat(`${dateStyle} ${timeStyle}`)); + const { fuzzyParseDateTime } = this; + return fuzzyParseDateTime(row.airDate); }, rowStyleClassFn(row) { const { getOverviewStatus, show } = this; @@ -811,22 +787,22 @@ export default { */ reflowLayout: debounce(function() { console.debug('Reflowing layout'); - - this.$nextTick(() => { - this.movecheckboxControlsBackground(); - }); + this.movecheckboxControlsBackground(); addQTip(); // eslint-disable-line no-undef }, 1000), /** * Adjust the checkbox controls (episode filter) background position */ movecheckboxControlsBackground() { - const height = $('#checkboxControls').height() + 10; - const top = $('#checkboxControls').offset().top - 3; + if (!this.$refs['show-header'].$refs.checkboxControls) { + return; + } + const height = this.$refs['show-header'].$refs.checkboxControls.getBoundingClientRect().height + 10 + 'px'; + const top = this.$refs['show-header'].$refs.checkboxControls.getBoundingClientRect().top + 'px'; - $('#checkboxControlsBackground').height(height); - $('#checkboxControlsBackground').offset({ top, left: 0 }); - $('#checkboxControlsBackground').show(); + this.$root.$refs.checkboxControlsBackground.style.top = top; + this.$root.$refs.checkboxControlsBackground.style.height = height; + this.$root.$refs.checkboxControlsBackground.style.display = 'block'; }, setEpisodeSceneNumbering(forSeason, forEpisode, sceneSeason, sceneEpisode) { const { $snotify, id, indexer, show } = this; @@ -1116,13 +1092,6 @@ export default { return bindData; }, - getCookie(key) { - const cookie = this.$cookies.get(key); - return JSON.parse(cookie); - }, - setCookie(key, value) { - return this.$cookies.set(key, JSON.stringify(value)); - }, updateEpisodeWatched(episode, watched) { const { id, indexer, getEpisodes, show } = this; const patchData = {}; @@ -1142,7 +1111,7 @@ export default { updatePaginationPerPage(rows) { const { setCookie } = this; this.paginationPerPage = rows; - setCookie('displayShow-pagination-perPage', rows); + setCookie('pagination-perPage', rows); }, onPageChange(params) { this.loadEpisodes(params.currentPage); @@ -1210,24 +1179,19 @@ export default { } } } - }, - columns: { - handler: function(newVal) { // eslint-disable-line object-shorthand - // Monitor the columns, to update the cookies, when changed. - const { setCookie } = this; - for (const column of newVal) { - if (column) { - setCookie(`displayShow-hide-field-${column.label}`, column.hidden); - } - } - }, - deep: true } + }, + beforeRouteLeave(to, from, next) { + // The show-header component has a summaryBackground and checkboxControlsBackground element. + // When leaving for another route, we need to hide these. + this.$root.$refs.summaryBackground.style.display = 'none'; + this.$root.$refs.checkboxControlsBackground.style.display = 'none'; + next(); } }; - diff --git a/themes-default/slim/src/components/helpers/index.js b/themes-default/slim/src/components/helpers/index.js index 71e387391f..7d729fe5f7 100644 --- a/themes-default/slim/src/components/helpers/index.js +++ b/themes-default/slim/src/components/helpers/index.js @@ -2,13 +2,16 @@ export { default as AppLink } from './app-link.vue'; export { default as Asset } from './asset.vue'; export { default as ConfigSceneExceptions } from './config-scene-exceptions.vue'; export { default as ConfigTemplate } from './config-template.vue'; -export { default as ConfigTextboxNumber } from './config-textbox-number.vue'; export { default as ConfigTextbox } from './config-textbox.vue'; +export { default as ConfigTextboxNumber } from './config-textbox-number.vue'; export { default as ConfigToggleSlider } from './config-toggle-slider.vue'; export { default as FileBrowser } from './file-browser.vue'; export { default as LanguageSelect } from './language-select.vue'; +export { default as LoadProgressBar } from './load-progress-bar.vue'; export { default as NamePattern } from './name-pattern.vue'; export { default as PlotInfo } from './plot-info.vue'; +export { default as ProgressBar } from './progress-bar.vue'; +export { default as PosterSizeSlider } from './poster-size-slider.vue'; export { default as QualityChooser } from './quality-chooser.vue'; export { default as QualityPill } from './quality-pill.vue'; export { default as ScrollButtons } from './scroll-buttons.vue'; diff --git a/themes-default/slim/src/components/helpers/load-progress-bar.vue b/themes-default/slim/src/components/helpers/load-progress-bar.vue new file mode 100644 index 0000000000..8c0bfb6533 --- /dev/null +++ b/themes-default/slim/src/components/helpers/load-progress-bar.vue @@ -0,0 +1,80 @@ + + + diff --git a/themes-default/slim/src/components/helpers/poster-size-slider.vue b/themes-default/slim/src/components/helpers/poster-size-slider.vue new file mode 100644 index 0000000000..2f87c3e38f --- /dev/null +++ b/themes-default/slim/src/components/helpers/poster-size-slider.vue @@ -0,0 +1,60 @@ + + + diff --git a/themes-default/slim/src/components/helpers/progress-bar.vue b/themes-default/slim/src/components/helpers/progress-bar.vue new file mode 100644 index 0000000000..a599b49c2f --- /dev/null +++ b/themes-default/slim/src/components/helpers/progress-bar.vue @@ -0,0 +1,26 @@ + + + diff --git a/themes-default/slim/src/components/helpers/quality-pill.vue b/themes-default/slim/src/components/helpers/quality-pill.vue index 221ae0bb3d..0dabf12c0d 100644 --- a/themes-default/slim/src/components/helpers/quality-pill.vue +++ b/themes-default/slim/src/components/helpers/quality-pill.vue @@ -4,13 +4,19 @@ - diff --git a/themes-default/slim/src/components/show-header.vue b/themes-default/slim/src/components/show-header.vue index cd3073dbda..2e5e4bc915 100644 --- a/themes-default/slim/src/components/show-header.vue +++ b/themes-default/slim/src/components/show-header.vue @@ -1,5 +1,6 @@