diff --git a/README.md b/README.md index e16ee04..a58cb29 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,6 @@ Linux (pyqt via pip): ## ToDo -* Use per-webview search bar instead of a global one * Create a new icon for the toot button * Create download manager * Handle cookies @@ -56,6 +55,7 @@ Linux (pyqt via pip): * Ability to add new custom searches via local or remote json * Add optional auto-updater with stable/testing channels * Add ability to 'freeze' tabs +* Add closed tab history * Rename the damn project ## Bugs diff --git a/qtweb/database/connection.py b/qtweb/database/connection.py index 0dd74ff..5f86b98 100644 --- a/qtweb/database/connection.py +++ b/qtweb/database/connection.py @@ -18,75 +18,65 @@ from ..config import var from ..functions import DotDict -tables = { - 'config': OrderedDict([ - ('id', 'INTEGER PRIMARY KEY'), - ('key', 'TEXT UNIQUE'), - ('value', 'TEXT') - ]), - 'bookmarks': OrderedDict([ - ('id', 'INTEGER PRIMARY KEY'), - ('name', 'TEXT'), - ('url', 'TEXT UNIQUE'), - ('description', 'TEXT'), - ('category', 'TEXT'), - ('lastupdate', 'DATETIME') - ]), - 'links': OrderedDict([ - ('id', 'INTEGER PRIMARY KEY'), - ('linkid', 'INTEGER') - ]), - 'mastodon': OrderedDict([ - ('id', 'INTEGER PRIMARY KEY'), - ('username', 'TEXT'), - ('displayname', 'TEXT'), - ('domain', 'TEXT'), - ('fullname', 'TEXT UNIQUE'), - ('apikey', 'TEXT'), - ('tootlimit', 'INTEGER'), - ('avatar', 'TEXT'), - ('lastupdate', 'DATETIME') - ]), - 'siteoptions': OrderedDict([ - ('id', 'INTEGER PRIMARY KEY'), - ('domain', 'TEXT UNIQUE'), - ('microphone', 'BOOLEAN DEFAULT 0'), - ('notification', 'BOOLEAN DEFAULT 0'), - ('camera', 'BOOLEAN DEFAULT 0'), - ('location', 'BOOLEAN DEFAULT 0'), - ('fullscreen', 'BOOLEAN DEFAULT 1'), - ('javascript', 'BOOLEAN DEFAULT 1'), - ('images', 'BOOLEAN DEFAULT 1'), - ('adblock', 'BOOLEAN DEFAULT 1'), - ('mastodon', 'BOOLEAN DEFAULT 0'), - ('allowhttp', 'BOOLEAN DEFAULT 1'), - ('capture', 'BOOLEAN DEFAULT 0'), - ('cookies', 'BOOLEAN DEFAULT 0'), - ('lastupdate', 'DATETIME') - ]), - 'tabs': OrderedDict([ - ('id', 'INTEGER PRIMARY KEY'), - ('title', 'TEXT'), - ('url', 'TEXT'), - ('tabid', 'INTEGER') - ]), - 'search': OrderedDict([ - ('id', 'INTEGER PRIMARY KEY'), - ('name', 'TEXT'), - ('keyword', 'TEXT UNIQUE'), - ('url', 'TEXT') - ]), - #('cookies', { - #'id': 'INTEGER PRIMARY KEY', - #'name': 'TEXT NOT NULL', - #'value': 'TEXT NOT NULL', - #'domain': 'TEXT NOT NULL', - #'path': 'TEXT NOT NULL', - #'expires': 'INTEGER NOT NULL', - #'http_only': 'BOOLEAN NOT NULL', - #'secure': 'BOOLEAN NOT NULL' - #}) -} +tables = DotDict({ + 'config': { + 'id': {'type': 'integer', 'options': ['primary key']}, + 'key': {'type': 'text', 'options': ['unique']}, + 'value': {'type': 'text'}, + }, + 'bookmarks': { + 'id': {'type': 'integer', 'options': ['primary key']}, + 'name': {'type': 'text'}, + 'url': {'type': 'text', 'options': ['unique']}, + 'description': {'type': 'text'}, + 'category': {'type': 'text'}, + 'lastupdate': {'type': 'datetime'}, + }, + 'links': { + 'id': {'type': 'integer', 'options': ['primary key']}, + 'linkid': {'type': 'integer'} + }, + 'mastodon': { + 'id': {'type': 'integer', 'options': ['primary key']}, + 'username': {'type': 'text'}, + 'displayname': {'type': 'text'}, + 'domain': {'type': 'text'}, + 'fullname': {'type': 'text', 'options': 'unique'}, + 'apikey': {'type': 'text'}, + 'tootlimit': {'type': 'integer'}, + 'avatar': {'type': 'text'}, + 'lastupdate': {'type': 'datetime'}, + }, + 'siteoptions': { + 'id': {'type': 'integer', 'options': ['primary key']}, + 'domain': {'type': 'text', 'options': 'unique'}, + 'microphone': {'type': 'BOOLEAN', 'default': False}, + 'notification': {'type': 'BOOLEAN', 'default': False}, + 'camera': {'type': 'BOOLEAN', 'default': False}, + 'location': {'type': 'BOOLEAN', 'default': False}, + 'fullscreen': {'type': 'BOOLEAN', 'default': True}, + 'javascript': {'type': 'BOOLEAN', 'default': True}, + 'images': {'type': 'BOOLEAN', 'default': True}, + 'adblock': {'type': 'BOOLEAN', 'default': True}, + 'mastodon': {'type': 'BOOLEAN', 'default': False}, + 'allowhttp': {'type': 'BOOLEAN', 'default': True}, + 'capture': {'type': 'BOOLEAN', 'default': False}, + 'cookies': {'type': 'BOOLEAN', 'default': False}, + 'lastupdate': {'type': 'datetime'} + }, + 'tabs': { + 'id': {'type': 'integer', 'options': ['primary key']}, + 'title': {'type': 'text'}, + 'url': {'type': 'text'}, + 'tabid': {'type': 'integer'} + }, + 'search': { + 'id': {'type': 'integer', 'options': ['primary key']}, + 'name': {'type': 'text'}, + 'keyword': {'type': 'text', 'options': ['unique']}, + 'url': {'type': 'text'} + } +}) SearchEngines = [ @@ -281,17 +271,24 @@ class DB(): def CreateTable(self, table): - layout = tables[table] + layout = DotDict(tables.get(table)) + + if not layout: + logging.error('Table config doesn\'t exist:', table) + return + cmd = f'CREATE TABLE IF NOT EXISTS {table}(' items = [] for k, v in layout.items(): - if k != 'foreignkey': - items.append(f'{k} {v}') + options = ' '.join(v.get('options', [])) + default = v.get('default') + item = f'{k} {v.type.upper()} {options}' - else: - field, ffield, fcol = layout[k] - items.append(f'FOREIGN KEY({field}) REFERENCES {ffield}({fcol})') + if default: + item += f'DEFAULT {default}' + + items.append(item) cmd += ', '.join(items) + ')' @@ -356,7 +353,7 @@ class DB(): return cookie -class DBResult(dict): +class DBResult(DotDict): def __init__(self, row, db, table, cursor): super().__init__() @@ -377,16 +374,24 @@ class DBResult(dict): def __delattr__(self, name): if name not in ['db', 'table']: - return self.__getitem__(name) + return self.__delitem__(name) else: return super().__delattr__(name) def __getattr__(self, value, default=None): - val = super().__getattr__(value, default) if default else self[value] + options = [value] - return DotDict(val) if type(val) == dict else val + if default: + options.append(default) + + if value in self.keys(): + val = super().__getitem__(*options) + return DotDict(val) if isinstance(val, dict) else val + + else: + return dict.__getattr__(*options) # Kept for backwards compatibility. Delete later. @@ -413,25 +418,6 @@ def CreateDatabase(): for engine in SearchEngines: db.insert('search', engine) - darktheme = { - 'name': 'Dark', - 'AlternateBase': '#353535', - 'Base': '#191919', - 'Button': '#353535', - 'ButtonText': '#ffffff', - 'BrightText': '#ff0000', - 'Highlight': '#2a82da', - 'HighlightedText': '#000000', - 'Link': '#2a82da', - 'Text': '#ffffff', - 'ToolTipBase': '#000000', - 'ToolTipText': '#ffffff', - 'Window': '#353535', - 'WindowText': '#ffffff' - } - - db.insert('themes', darktheme) - perm = True backupdb = f'{var.database}.backup-{prev_dbversion}' diff --git a/qtweb/database/get.py b/qtweb/database/get.py index e3b0b02..f6286e6 100644 --- a/qtweb/database/get.py +++ b/qtweb/database/get.py @@ -6,7 +6,7 @@ from ..Lib.IzzyLib.misc import boolean from ..Lib.IzzyLib import logging from .connection import db, tables, ParseData -from ..functions import psl +from ..functions import psl, DotDict from PyQt5.QtGui import QPalette, QColor @@ -50,18 +50,27 @@ def permissions(domain=None, defaults=False): else: rows = [db.cache['siteoptions'].fetch(domain)] if domain else cache.values() - for row in rows: - if not row and defaults: - values = [None, None, False, False, False, False, True, True, True, True, False, True, False, False, None] - row = db.resclass['siteoptions'](*values) + if rows == [None]: + rows = None - for k, v in row.asdict().items(): - setattr(row, k, boolean(v) if k not in ['id', 'domain', 'lastupdate'] else v) + if not rows and defaults: + row = DotDict() - if domain: - return row + for k, v in tables.siteoptions.items(): + default = v.get('default') + row[k] = default if default else None - data.append(row) + return row + + else: + for row in rows: + for k, v in row.items(): + row[k] = boolean(v) if k not in ['id', 'domain', 'lastupdate'] else v + + if domain: + return row + + data.append(row) return data diff --git a/qtweb/dialogs.py b/qtweb/dialogs.py index e9010a1..dd816af 100644 --- a/qtweb/dialogs.py +++ b/qtweb/dialogs.py @@ -349,7 +349,7 @@ class NewDownload(QDialog): logging.verbose('Starting download of', url) self.SetFilename() self.download.item.accept() - self.download.item.finished.connect(lambda: profile._download_finished(self.download)) + self.download.item.finished.connect(lambda: self.profile._download_finished(self.download)) self.finished = True self.close() diff --git a/qtweb/functions.py b/qtweb/functions.py index 1b26c31..eef60df 100644 --- a/qtweb/functions.py +++ b/qtweb/functions.py @@ -455,22 +455,19 @@ def ParseSoundcloudInfo(data): class DotDict(dict): - __setattr__ = dict.__setitem__ - __delattr__ = dict.__delitem__ - def __init__(self, value=None, **kwargs): super().__init__() + self.__setattr__ = dict.__setitem__ + self.__delattr__ = dict.__delitem__ + self.__getitem__ = self.__getattr__ if value.__class__ == str: self.FromJson(value) elif value.__class__ in [dict, DotDict]: - self.update(dict(value)) + self.update(value) - elif not value: - pass - - else: + elif value: raise TypeError('The value must be a JSON string, dict, or another DotDict object, not', value.__class__) if kwargs: @@ -478,17 +475,8 @@ class DotDict(dict): self.update(kwargs) - def __getattr__(self, value, default=None): - val = self.get(value, default) if default else self[value] - - return DotDict(val) if type(val) == dict else val - - def ToJson(self, **kwargs): - if not kwargs.get('indent'): - kwargs['indent'] = 4 - - return json.dumps(self, **kwargs) + return self.__str__(**kwargs) def FromJson(self, string): @@ -496,6 +484,29 @@ class DotDict(dict): self.update(data) + def __parse_item(self, data): + return DotDict(data) if type(data) == dict else data + + + def items(self): + data = [] + + for k, v in super(DotDict, self).items(): + value = self.__parse_item(v) + data.append((k, value)) + + return data + + + def __str__(self, **kwargs): + return json.dumps(self, **kwargs) + + + def __getattr__(self, value, default=None): + val = self.get(value, default) if default else self[value] + return self.__parse_item(val) + + def ExceptionHandler(exctype, value, tb): sys._excepthook(exctype, value, tb) diff --git a/qtweb/webprofile.py b/qtweb/webprofile.py index 2d1f0ac..47b6fcd 100644 --- a/qtweb/webprofile.py +++ b/qtweb/webprofile.py @@ -130,7 +130,7 @@ class WebEngineProfile(QWebEngineProfile): filename = Path(download.path()) else: - filename = Path(download.downloadDirectory(), download.downloadFileName()) + filename = Path(download.item.downloadDirectory(), download.item.downloadFileName()) url = download.item.url().toString() diff --git a/qtweb/webview.py b/qtweb/webview.py index 1c3023a..1a274eb 100644 --- a/qtweb/webview.py +++ b/qtweb/webview.py @@ -187,15 +187,18 @@ class WebEngineView(QWebEngineView): logging.verbose(f'Rejected permission for {host}:', name) - def _fullscreen_request(self, action, request): - host = origin().host() + def _fullscreen_request(self, request): + host = request.origin().host() site = get.permissions(host, defaults=True) if site.fullscreen: - logging.verbose('Allowing fullscreen for:', host) + logging.verbose('Allowing fullscreen for', host) request.accept() self.Fullscreen(request.toggleOn()) + else: + logging.verbose('Disallowing fullscreen for ', host) + def _handle_auth(self, location, authenticator): url = location.toString()