Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:X0F:branches:multimedia
trackma
trackma_gtk-next-ep.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File trackma_gtk-next-ep.patch of Package trackma
diff --git a/pyproject.toml b/pyproject.toml index 7cf9191..2f3a468 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,8 +17,6 @@ classifiers = [ "Topic :: Internet", "Topic :: Multimedia", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Operating System :: POSIX", @@ -27,7 +25,7 @@ classifiers = [ [tool.poetry.dependencies] -python = "^3.8" +python = "^3.10" requests = "^2.28.1" inotify = { version = "^0.2.10", optional = true } pillow = { version = ">9", optional = true } diff --git a/trackma/ui/gtk/mainview.py b/trackma/ui/gtk/mainview.py index 0a7897b..345722c 100644 --- a/trackma/ui/gtk/mainview.py +++ b/trackma/ui/gtk/mainview.py @@ -30,12 +30,11 @@ from trackma.ui.gtk.showtreeview import ShowListFilter, ShowListStore, ShowTreeV @Gtk.Template.from_file(os.path.join(gtk_dir, 'data/mainview.ui')) class MainView(Gtk.Box): - __gtype_name__ = 'MainView' __gsignals__ = { 'error': (GObject.SignalFlags.RUN_FIRST, None, - (str, )), + (str,)), 'success': (GObject.SignalFlags.RUN_CLEANUP, None, ()), 'error-fatal': (GObject.SignalFlags.RUN_FIRST, None, @@ -98,6 +97,7 @@ class MainView(Gtk.Box): self._engine_reload(account, mediatype, extern_widget) def _init_widgets(self): + self._visible_column_reset() self.image_box = ImageBox(100, 150) self.image_box.show() self.image_container_box.pack_start(self.image_box, False, False, 0) @@ -559,13 +559,26 @@ class MainView(Gtk.Box): utils.save_config(self._config, self._configfile) + def _visible_column_reset(self): + """Should be called when column naming scheme changes to make sure that the default columns are + visible by default, regardless of config + + In 1.1: + Default column 'Progress' was renamed to 'Watched', + Default column 'Percent' was renamed to 'Progress'. + 'New episode' added to default columns""" + column_version = '1.1' # Column naming version number + if self._config['column_version'] != column_version: + self._config['visible_columns'] = utils.gtk_defaults['visible_columns'] + self._config['column_version'] = column_version + class NotebookPage(Gtk.ScrolledWindow): __gtype_name__ = 'NotebookPage' __gsignals__ = { 'show-selected': (GObject.SignalFlags.RUN_FIRST, None, - (int, )), + (int,)), 'show-action': (GObject.SignalFlags.RUN_FIRST, None, (int, object)), 'column-toggled': (GObject.SignalFlags.RUN_FIRST, None, @@ -580,7 +593,8 @@ class NotebookPage(Gtk.ScrolledWindow): self._selected_show = 0 self._list = _list self._title = title - self._title_text = self._engine.mediainfo['statuses_dict'][status] if status in self._engine.mediainfo['statuses_dict'].keys( + self._title_text = self._engine.mediainfo['statuses_dict'][status] if status in self._engine.mediainfo[ + 'statuses_dict'].keys( ) else 'All' self._init_widgets(page_num, status, config) @@ -592,14 +606,9 @@ class NotebookPage(Gtk.ScrolledWindow): self._show_tree_view = ShowTreeView( config['colors'], config['visible_columns'], - config['episodebar_style']) - self._show_tree_view.set_model( - Gtk.TreeModelSort( - model=ShowListFilter( - status=self.status, - child_model=self._list - ) - ) + self.status, + self._list, + config['episodebar_style'], ) self._title.set_text('%s (%d)' % ( self._title_text, @@ -687,17 +696,17 @@ class NotebookPage(Gtk.ScrolledWindow): show = self._engine.get_show_info(self._selected_show) menu = Gtk.Menu() - mb_play = Gtk.ImageMenuItem('Play Next', + mb_play = Gtk.ImageMenuItem('Play next episode', Gtk.Image.new_from_icon_name( "media-playback-start", Gtk.IconSize.MENU)) mb_play.connect("activate", self._on_mb_activate, ShowEventType.PLAY_NEXT) - mb_info = Gtk.MenuItem("Show details...") + mb_info = Gtk.MenuItem("Show details") mb_info.connect("activate", self._on_mb_activate, ShowEventType.DETAILS) - mb_web = Gtk.MenuItem("Open web site") + mb_web = Gtk.MenuItem("Open on " + self._engine.api_info['name']) mb_web.connect("activate", self._on_mb_activate, ShowEventType.OPEN_WEBSITE) @@ -709,7 +718,7 @@ class NotebookPage(Gtk.ScrolledWindow): mb_copy.connect("activate", self._on_mb_activate, ShowEventType.COPY_TITLE) - mb_alt_title = Gtk.MenuItem("Set alternate title...") + mb_alt_title = Gtk.MenuItem("Set alternate title") mb_alt_title.connect("activate", self._on_mb_activate, ShowEventType.CHANGE_ALTERNATIVE_TITLE) diff --git a/trackma/ui/gtk/showtreeview.py b/trackma/ui/gtk/showtreeview.py index 652fd6d..0ff6c21 100644 --- a/trackma/ui/gtk/showtreeview.py +++ b/trackma/ui/gtk/showtreeview.py @@ -19,32 +19,61 @@ from gi.repository import GObject, Gdk, Gtk, Pango from trackma import utils +# Declare named constants for the tree references, so it's more unified and easier to read +# Putting them in their own class also enables whatever we might want to do with it in the future. +class TreeConstants: + SHOW_ID = 0 # Show ID + TITLE = 1 # Show title + MY_PROGRESS = 2 # Number of watched episodes + MY_SCORE = 3 # User given score + WATCHED_EPISODES_FRACTION = 4 # Watched episodes / total episodes. E.g: 7 / 13 + MY_SCORE_STRING = 5 # User given score (string), with 'self.decimals' decimals + TOTAL_EPS = 6 # Total number of episodes + AIRED_EPS = 7 # (Estimated) number of episodes aired. + # If no number is provided by the lib, 1 episode / week is assumed and calculated accordingly + AVAILABLE_EPS = 8 # Number of available episodes in the local library + COLOR = 9 # Used with the _get_color method to return the color preset for a show + PROGRESS_PERCENTAGE = 10 # % of episodes watched. 7 / 13 -> + START_DATE = 11 # Start date of the show + END_DATE = 12 # End date of the show + MY_START_DATE = 13 # Date when the user started watching the show + MY_FINISH_DATE = 14 # Date when the user finished the show + MY_STATUS = 15 # User's show status (watching, paused, completed etc.) + SHOW_STATUS = 16 # Show's status (airing, upcoming etc.) + NEXT_EPISODE_AIR_TIME_RELATIVE = 17 # Relative time until the next episode as 'X days / hours / minutes' + + class ShowListStore(Gtk.ListStore): + # Determines the structure of the tree and holds the actual data after it is appended from row[]. + # Entry order must match with row[]. __cols = ( - ('id', int), - ('title', str), - ('stat', int), - ('score', float), - ('stat-text', str), - ('score-text', str), - ('total-eps', int), - ('subvalue', int), - ('avail-eps', GObject.TYPE_PYOBJECT), - ('color', str), - ('stat-pcent', int), - ('start', str), - ('end', str), - ('my-start', str), - ('my-end', str), - ('my-status', str), - ('status', int), + (TreeConstants.SHOW_ID, int), + (TreeConstants.TITLE, str), + (TreeConstants.MY_PROGRESS, int), + (TreeConstants.MY_SCORE, float), + (TreeConstants.WATCHED_EPISODES_FRACTION, str), + (TreeConstants.MY_SCORE_STRING, str), + (TreeConstants.TOTAL_EPS, int), + (TreeConstants.AIRED_EPS, int), + (TreeConstants.AVAILABLE_EPS, GObject.TYPE_PYOBJECT), + (TreeConstants.COLOR, str), + (TreeConstants.PROGRESS_PERCENTAGE, int), + (TreeConstants.START_DATE, str), + (TreeConstants.END_DATE, str), + (TreeConstants.MY_START_DATE, str), + (TreeConstants.MY_FINISH_DATE, str), + (TreeConstants.MY_STATUS, str), + (TreeConstants.SHOW_STATUS, int), + (TreeConstants.NEXT_EPISODE_AIR_TIME_RELATIVE, str), ) - def __init__(self, decimals=0, colors=dict()): + def __init__(self, decimals=0, colors=None): super().__init__(*self.__class__.__columns__()) + if colors is None: + colors = dict() self.colors = colors self.decimals = decimals - self.set_sort_column_id(1, Gtk.SortType.ASCENDING) + self.set_sort_column_id(TreeConstants.TITLE, Gtk.SortType.ASCENDING) @staticmethod def format_date(date): @@ -63,7 +92,7 @@ class ShowListStore(Gtk.ListStore): @classmethod def column(cls, key): try: - return cls.__cols.index(next(i for i in cls.__cols if i[0] == key)) + return cls.__cols.index(next(i for i in cls.__cols if i[TreeConstants.SHOW_ID] == key)) except ValueError: return None @@ -80,10 +109,11 @@ class ShowListStore(Gtk.ListStore): return None def append(self, show, altname=None, eps=None): - episodes_str = "{} / {}".format(show['my_progress'], - show['total'] or '?') + watched_episodes_fraction = "{} / {}".format(show['my_progress'], + show['total'] or '?') if show['total'] and show['my_progress'] <= show['total']: - progress = (float(show['my_progress']) / show['total']) * 100 + progress_float = (show['my_progress'] / show['total']) * 100 + progress = int(progress_float) else: progress = 0 @@ -104,11 +134,16 @@ class ShowListStore(Gtk.ListStore): my_start_date = self.format_date(show['my_start_date']) my_finish_date = self.format_date(show['my_finish_date']) + # Gets the (short) relative airing time of the next episode compared to UTC + next_episode_air_time_relative = utils.calculate_relative_time(show['next_ep_time'], + utc=True, fulltime=False) + + # Corresponds to __cols, but is used locally to store the data before appending. row = [show['id'], title_str, show['my_progress'], show['my_score'], - episodes_str, + watched_episodes_fraction, score_str, show['total'], aired_eps, @@ -120,13 +155,14 @@ class ShowListStore(Gtk.ListStore): my_start_date, my_finish_date, show['my_status'], - show['status'] + show['status'], + next_episode_air_time_relative, ] super().append(row) def update_or_append(self, show): for row in self: - if int(row[0]) == show['id']: + if int(row[TreeConstants.SHOW_ID]) == show['id']: self.update(show, row) return self.append(show) @@ -134,49 +170,47 @@ class ShowListStore(Gtk.ListStore): def update(self, show, row=None): if not row: for row in self: - if int(row[0]) == show['id']: + if int(row[TreeConstants.SHOW_ID]) == show['id']: break - if row and int(row[0]) == show['id']: + if row and int(row[TreeConstants.SHOW_ID]) == show['id']: episodes_str = "{} / {}".format(show['my_progress'], show['total'] or '?') - row[2] = show['my_progress'] - row[4] = episodes_str + row[TreeConstants.MY_PROGRESS] = show['my_progress'] + row[TreeConstants.WATCHED_EPISODES_FRACTION] = episodes_str score_str = "%0.*f" % (self.decimals, show['my_score']) - row[3] = show['my_score'] - row[5] = score_str - row[9] = self._get_color(show, row[8]) - row[15] = show['my_status'] + row[TreeConstants.MY_SCORE] = show['my_score'] + row[TreeConstants.MY_SCORE_STRING] = score_str + row[TreeConstants.COLOR] = self._get_color(show, row[TreeConstants.AVAILABLE_EPS]) + row[TreeConstants.MY_STATUS] = show['my_status'] return - # print("Warning: Show ID not found in ShowView (%d)" % show['id']) - def update_title(self, show, altname=None): for row in self: - if int(row[0]) == show['id']: + if int(row[TreeConstants.SHOW_ID]) == show['id']: if altname: title_str = "%s [%s]" % (show['title'], altname) else: title_str = show['title'] - row[1] = title_str + row[TreeConstants.SHOW_ID] = title_str return def remove(self, show=None, show_id=None): for row in self: - if int(row[0]) == (show['id'] if show is not None else show_id): + if int(row[TreeConstants.SHOW_ID]) == (show['id'] if show is not None else show_id): Gtk.ListStore.remove(self, row.iter) return def playing(self, show, is_playing): # Change the color if the show is currently playing for row in self: - if int(row[0]) == show['id']: + if int(row[TreeConstants.SHOW_ID]) == show['id']: if is_playing: - row[9] = self.colors['is_playing'] + row[TreeConstants.COLOR] = self.colors['is_playing'] else: - row[9] = self._get_color(show, row[8]) + row[TreeConstants.COLOR] = self._get_color(show, row[TreeConstants.AVAILABLE_EPS]) return @@ -190,7 +224,7 @@ class ShowListFilter(Gtk.TreeModelFilter): self._status = status def status_filter(self, model, iterator, data): - return self._status is None or model[iterator][15] == self._status + return self._status is None or model[iterator][TreeConstants.MY_STATUS] == self._status def get_value(self, obj, key='id'): try: @@ -207,102 +241,132 @@ class ShowTreeView(Gtk.TreeView): __gsignals__ = {'column-toggled': (GObject.SignalFlags.RUN_LAST, GObject.TYPE_PYOBJECT, (GObject.TYPE_STRING, GObject.TYPE_BOOLEAN))} - def __init__(self, colors, visible_columns, progress_style=1): + def __init__(self, colors, visible_columns, status, _list, progress_style=1): + # Sets up the tree Gtk.TreeView.__init__(self) self.colors = colors self.visible_columns = visible_columns self.progress_style = progress_style + self.status = status + self._list = _list + + self.set_model( + Gtk.TreeModelSort( + model=ShowListFilter( + status=self.status, + child_model=self._list + ) + ) + ) self.set_enable_search(True) - self.set_search_column(1) + self.set_search_column(TreeConstants.TITLE) self.set_property('has-tooltip', True) self.connect('query-tooltip', self.show_tooltip) + self.previous_sort_column = 'Title' # Sets the default "0th" previous column for sorting self.cols = dict() + + # Defines the default column order as well. If the default visible columns are renamed or otherwise changed, + # _default_column_reset() in mainview.py should be changed to accommodate the new names. self.available_columns = ( - ('Title', 1), - ('Progress', 2), - ('Score', 3), - ('Percent', 10), - ('Start', 11), - ('End', 12), - ('My start', 13), - ('My end', 14), + ('Title', TreeConstants.TITLE), + ('Watched', TreeConstants.MY_PROGRESS), + ('Score', TreeConstants.MY_SCORE), + ('Next episode', TreeConstants.NEXT_EPISODE_AIR_TIME_RELATIVE), + ('Start', TreeConstants.START_DATE), + ('End', TreeConstants.END_DATE), + ('My start', TreeConstants.MY_START_DATE), + ('My end', TreeConstants.MY_FINISH_DATE), + ('Progress', TreeConstants.PROGRESS_PERCENTAGE), ) - for (name, sort) in self.available_columns: - self.cols[name] = Gtk.TreeViewColumn(name) - self.cols[name].set_sort_column_id(sort) + # Creates pre-defined columns + for (name, key) in self.available_columns: + self.cols[name] = Gtk.TreeViewColumn() + self.cols[name].set_clickable(True) + self.cols[name].connect("clicked", lambda _, column_key=key, column=self.cols[name]: + self._on_column_clicked(column_key, column)) + self.cols[name].set_alignment(0.5) + self.cols[name].set_title(name) + + # Set up the percent / progress bar + if name == 'Progress': + if self.progress_style == 0: + renderer = Gtk.CellRendererProgress() + self.cols[name].pack_start(renderer, False) + self.cols[name].add_attribute(renderer, 'value', TreeConstants.PROGRESS_PERCENTAGE) + else: + renderer = ProgressCellRenderer(self.colors) + self.cols[name].pack_start(renderer, False) + self.cols[name].add_attribute(renderer, 'value', TreeConstants.MY_PROGRESS) + self.cols[name].add_attribute(renderer, 'total', TreeConstants.TOTAL_EPS) + self.cols[name].add_attribute(renderer, 'subvalue', TreeConstants.AIRED_EPS) + self.cols[name].add_attribute(renderer, 'eps', TreeConstants.AVAILABLE_EPS) + else: + renderer = Gtk.CellRendererText() + self.cols[name].pack_start(renderer, False) + renderer.set_alignment(0.5, 0.5) + + if name not in self.visible_columns: + self.cols[name].set_visible(False) + + # Populate columns + match name: + case 'Title': + self.cols[name].set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) + self.cols[name].set_resizable(True) + self.cols[name].set_expand(True) + self.cols[name].add_attribute(renderer, 'text', TreeConstants.TITLE) + self.cols[name].add_attribute(renderer, 'foreground', TreeConstants.COLOR) + renderer.set_property('ellipsize', Pango.EllipsizeMode.END) + self.cols[name].set_alignment(0) + renderer.set_alignment(0, 0.5) + self.cols[name].set_sort_indicator(True) + + case 'Watched': + self.cols[name].add_attribute(renderer, 'text', TreeConstants.WATCHED_EPISODES_FRACTION) + + case 'Progress': + self.cols[name].set_min_width(200) + + case 'Score': + self.cols[name].add_attribute(renderer, 'text', TreeConstants.MY_SCORE_STRING) + + case 'Start': + self.cols[name].add_attribute(renderer, 'text', TreeConstants.START_DATE) + + case 'End': + self.cols[name].add_attribute(renderer, 'text', TreeConstants.END_DATE) + + case 'My start': + self.cols[name].add_attribute(renderer, 'text', TreeConstants.MY_START_DATE) + + case 'My end': + self.cols[name].add_attribute(renderer, 'text', TreeConstants.MY_FINISH_DATE) + + case 'Next episode': + self.cols[name].add_attribute(renderer, 'text', TreeConstants.NEXT_EPISODE_AIR_TIME_RELATIVE) + self.get_model().set_sort_func(sort_column_id=TreeConstants.NEXT_EPISODE_AIR_TIME_RELATIVE, + sort_func=self._next_episode_sort_func, user_data=self) + + case _: + pass # This is a hack to allow for right-clickable header label = Gtk.Label(name) label.show() self.cols[name].set_widget(label) - - self.append_column(self.cols[name]) - w = self.cols[name].get_widget() while not isinstance(w, Gtk.Button): w = w.get_parent() + w.connect('button-press-event', self._header_right_click) - w.connect('button-press-event', self._header_button_press) - - if name not in self.visible_columns: - self.cols[name].set_visible(False) + # Appends populated columns + self.append_column(self.cols[name]) - # renderer_id = Gtk.CellRendererText() - # self.cols['ID'].pack_start(renderer_id, False, True, 0) - # self.cols['ID'].set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) - # self.cols['ID'].set_expand(False) - # self.cols['ID'].add_attribute(renderer_id, 'text', 0) - - renderer_title = Gtk.CellRendererText() - self.cols['Title'].pack_start(renderer_title, False) - self.cols['Title'].set_resizable(True) - self.cols['Title'].set_sizing(Gtk.TreeViewColumnSizing.FIXED) - self.cols['Title'].set_expand(True) - self.cols['Title'].add_attribute(renderer_title, 'text', 1) - # Using foreground-gdk does not work, possibly due to the timing of it being set - self.cols['Title'].add_attribute(renderer_title, 'foreground', 9) - renderer_title.set_property('ellipsize', Pango.EllipsizeMode.END) - - renderer_progress = Gtk.CellRendererText() - self.cols['Progress'].pack_start(renderer_progress, False) - self.cols['Progress'].add_attribute(renderer_progress, 'text', 4) - self.cols['Progress'].set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) - self.cols['Progress'].set_expand(False) - - if self.progress_style == 0: - renderer_percent = Gtk.CellRendererProgress() - self.cols['Percent'].pack_start(renderer_percent, False) - self.cols['Percent'].add_attribute(renderer_percent, 'value', 10) - else: - renderer_percent = ProgressCellRenderer(self.colors) - self.cols['Percent'].pack_start(renderer_percent, False) - self.cols['Percent'].add_attribute(renderer_percent, 'value', 2) - self.cols['Percent'].add_attribute(renderer_percent, 'total', 6) - self.cols['Percent'].add_attribute(renderer_percent, 'subvalue', 7) - self.cols['Percent'].add_attribute(renderer_percent, 'eps', 8) - renderer_percent.set_fixed_size(100, -1) - - renderer = Gtk.CellRendererText() - self.cols['Score'].pack_start(renderer, False) - self.cols['Score'].add_attribute(renderer, 'text', 5) - renderer = Gtk.CellRendererText() - self.cols['Start'].pack_start(renderer, False) - self.cols['Start'].add_attribute(renderer, 'text', 11) - renderer = Gtk.CellRendererText() - self.cols['End'].pack_start(renderer, False) - self.cols['End'].add_attribute(renderer, 'text', 12) - renderer = Gtk.CellRendererText() - self.cols['My start'].pack_start(renderer, False) - self.cols['My start'].add_attribute(renderer, 'text', 13) - renderer = Gtk.CellRendererText() - self.cols['My end'].pack_start(renderer, False) - self.cols['My end'].add_attribute(renderer, 'text', 14) - - def _header_button_press(self, button, event): + def _header_right_click(self, button, event): if event.button == 3: menu = Gtk.Menu() for name, sort in self.available_columns: @@ -320,6 +384,112 @@ class ShowTreeView(Gtk.TreeView): return False + @staticmethod + def _next_episode_sort_func(model, iter1, iter2, user_data) -> int: + """Time based sort function for the "Next episode" column. Always sorts "-" and "?" below everything.""" + + # Get the values from the "Next episode" column for the two rows + value1 = model.get_value(iter1, TreeConstants.NEXT_EPISODE_AIR_TIME_RELATIVE) + value2 = model.get_value(iter2, TreeConstants.NEXT_EPISODE_AIR_TIME_RELATIVE) + + sort_id, sort_order = user_data.get_model().get_sort_column_id() # Get the current sort order of the model + + special_cases = ('-', '?') + if value1 in special_cases: + return 1 if sort_order == Gtk.SortType.ASCENDING else -1 + elif value2 in special_cases: + return -1 if sort_order == Gtk.SortType.ASCENDING else 1 + + # Parse the time intervals, convert everything to minutes and sort accordingly + days1, hours1, minutes1 = utils.parse_time_interval(value1) + days2, hours2, minutes2 = utils.parse_time_interval(value2) + + total_minutes1 = days1 * 24 * 60 + hours1 * 60 + minutes1 + total_minutes2 = days2 * 24 * 60 + hours2 * 60 + minutes2 + + if total_minutes1 < total_minutes2: + return -1 + elif total_minutes1 == total_minutes2: + return 0 + else: + return 1 + + def _on_column_clicked(self, key, column): + # Todo: "Title sort" should sort shows with available episodes first -> custom sort function needed + """Sets up sorting for the clicked column, based on what the previous sorting was. + We can't simply use self.cols[name].set_sort_order_column_id(key) because that inevitably allocates screen + space for the sort indicator arrow. This way, the sort indicator is only visible (and takes up space) for the + currently sorted column. + + In order to make the list look uniform while keeping the default sort direction logical, + e.g: highest score first, we need to dynamically change the sort indicator directions. + + We're saving the sorted column because it's not possible to get the column purely from the ID without + iterating through the columns, so this is more efficient""" + + sort_column_id_model, sort_order_model = self.get_model().get_sort_column_id() + sort_column_id_model = sort_column_id_model if sort_column_id_model is not None else TreeConstants.TITLE + sort_order_model = sort_order_model if sort_order_model is not None else Gtk.SortType.ASCENDING + # Sort order and ID passed by the model. Order and ID default to none and correspond to the previously sorted + # column ID, NOT the clicked column. Set sort_column_id_model to 'TreeConstants.TITLE', + # as that's the default sort column. + + column_title = column.get_title() # Gets the clicked column title + sort_order_column = column.get_sort_order() # Sort order passed by the column (indicator direction) + + if sort_column_id_model == key: # Check if we're trying to sort the same column that's already being sorted + self._reverse_sort_order(column, sort_order_model, sort_order_column, key) # Reverse sorting + + else: # We're trying to sort a new column + match column_title: # Check which column and set the sort indicator accordingly + case 'Score': # Default should be large -> small, e.g: 10, 8, 7, 3 + column.set_sort_order(Gtk.SortType.ASCENDING) + self.get_model().set_sort_column_id(key, Gtk.SortType.DESCENDING) + + case 'Watched': # Default should be large -> small, e.g: 18 / 23, 12 / 13, 7 / 13 + column.set_sort_order(Gtk.SortType.ASCENDING) + self.get_model().set_sort_column_id(key, Gtk.SortType.DESCENDING) + + case 'Progress': # Default should be large -> small + column.set_sort_order(Gtk.SortType.ASCENDING) + self.get_model().set_sort_column_id(key, Gtk.SortType.DESCENDING) + + case _: # Everything else should be normal + column.set_sort_order(Gtk.SortType.ASCENDING) + self.get_model().set_sort_column_id(key, Gtk.SortType.ASCENDING) + + self.cols[self.previous_sort_column].set_sort_indicator(False) # Disable the previous sort indicator + column.set_sort_indicator(True) # Enable the new sort indicator + self.previous_sort_column = column_title # Save the new sorting column + + def _reverse_sort_order(self, column, sort_order_model, sort_order_column, key): + """Reverses both the actual sort order and the visual sorting indicator arrow direction""" + + if sort_order_model == Gtk.SortType.ASCENDING: + self._reverse_sort_indicator(column, sort_order_column) + self.get_model().set_sort_column_id(key, Gtk.SortType.DESCENDING) + + elif sort_order_model == Gtk.SortType.DESCENDING: + self._reverse_sort_indicator(column, sort_order_column) + self.get_model().set_sort_column_id(key, Gtk.SortType.ASCENDING) + + else: + raise ValueError("Invalid sort order. Must be Gtk.SortType.ASCENDING or Gtk.SortType.DESCENDING") + + @staticmethod + def _reverse_sort_indicator(column, sort_order_column): + """Reverses the visual sorting indicator direction""" + if sort_order_column == Gtk.SortType.ASCENDING: + column.set_sort_order(Gtk.SortType.DESCENDING) + + elif sort_order_column == Gtk.SortType.DESCENDING: + column.set_sort_order(Gtk.SortType.ASCENDING) + + else: + raise ValueError("Invalid sort order. Must be Gtk.SortType.ASCENDING or Gtk.SortType.DESCENDING") + + + @property def filter(self): return self.props.model.props.model @@ -331,26 +501,25 @@ class ShowTreeView(Gtk.TreeView): return False _, col, _, _ = view.get_path_at_pos(tx, ty) - if col != self.cols['Percent']: + if col != self.cols['Progress']: return False def gv(key): return model.get_value(tree_iter, ShowListStore.column(key)) - lines = [] - lines.append("Watched: %d" % gv('stat')) + lines = ["Watched: %d" % gv(TreeConstants.MY_PROGRESS)] - aired = gv('subvalue') - status = gv('status') + aired = gv(TreeConstants.AIRED_EPS) + status = gv(TreeConstants.SHOW_STATUS) if aired and not status == utils.Status.NOTYET: lines.append("Aired%s: %d" % ( ' (estimated)' if status == utils.Status.AIRING else '', aired)) - avail_eps = gv('avail-eps') + avail_eps = gv(TreeConstants.AVAILABLE_EPS) if len(avail_eps) > 0: lines.append("Available: %d" % max(avail_eps)) - lines.append("Total: %s" % (gv('total-eps') or '?')) + lines.append("Total: %s" % (gv(TreeConstants.TOTAL_EPS) or '?')) tip.set_markup('\n'.join(lines)) renderer = next(iter(col.get_cells())) @@ -363,7 +532,7 @@ class ShowTreeView(Gtk.TreeView): def select(self, show): """Select specified row or first if not found""" for row in self.get_model(): - if int(row[0]) == show['id']: + if int(row[TreeConstants.SHOW_ID]) == show['id']: selection = self.get_selection() selection.select_iter(row.iter) return @@ -419,7 +588,7 @@ class ProgressCellRenderer(Gtk.CellRenderer): return getattr(self, pspec.name) def do_render(self, cr, widget, background_area, cell_area, flags): - (x, y, w, h) = self.do_get_size(widget, cell_area) + (x, y, w, h) = self._do_get_size(widget, cell_area) # set_source_rgb(0.9, 0.9, 0.9) cr.set_source_rgb(*self.__get_color(self.colors['progress_bg'])) @@ -438,7 +607,7 @@ class ProgressCellRenderer(Gtk.CellRenderer): # set_source_rgb(0.7, 0.7, 0.7) cr.set_source_rgb( *self.__get_color(self.colors['progress_sub_bg'])) - cr.rectangle(x, y+h-self._subheight, mid, h-(h-self._subheight)) + cr.rectangle(x, y + h - self._subheight, mid, h - (h - self._subheight)) cr.fill() if self.value: @@ -463,11 +632,12 @@ class ProgressCellRenderer(Gtk.CellRenderer): if 0 < episode <= self.total: start = int(w / float(self.total) * (episode - 1)) finish = int(w / float(self.total) * episode) - cr.rectangle(x+start, y+h-self._subheight, - finish-start, h-(h-self._subheight)) + cr.rectangle(x + start, y + h - self._subheight, + finish - start, h - (h - self._subheight)) cr.fill() - def do_get_size(self, widget, cell_area): + @classmethod + def _do_get_size(cls, widget, cell_area): if cell_area is None: return 0, 0, 0, 0 x = cell_area.x diff --git a/trackma/ui/qt/models.py b/trackma/ui/qt/models.py index f561045..16bdc71 100644 --- a/trackma/ui/qt/models.py +++ b/trackma/ui/qt/models.py @@ -83,13 +83,13 @@ class ShowListModel(QtCore.QAbstractTableModel): del self.colors[row] def _calculate_next_ep(self, row, show): - if self.mediainfo.get('date_next_ep'): - if 'next_ep_time' in show: - delta = show['next_ep_time'] - datetime.datetime.utcnow() - self.next_ep[row] = "%i days, %02d hrs." % ( - delta.days, delta.seconds/3600) - elif row in self.next_ep: - del self.next_ep[row] + if self.mediainfo.get('date_next_ep') and show['next_ep_time'] is not None: + delta = show['next_ep_time'].replace(tzinfo=datetime.timezone.utc) - \ + datetime.datetime.now(tz=datetime.timezone.utc) # Make sure it's UTC + self.next_ep[row] = "%i days, %02d hrs." % ( + delta.days, delta.seconds / 3600) + elif row in self.next_ep: + del self.next_ep[row] def _calculate_eps(self, row, show): aired_eps = utils.estimate_aired_episodes(show) @@ -138,7 +138,7 @@ class ShowListModel(QtCore.QAbstractTableModel): self._calculate_color(row, show) self.dataChanged.emit(self.index( - row, 0), self.index(row, len(self.columns)-1)) + row, 0), self.index(row, len(self.columns) - 1)) def rowCount(self, parent): if self.showlist: @@ -185,8 +185,8 @@ class ShowListModel(QtCore.QAbstractTableModel): if show['total']: total = show['total'] else: - total = (int(show['my_progress']/12)+1) * \ - 12 # Round up to the next cour + total = (int(show['my_progress'] / 12) + 1) * \ + 12 # Round up to the next cour if row in self.eps: return (show['my_progress'], total, self.eps[row][0], self.eps[row][1]) @@ -395,7 +395,8 @@ class ShowListProxy(QtCore.QSortFilterProxyModel): self.invalidateFilter() def filterAcceptsRow(self, source_row, source_parent): - if self.filter_status is not None and self.sourceModel().showlist[source_row]['my_status'] != self.filter_status: + if self.filter_status is not None and self.sourceModel().showlist[source_row]['my_status'] \ + != self.filter_status: return False if self.filter_columns: diff --git a/trackma/utils.py b/trackma/utils.py index c4ddd30..9400be6 100644 --- a/trackma/utils.py +++ b/trackma/utils.py @@ -510,9 +510,77 @@ def show(): 'image': '', 'image_thumb': '', 'queued': False, + 'next_ep_time': None } +def calculate_relative_time(time_end: datetime, utc: bool, fulltime: bool = True) -> str: + """Function that calculates the relative time between 2 datetime objects. + If full=False, it returns only the greatest nonzero time unit""" + + if time_end: + try: + if utc: + time_end = time_end.replace(tzinfo=datetime.timezone.utc) + # Make sure that time_end is timezone aware in UTC + current_time = datetime.datetime.now(tz=datetime.timezone.utc) + else: + current_time = datetime.datetime.now() + time_difference = time_end - current_time + days = time_difference.days + hours, remainder = divmod(time_difference.seconds, 3600) + minutes, seconds = divmod(remainder, 60) + + time_units = [("days", days), ("hours", hours), ("minutes", minutes)] + + # Filter out units with a value of 0 + non_zero_units = [(unit, value) for unit, value in time_units if value != 0] + + if fulltime: + result = ", ".join([ + f"in {value + (1 if remainder > 0 else 0)} " + f"{unit if value + (1 if remainder > 0 else 0) != 1 else unit[:-1]}" + for unit, value in non_zero_units + ]) + else: + # Display only the greatest non-zero unit + result = next( + ( + f"in {value + (1 if remainder > 0 else 0)} " + f"{unit if value + (1 if remainder > 0 else 0) != 1 else unit[:-1]}" + for unit, value in non_zero_units + ), + "error" + ) + return result + except ValueError: + return '?' + else: + return '-' + + +def parse_time_interval(value): + """Parse the time interval string and return it as a tuple (days, hours, minutes)""" + + pattern = ( + r'in\s*' + r'(?:(\d+) day(?:s)?)?' + r'(?:,\s*)?' + r'(?:(\d+) hour(?:s)?)?' + r'(?:,\s*)?' + r'(?:(\d+) minute(?:s)?)?' + r'|[-?]' + ) + + match = re.match(pattern, value) + if match: + days = int(match.group(1) or 0) + hours = int(match.group(2) or 0) + minutes = int(match.group(3) or 0) + return days, hours, minutes + return 0, 0, 0 # Return a default value if the format is not matched + + class TrackmaError(Exception): pass @@ -663,9 +731,9 @@ gtk_defaults = { 'start_in_tray': False, 'tray_api_icon': False, 'remember_geometry': False, - 'last_width': 740, - 'last_height': 480, - 'visible_columns': ['Title', 'Progress', 'Score', 'Percent'], + 'last_width': 1080, + 'last_height': 720, + 'visible_columns': ['Title', 'Watched', 'Score', 'Progress', 'Next episode'], 'episodebar_style': 1, 'colors': { 'is_airing': '#0099CC', @@ -679,6 +747,7 @@ gtk_defaults = { 'progress_sub_fg': '#668099', 'progress_complete': '#99CCB3', }, + 'column_version': ['1.1'] } qt_defaults = { @@ -691,8 +760,8 @@ qt_defaults = { 'remember_columns': False, 'last_x': 0, 'last_y': 0, - 'last_width': 740, - 'last_height': 480, + 'last_width': 1080, + 'last_height': 720, 'visible_columns': ['Title', 'Progress', 'Score', 'Percent'], 'inline_edit': True, 'columns_state': None,
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor