summaryrefslogtreecommitdiff
path: root/tidal_dl_ng/helper/gui.py
blob: 35a517fabec186adedfc8b20a4076ceee89e8704 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
import re
from typing import cast

from PySide6 import QtCore, QtGui, QtWidgets
from tidalapi import Album, Mix, Playlist, Track, UserPlaylist, Video
from tidalapi.artist import Artist
from tidalapi.media import Quality
from tidalapi.playlist import Folder

from tidal_dl_ng.constants import QualityVideo


def get_table_data(
    item: QtWidgets.QTreeWidgetItem, column: int
) -> Track | Video | Album | Artist | Mix | Playlist | UserPlaylist:
    result: Track | Video | Album | Artist = item.data(column, QtCore.Qt.ItemDataRole.UserRole)

    return result


def get_table_text(item: QtWidgets.QTreeWidgetItem, column: int) -> str:
    result: str = item.text(column)

    return result


def get_results_media_item(
    index: QtCore.QModelIndex, proxy: QtCore.QSortFilterProxyModel, model: QtGui.QStandardItemModel
) -> Track | Video | Album | Artist | Playlist | Mix:
    # Switch column to "obj" column and map proxy data to our model.
    item: QtGui.QStandardItem = model.itemFromIndex(proxy.mapToSource(index.siblingAtColumn(1)))
    result: Track | Video | Album | Artist = item.data(QtCore.Qt.ItemDataRole.UserRole)

    return result


def get_user_list_media_item(item: QtWidgets.QTreeWidgetItem) -> Mix | Playlist | UserPlaylist | Folder | str:
    result: Mix | Playlist | UserPlaylist | Folder | str = get_table_data(item, 1)

    return result


def get_queue_download_media(
    item: QtWidgets.QTreeWidgetItem,
) -> Mix | Playlist | UserPlaylist | Track | Video | Album | Artist:
    result: Mix | Playlist | UserPlaylist | Track | Video | Album | Artist = get_table_data(item, 1)

    return result


def get_queue_download_quality(
    item: QtWidgets.QTreeWidgetItem,
    column: int,
) -> str:
    result: str = get_table_text(item, column)

    return result


def get_queue_download_quality_audio(
    item: QtWidgets.QTreeWidgetItem,
) -> Quality:
    result: Quality = cast(Quality, get_queue_download_quality(item, 4))

    return result


def get_queue_download_quality_video(
    item: QtWidgets.QTreeWidgetItem,
) -> QualityVideo:
    result: QualityVideo = cast(QualityVideo, get_queue_download_quality(item, 5))

    return result


def set_table_data(
    item: QtWidgets.QTreeWidgetItem,
    data: Track | Video | Album | Artist | Mix | Playlist | UserPlaylist | Folder | str,
    column: int,
):
    item.setData(column, QtCore.Qt.ItemDataRole.UserRole, data)


def set_results_media(item: QtWidgets.QTreeWidgetItem, media: Track | Video | Album | Artist):
    set_table_data(item, media, 1)


def set_user_list_media(
    item: QtWidgets.QTreeWidgetItem,
    media: Track | Video | Album | Artist | Mix | Playlist | UserPlaylist | Folder | str,
):
    set_table_data(item, media, 1)


def set_queue_download_media(
    item: QtWidgets.QTreeWidgetItem, media: Mix | Playlist | UserPlaylist | Track | Video | Album | Artist
):
    set_table_data(item, media, 1)


class FilterHeader(QtWidgets.QHeaderView):
    filter_activated = QtCore.Signal()

    def __init__(self, parent):
        super().__init__(QtCore.Qt.Horizontal, parent)
        self._editors = []
        self._padding = 4
        self.setCascadingSectionResizes(True)
        self.setSectionResizeMode(QtWidgets.QHeaderView.Interactive)
        self.setStretchLastSection(True)
        self.setDefaultAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
        self.setSortIndicatorShown(False)
        self.setSectionsMovable(True)
        self.sectionResized.connect(self.adjust_positions)
        parent.horizontalScrollBar().valueChanged.connect(self.adjust_positions)

    def set_filter_boxes(self, count):
        while self._editors:
            editor = self._editors.pop()
            editor.deleteLater()

        for _ in range(count):
            editor = QtWidgets.QLineEdit(self.parent())
            editor.setPlaceholderText("Filter")
            editor.setClearButtonEnabled(True)
            editor.returnPressed.connect(self.filter_activated.emit)
            self._editors.append(editor)

        self.adjust_positions()

    def sizeHint(self):
        size = super().sizeHint()

        if self._editors:
            height = self._editors[0].sizeHint().height()

            size.setHeight(size.height() + height + self._padding)

        return size

    def updateGeometries(self):
        if self._editors:
            height = self._editors[0].sizeHint().height()

            self.setViewportMargins(0, 0, 0, height + self._padding)
        else:
            self.setViewportMargins(0, 0, 0, 0)

        super().updateGeometries()
        self.adjust_positions()

    def adjust_positions(self):
        for index, editor in enumerate(self._editors):
            height = editor.sizeHint().height()

            editor.move(self.sectionPosition(index) - self.offset() + 2, height + (self._padding // 2))
            editor.resize(self.sectionSize(index), height)

    def filter_text(self, index) -> str:
        if 0 <= index < len(self._editors):
            return self._editors[index].text()

        return ""

    def set_filter_text(self, index, text):
        if 0 <= index < len(self._editors):
            self._editors[index].setText(text)

    def clear_filters(self):
        for editor in self._editors:
            editor.clear()


class HumanProxyModel(QtCore.QSortFilterProxyModel):
    def _human_key(self, key):
        parts = re.split(r"(\d*\.\d+|\d+)", key)

        return tuple((e.swapcase() if i % 2 == 0 else float(e)) for i, e in enumerate(parts))

    def lessThan(self, source_left, source_right):
        data_left = source_left.data()
        data_right = source_right.data()

        if isinstance(data_left, str) and isinstance(data_right, str):
            return self._human_key(data_left) < self._human_key(data_right)

        return super().lessThan(source_left, source_right)

    @property
    def filters(self):
        if not hasattr(self, "_filters"):
            self._filters = []

        return self._filters

    @filters.setter
    def filters(self, filters):
        self._filters = filters

        self.invalidateFilter()

    def filterAcceptsRow(self, source_row: int, source_parent: QtCore.QModelIndex) -> bool:
        model = self.sourceModel()
        source_index = model.index(source_row, 0, source_parent)
        result: [bool] = []

        # Show top level children
        for child_row in range(model.rowCount(source_index)):
            if self.filterAcceptsRow(child_row, source_index):
                return True

        # Filter for actual needle
        for i, text in self.filters:
            if 0 <= i < self.columnCount():
                ix = self.sourceModel().index(source_row, i, source_parent)
                data = ix.data()

                # Append results to list to enable an AND operator for filtering.
                result.append(bool(re.search(rf"{text}", data, re.MULTILINE | re.IGNORECASE)) if data else False)

        # If no filter set, just set the result to True.
        if not result:
            result.append(True)

        return all(result)