Add ViewManager

- Updated to be more self-contained and work better with unsupported skins
- TODO: Add support for setting default sort order and sort direction

Fix content type not being set to episodes

- Fix #586, #589

Update for restructure of xbmc_plugin

- Only set view mode if directory items successfully added

Fix preselect on view_manager view lists

Update to match new setup wizard

Update for reorganised/renamed constants fca610c

Update for updated XbmcContext.apply_content 8a8247a

Update for new localize and logging methods

Update to fix setting view mode not working if container is still updating

Update to handle sort method and order and workaround #1243
This commit is contained in:
MoojMidge 2025-09-03 12:58:31 +09:00
parent 56c3fa474c
commit bb6999d786
12 changed files with 437 additions and 2 deletions

View file

@ -144,6 +144,7 @@ class AbstractProvider(object):
if last_run and last_run > 1:
self.pre_run_wizard_step(provider=self, context=context)
wizard_steps = self.get_wizard_steps()
wizard_steps.extend(ui.get_view_manager().get_wizard_steps())
step = 0
steps = len(wizard_steps)

View file

@ -177,6 +177,8 @@ PAGE = 'page'
PLAYLIST_IDS = 'playlist_ids'
SCREENSAVER = 'screensaver'
SEEK = 'seek'
SORT_DIR = 'sort_dir'
SORT_METHOD = 'sort_method'
START = 'start'
VIDEO_IDS = 'video_ids'
@ -335,6 +337,8 @@ __all__ = (
'PLAYLIST_IDS',
'SCREENSAVER',
'SEEK',
'SORT_DIR',
'SORT_METHOD',
'START',
'VIDEO_IDS',

View file

@ -11,8 +11,8 @@
from __future__ import absolute_import, division, unicode_literals
VIDEO_CONTENT = 'videos'
LIST_CONTENT = 'files'
VIDEO_CONTENT = 'episodes'
LIST_CONTENT = 'default'
COMMENTS = 'comments'
HISTORY = 'history'

View file

@ -14,6 +14,7 @@ import sys
from . import const_content_types as CONTENT
from ..compatibility import (
xbmc,
xbmcplugin,
)
@ -77,12 +78,26 @@ methods = [
('VIDEO_ORIGINAL_TITLE', 20376, 57),
('VIDEO_ORIGINAL_TITLE_IGNORE_THE', 20376, None),
]
SORT_ID_MAPPING = {}
SORT = sys.modules[__name__]
name = label_id = sort_by = sort_method = None
for name, label_id, sort_by in methods:
sort_method = getattr(xbmcplugin, 'SORT_METHOD_' + name, 0)
setattr(SORT, name, sort_method)
if sort_by is not None:
SORT_ID_MAPPING.update((
(name, sort_by),
(xbmc.getLocalizedString(label_id).lower(), sort_by),
(sort_method, sort_by if sort_method else 0),
))
SORT_ID_MAPPING.update((
(CONTENT.VIDEO_CONTENT.join(('__', '__')), SORT.UNSORTED),
(CONTENT.LIST_CONTENT.join(('__', '__')), SORT.LABEL),
(CONTENT.COMMENTS.join(('__', '__')), SORT.CHANNEL),
(CONTENT.HISTORY.join(('__', '__')), SORT.LASTPLAYED),
))
# Label mask token details:
# https://github.com/xbmc/xbmc/blob/master/xbmc/utils/LabelFormatter.cpp#L33-L105
@ -179,6 +194,7 @@ COMMENTS_CONTENT_SIMPLE = (
del (
sys,
CONTENT,
xbmc,
xbmcplugin,
methods,
SORT,

View file

@ -61,6 +61,8 @@ from ..constants import (
PLAY_USING,
SCREENSAVER,
SEEK,
SORT_DIR,
SORT_METHOD,
START,
SUBSCRIPTION_ID,
VIDEO_ID,
@ -169,6 +171,8 @@ class AbstractContext(object):
'q',
'rating',
'reload_path',
SORT_DIR,
SORT_METHOD,
'search_type',
SUBSCRIPTION_ID,
'uri',

View file

@ -701,6 +701,7 @@ class XbmcContext(AbstractContext):
path=self.get_path())
if content_type != 'default':
xbmcplugin.setContent(self._plugin_handle, content_type)
ui.get_view_manager().set_view_mode(content_type)
if category_label is None:
category_label = self.get_param('category_label')

View file

@ -34,6 +34,8 @@ from ...constants import (
REFRESH_CONTAINER,
RELOAD_ACCESS_MANAGER,
REROUTE_PATH,
SORT_DIR,
SORT_METHOD,
SYNC_LISTITEM,
TRAKT_PAUSE_FLAG,
VIDEO_ID,
@ -424,6 +426,37 @@ class XbmcPlugin(AbstractPlugin):
},
))
# set alternative view mode
view_manager = ui.get_view_manager()
if view_manager.is_override_view_enabled():
post_run_actions.append((
view_manager.apply_view_mode,
{
'context': context,
},
))
if is_same_path:
sort_method = kwargs.get(SORT_METHOD)
if sort_method:
post_run_actions.append((
view_manager.apply_sort_method,
{
'context': context,
SORT_METHOD: sort_method,
},
))
sort_dir = kwargs.get(SORT_DIR)
if sort_dir:
post_run_actions.append((
view_manager.apply_sort_dir,
{
'context': context,
SORT_DIR: sort_dir,
},
))
if post_run_actions:
self.post_run(context, ui, *post_run_actions)
return succeeded

View file

@ -17,6 +17,8 @@ from .constants import (
CHECK_SETTINGS,
FOLDER_URI,
PATHS,
SORT_DIR,
SORT_METHOD,
)
from .context import XbmcContext
from .debug import Profiler
@ -90,6 +92,20 @@ def run(context=_context,
refresh = context.refresh_requested(force=True, off=True, params=params)
new_params['refresh'] = refresh if refresh else 0
sort_method = (
params.get(SORT_METHOD)
or ui.get_infolabel('Container.SortMethod')
)
if sort_method:
new_kwargs[SORT_METHOD] = sort_method.lower()
sort_dir = (
params.get(SORT_DIR)
or ui.get_infolabel('Container.SortOrder')
)
if sort_dir:
new_kwargs[SORT_DIR] = sort_dir.lower()
if new_params:
context.set_params(**new_params)

View file

@ -22,6 +22,9 @@ class AbstractContextUI(object):
message_template=None):
raise NotImplementedError()
def get_view_manager(self):
raise NotImplementedError()
@staticmethod
def on_keyboard_input(title, default='', hidden=False):
raise NotImplementedError()

View file

@ -0,0 +1,320 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2014-2016 bromix (plugin.video.youtube)
Copyright (C) 2016-2025 plugin.video.youtube
SPDX-License-Identifier: GPL-2.0-only
See LICENSES/GPL-2.0-only for more information.
"""
from __future__ import absolute_import, division, unicode_literals
from ... import logging
from ...compatibility import xbmc
from ...constants import CONTENT, SORT, SORT_DIR, SORT_METHOD
class ViewManager(object):
log = logging.getLogger(__name__)
SETTINGS = {
'override': 'kodion.view.override', # (bool)
'view_default': 'kodion.view.default', # (int)
'view_type': 'kodion.view.{0}', # (int)
}
SUPPORTED_TYPES_MAP = {
CONTENT.LIST_CONTENT: 'default',
CONTENT.VIDEO_CONTENT: 'episodes',
}
STRING_MAP = {
'prompt': 30777,
'unsupported_skin': 10109,
'supported_skin': 14240,
'albums': 30035,
'artists': 30034,
'default': 30027,
'episodes': 30028,
'movies': 30029,
'songs': 30033,
'tvshows': 30032,
}
SKIN_DATA = {
'skin.confluence': {
'default': (
{'name': 'List', 'id': 50},
{'name': 'Big List', 'id': 51},
{'name': 'Thumbnail', 'id': 500}
),
'movies': (
{'name': 'List', 'id': 50},
{'name': 'Big List', 'id': 51},
{'name': 'Thumbnail', 'id': 500},
{'name': 'Media info', 'id': 504},
{'name': 'Media info 2', 'id': 503}
),
'episodes': (
{'name': 'List', 'id': 50},
{'name': 'Big List', 'id': 51},
{'name': 'Thumbnail', 'id': 500},
{'name': 'Media info', 'id': 504},
{'name': 'Media info 2', 'id': 503}
),
'tvshows': (
{'name': 'List', 'id': 50},
{'name': 'Big List', 'id': 51},
{'name': 'Thumbnail', 'id': 500},
{'name': 'Poster', 'id': 500},
{'name': 'Wide', 'id': 505},
{'name': 'Media info', 'id': 504},
{'name': 'Media info 2', 'id': 503},
{'name': 'Fanart', 'id': 508}
),
'musicvideos': (
{'name': 'List', 'id': 50},
{'name': 'Big List', 'id': 51},
{'name': 'Thumbnail', 'id': 500},
{'name': 'Media info', 'id': 504},
{'name': 'Media info 2', 'id': 503}
),
'songs': (
{'name': 'List', 'id': 50},
{'name': 'Big List', 'id': 51},
{'name': 'Thumbnail', 'id': 500},
{'name': 'Media info', 'id': 506}
),
'albums': (
{'name': 'List', 'id': 50},
{'name': 'Big List', 'id': 51},
{'name': 'Thumbnail', 'id': 500},
{'name': 'Media info', 'id': 506}
),
'artists': (
{'name': 'List', 'id': 50},
{'name': 'Big List', 'id': 51},
{'name': 'Thumbnail', 'id': 500},
{'name': 'Media info', 'id': 506}
)
},
'skin.aeon.nox.5': {
'default': (
{'name': 'List', 'id': 50},
{'name': 'Episodes', 'id': 502},
{'name': 'LowList', 'id': 501},
{'name': 'BannerWall', 'id': 58},
{'name': 'Shift', 'id': 57},
{'name': 'Posters', 'id': 56},
{'name': 'ShowCase', 'id': 53},
{'name': 'Landscape', 'id': 52},
{'name': 'InfoWall', 'id': 51}
)
},
'skin.xperience1080+': {
'default': (
{'name': 'List', 'id': 50},
{'name': 'Thumbnail', 'id': 500},
),
'episodes': (
{'name': 'List', 'id': 50},
{'name': 'Info list', 'id': 52},
{'name': 'Fanart', 'id': 502},
{'name': 'Landscape', 'id': 54},
{'name': 'Poster', 'id': 55},
{'name': 'Thumbnail', 'id': 500},
{'name': 'Banner', 'id': 60}
),
},
'skin.xperience1080': {
'default': (
{'name': 'List', 'id': 50},
{'name': 'Thumbnail', 'id': 500},
),
'episodes': (
{'name': 'List', 'id': 50},
{'name': 'Info list', 'id': 52},
{'name': 'Fanart', 'id': 502},
{'name': 'Landscape', 'id': 54},
{'name': 'Poster', 'id': 55},
{'name': 'Thumbnail', 'id': 500},
{'name': 'Banner', 'id': 60}
),
},
'skin.estuary': {
'default': (
{'name': 'IconWall', 'id': 52},
{'name': 'WideList', 'id': 55},
),
'videos': (
{'name': 'Shift', 'id': 53},
{'name': 'InfoWall', 'id': 54},
{'name': 'WideList', 'id': 55},
{'name': 'Wall', 'id': 500},
),
'episodes': (
{'name': 'InfoWall', 'id': 54},
{'name': 'Wall', 'id': 500},
{'name': 'WideList', 'id': 55},
)
}
}
def __init__(self, context):
self._context = context
self._view_mode = None
def is_override_view_enabled(self):
return self._context.get_settings().get_bool(self.SETTINGS['override'])
def get_wizard_steps(self):
return (self.run,)
def run(self, context, step, steps, **_kwargs):
localize = context.localize
skin_id = xbmc.getSkinDir()
if skin_id in self.SKIN_DATA:
status = localize(self.STRING_MAP['supported_skin'])
else:
status = localize(self.STRING_MAP['unsupported_skin'])
prompt_text = localize(self.STRING_MAP['prompt'], (skin_id, status))
step += 1
if context.get_ui().on_yes_no_input(
'{youtube} - {setup_wizard} ({step}/{steps})'.format(
youtube=localize('youtube'),
setup_wizard=localize('setup_wizard'),
step=step,
steps=steps,
),
localize('setup_wizard.prompt.x', prompt_text)
):
for view_type in self.SUPPORTED_TYPES_MAP:
self.update_view_mode(skin_id, view_type)
return step
def get_view_mode(self):
if self._view_mode is None:
self.set_view_mode()
return self._view_mode
def set_view_mode(self, view_type='default'):
settings = self._context.get_settings()
default = settings.get_int(self.SETTINGS['view_default'], 50)
if view_type == 'default':
view_mode = default
else:
view_type = self.SUPPORTED_TYPES_MAP.get(view_type, 'default')
view_mode = settings.get_int(
self.SETTINGS['view_type'].format(view_type), default
)
self._view_mode = view_mode
def update_view_mode(self, skin_id, view_type='default'):
view_id = -1
settings = self._context.get_settings()
ui = self._context.get_ui()
content_type = self.SUPPORTED_TYPES_MAP[view_type]
if content_type not in self.STRING_MAP:
self.log.warning('Unsupported content type: %r', content_type)
return False
title = self._context.localize(self.STRING_MAP[content_type])
view_setting = self.SETTINGS['view_type'].format(content_type)
current_value = settings.get_int(view_setting)
if current_value == -1:
self.log.warning('No setting for content type: %r', content_type)
return False
skin_data = self.SKIN_DATA.get(skin_id, {})
view_type_data = skin_data.get(view_type) or skin_data.get(content_type)
if view_type_data:
items = []
preselect = -1
for view_data in view_type_data:
view_id = view_data['id']
items.append((view_data['name'], view_id))
if view_id == current_value:
preselect = len(items) - 1
view_id = ui.on_select(title, items, preselect=preselect)
else:
self.log.warning('Unsupported view: %r', view_type)
if view_id == -1:
result, view_id = ui.on_numeric_input(title, current_value)
if not result:
return False
if view_id > -1:
settings.set_int(view_setting, view_id)
settings.set_bool(self.SETTINGS['override'], True)
return True
return False
def apply_view_mode(self, context):
view_mode = self.get_view_mode()
if view_mode is None:
return
self.log.debug('Applying view mode: %r', view_mode)
context.execute('Container.SetViewMode(%s)' % view_mode)
@classmethod
def apply_sort_method(cls, context, **kwargs):
execute = context.execute
get_infolabel = xbmc.getInfoLabel
sort_method = (kwargs.get(SORT_METHOD)
or CONTENT.VIDEO_CONTENT.join(('__', '__'))).lower()
sort_id = SORT.SORT_ID_MAPPING.get(sort_method)
if sort_id is None:
cls.log.warning('Unknown sort method: %r', sort_method)
return
_sort_method = get_infolabel('Container.SortMethod').lower()
_sort_id = SORT.SORT_ID_MAPPING.get(_sort_method)
# Check if hardcoded sort method to ID mapping has changed
if not xbmc.getCondVisibility('Container.SortMethod(%s)' % _sort_id):
cls.log.warning('Sort method mismatch: {method!r} ID is not {id}',
method=_sort_method,
id=_sort_id)
# Workaround for Container.SetSortMethod failing for some sort methods
num_attempts_remaining = 3
while num_attempts_remaining and not context.sleep(0.1):
cls.log.debug('Applying sort method: {method!r} ({id})',
method=sort_method,
id=sort_id)
# Workaround for Container.SetSortMethod(0) being a noop
# https://github.com/xbmc/xbmc/blob/7e1a55cb861342cd9062745161d88aca08dcead1/xbmc/windows/GUIMediaWindow.cpp#L502
if sort_id == 0:
# Sort by track number to reset sort order to default order
_sort_id = SORT.SORT_ID_MAPPING.get(SORT.TRACKNUM)
execute('Container.SetSortMethod(%s)' % _sort_id)
# Then switch to previous sort method which is default/unsorted
# as per the order set in XbmcContext.apply_content
execute('Container.PreviousSortMethod')
else:
execute('Container.SetSortMethod(%s)' % sort_id)
if get_infolabel('Container.SortMethod').lower() == sort_method:
break
num_attempts_remaining -= 1
else:
cls.log.warning('Unable to apply sort method: {method!r} ({id})',
method=sort_method,
id=sort_id)
@classmethod
def apply_sort_dir(cls, context, **kwargs):
sort_dir = kwargs.get(SORT_DIR, 'ascending')
if not xbmc.getCondVisibility('Container.SortDirection(%s)' % sort_dir):
cls.log.debug('Applying sort direction: %r', sort_dir)
# This builtin should be Container.SortDirection but has been broken
# since Kodi v16
# https://github.com/xbmc/xbmc/commit/ac870b64b16dfd0fc2bd0496c14529cf6d563f41
context.execute('Container.SetSortDirection')

View file

@ -12,6 +12,7 @@ from __future__ import absolute_import, division, unicode_literals
from weakref import proxy
from .view_manager import ViewManager
from ..abstract_context_ui import AbstractContextUI
from ... import logging
from ...compatibility import string_type, xbmc, xbmcgui
@ -45,6 +46,7 @@ class XbmcContextUI(AbstractContextUI):
def __init__(self, context):
super(XbmcContextUI, self).__init__()
self._context = context
self._view_manager = None
def create_progress_dialog(self,
heading,
@ -75,6 +77,12 @@ class XbmcContextUI(AbstractContextUI):
),
)
def get_view_manager(self):
if self._view_manager is None:
self._view_manager = ViewManager(self._context)
return self._view_manager
@staticmethod
def on_keyboard_input(title, default='', hidden=False):
# Starting with Gotham (13.X > ...)

View file

@ -896,6 +896,35 @@
</constraints>
<control format="string" type="spinner"/>
</setting>
<setting id="kodion.view.override" type="boolean" label="30026" help="">
<level>0</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="kodion.view.default" type="integer" parent="kodion.view.override" label="30027" help="">
<level>0</level>
<default>55</default>
<dependencies>
<dependency type="enable">
<condition setting="kodion.view.override" operator="is">true</condition>
</dependency>
</dependencies>
<control format="integer" type="edit">
<heading>30027</heading>
</control>
</setting>
<setting id="kodion.view.episodes" type="integer" parent="kodion.view.override" label="30028" help="">
<level>0</level>
<default>55</default>
<dependencies>
<dependency type="enable">
<condition setting="kodion.view.override" operator="is">true</condition>
</dependency>
</dependencies>
<control format="integer" type="edit">
<heading>30028</heading>
</control>
</setting>
</group>
<group id="regional" label="14222">
<setting id="youtube.language_region.configure" type="action" label="30527" help="">