Merge pull request 'version 0.1.7' (#2) from dev into main

This commit is contained in:
Izalia Mae 2020-09-24 16:44:16 -04:00
commit 554da51046
37 changed files with 2255 additions and 1385 deletions

View file

@ -4,11 +4,13 @@ Web browser written in Python and Qt with PyQt5
## Features
*
* Quick access to various page functions in the status bar
* Login to a Mastodon account
* Quick access to domain permissions in the status bar
* Create new Mastodon posts without going to the webui
* Interact with Mastodon posts by right-clicking the post's url
* Control the browser via cli from a terminal
* Ability to add user scripts (basic support atm)
* Custom searches via keyword
* Remote web dev tools (localhost only atm)
note: I may add full pleroma support in the future
@ -31,7 +33,7 @@ Make sure you add Python to PATH
### Python dependencies
Windows:
pip3 install -r requirements.txt -r requirements-win.txt
Linux:
@ -44,20 +46,23 @@ Linux (pyqt via pip):
## ToDo
* Create a new icon for the toot button
* Create download manager
* Handle cookies
* Add folder structure for bookmarks
* Create history tab
* Add proxy support
* 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
* Resetting a page search sometimes scrolls the page to the top or an element on the page
* Web notifications use the wrong icon sometimes
* Switching between PyQt versions resets cookies
* Stop and reload buttons occasionally don't get set properly after finished loading
* The nitter redirect only works when loading a url from the navbar
* Web inspector is kinda broken
* The webview is really blurry and glitchy in virtualbox with the VboxSVGA controller with 3d accel enabled
@ -67,3 +72,6 @@ Linux (pyqt via pip):
* No web notification support in QWebEngine
* Download urls with query parameters will probably produce junk names atm because QWebEngineDownloadItem.suggestedFileName() doesn't exist
* Can't login to twitch.tv and channels sometimes don't load
* Can't browse github code trees. Use right click > Open Link in New Tab to work around the issue for now
* Gitlab servers respond with a 422 when trying to login

View file

@ -24,9 +24,10 @@ class httpClient:
self.headers['User-Agent'] = self.agent
def _fetch(self, url, headers={}, method='GET', data=None, cached=True):
def _fetch(self, url, new_headers={}, method='GET', data=None, cached=True):
cached_data = self.cache.fetch(url)
#url = url.split('#')[0]
headers = self.headers.copy()
headers.update(new_headers)
if cached and cached_data:
logging.debug(f'Returning cached data for {url}')

View file

@ -1,5 +1,5 @@
__application__ = 'QtWeb'
__version_info__ = (0,1,6,9)
__version_info__ = (0,1,7)
__version__ = '.'.join(str(v) for v in __version_info__)
__author__ = 'Zoey Mae'

View file

@ -1,17 +1,14 @@
import sys
from collections import namedtuple
from os.path import abspath, dirname, join, isdir, expanduser
from os.path import abspath, dirname, join, isdir
from os import environ, makedirs, walk
from random import randint
from . import __version__, __application__, isWindows
from .arguments import profile
from .Lib.IzzyLib import logging
from .Lib.IzzyLib.misc import randomgen
import PyQt5
from PyQt5.QtCore import QStandardPaths
from PyQt5.QtWebEngineWidgets import QWebEngineProfile
@ -41,12 +38,21 @@ variables = {
'lock': join(datapath, profile, 'lock.pid'),
'home': home,
'temp': join(temp, 'qtweb'),
'local': 'web:'
'local': 'qtweb'
}
default_vars = {
'homepage': 'https://ddg.gg',
'nitter': 'https://nitter.net',
'mhost': '127.0.0.1',
'mport': 8008
}
# Set various variables
var = namedtuple('Variables', variables.keys())(*variables.values())
default = namedtuple('Default', default_vars.keys())(*default_vars.values())
## create necessary data dirs
if not isdir(var.profilepath):
@ -55,4 +61,7 @@ if not isdir(var.profilepath):
if not isdir(var.temp):
makedirs(var.temp, exist_ok=True)
if not isdir(join(var.datapath, 'UserScripts')):
makedirs(join(var.datapath, 'UserScripts'), exist_ok=True)
environ['QTWEBENGINE_DICTIONARIES_PATH'] = join(var.resources, 'dictionaries')

View file

@ -9,7 +9,6 @@ from .connection import db, CreateDatabase
from .. import dbversion
dbverrow = get.config('version', 0) if db.TableCheck('config') else 0
current_dbversion = 202007110621 if int(dbverrow) == 2020071110621 else dbverrow

View file

@ -3,7 +3,6 @@ import shutil, sqlite3, traceback
from os.path import isfile
from collections import OrderedDict
from sqlite3 import OperationalError
from dataclasses import make_dataclass, field, asdict
from contextlib import contextmanager
from pathlib import Path
from datetime import datetime
@ -16,78 +15,68 @@ from DBUtils.PooledDB import PooledDB
from .. import dbversion, prev_dbversion
from ..config import var
from ..functions import DateTime
from ..functions import DotDict
tables = {
'config': OrderedDict([
('id', 'INTEGER PRIMARY KEY'),
('key', 'TEXT'),
('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'),
('apikey', 'TEXT'),
('tootlimit', 'INTEGER'),
('avatar', 'TEXT'),
('lastupdate', 'DATETIME')
]),
'siteoptions': OrderedDict([
('id', 'INTEGER PRIMARY KEY'),
('domain', 'TEXT'),
('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 = [
@ -135,12 +124,12 @@ def ParseData(table, row):
class DB():
def __init__(self, dbfile=var.database):
self.db = PooledDB(sqlite3, database=dbfile, maxconnections=50, mincached=5, maxusage=1)
self.window = None
self.cache = {}
self.resclass = {}
for table in tables.keys():
self._setup_cache(table)
self._setup_result_class(table)
# function aliases
self.get = self.fetch
@ -154,23 +143,6 @@ class DB():
self.cache[table] = LRUCache(128)
def _setup_result_class(self, table):
def UpdateDict(dict1, dict2):
dict1.update(dict2)
return dict1
self.resclass[table] = make_dataclass(
table.title(),
[(key, 'typing.Any', field(default=None)) for key in self._table_keys(table)],
namespace={
'_asdict': lambda self: asdict(self), #remove later
'asdict': lambda self: asdict(self),
'Update': lambda self, data=None: db.update(table, self.id, UpdateDict(self.asdict(), data if data else {})),
'Remove': lambda self: db.remove(table, self.id)
}
)
def close(self):
self.db.close()
@ -221,39 +193,33 @@ class DB():
def fetch(self, table, rowid=None, single=True, sort=None, **kwargs):
db = self
table = table if tables.get(table) else table.replace('_migrate', '')
tablekeys = self._table_keys(table)
querysort = f'ORDER BY {sort}'
Result = self.resclass.get(table, None)
if not Result:
logging.error('Invalid Result object for table:', table)
return
with self.GetCursor() as cursor:
resultOpts = [self, table, cursor]
if rowid:
result = self.query(f"SELECT * FROM {table} WHERE id = ?", [rowid])
if rowid:
cursor.execute(f"SELECT * FROM {table} WHERE id = ?", [rowid])
elif kwargs:
placeholders = [f'{k} = ?' for k in kwargs.keys()]
values = kwargs.values()
elif kwargs:
placeholders = [f'{k} = ?' for k in kwargs.keys()]
values = kwargs.values()
where = ' and '.join(placeholders)
query = f"SELECT * FROM {table} WHERE {where} {querysort if sort else ''}"
result = self.query(query, list(values))
else:
result = self.query(f'SELECT * FROM {table} {querysort if sort else ""}')
if result:
if single:
res = Result(*ParseData(table, result[0]))
where = ' and '.join(placeholders)
query = f"SELECT * FROM {table} WHERE {where} {querysort if sort else ''}"
cursor.execute(query, list(values))
else:
res = [Result(*ParseData(table, row)) for row in result]
cursor.execute(f'SELECT * FROM {table} {querysort if sort else ""}')
return res
rows = cursor.fetchall()
if rows:
if single:
return DBResult(rows[0] if single else rows, *resultOpts)
return [DBResult(row, *resultOpts) for row in rows]
else:
return None if single else []
@ -286,8 +252,7 @@ class DB():
def query(self, string, values=[]):
with self.GetCursor() as cursor:
cursor.execute(string, values)
rows = cursor.fetchall()
return rows
return cursor.fetchall()
def GetFields(self, table, ignore_fields=[]):
@ -306,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) + ')'
@ -333,7 +305,8 @@ class DB():
def UpVersion(self, v):
row = self.fetch('config', key='version')
self.update('config', row.id, {'value': str(v)})
row.value = str(v)
row.Update()
def AddColumn(self, table, name, datatype, default=None, options=None):
@ -380,11 +353,67 @@ class DB():
return cookie
class DBResult(DotDict):
def __init__(self, row, db, table, cursor):
super().__init__()
self.db = db
self.table = table
for idx, col in enumerate(cursor.description):
self[col[0]] = row[idx]
def __setattr__(self, name, value):
if name not in ['db', 'table']:
return self.__setitem__(name, value)
else:
return super().__setattr__(name, value)
def __delattr__(self, name):
if name not in ['db', 'table']:
return self.__delitem__(name)
else:
return super().__delattr__(name)
def __getattr__(self, value, default=None):
options = [value]
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.
def asdict(self):
return self
def Update(self, data={}):
self.update(data)
self.db.update(self.table, self.id, self)
def Remove(self):
self.db.remove(self.table, self.id)
def CreateDatabase():
for name in tables.keys():
db.CreateTable(name)
db.insert('config', {'key': 'version', 'value': dbversion})
db.insert('config', {'key': 'search', 'value': 'ddg'})
for engine in SearchEngines:
db.insert('search', engine)

View file

@ -6,9 +6,12 @@ from ..Lib.IzzyLib.misc import boolean
from ..Lib.IzzyLib import logging
from .connection import db, tables, ParseData
from ..functions import psl, DateTime
from ..functions import psl, DotDict
from PyQt5.QtGui import QPalette, QColor
# todo: add column to specify type instead of just sending everything through Boolean
def config(key=None, default=None):
cache = db.cache['config']
if len(cache.items()) == 0:
@ -47,23 +50,32 @@ 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
def mastoaccount(name=None):
def mastoaccount(name=None, default=False):
cache = db.cache['mastodon']
if len(cache.items()) == 0:
rows = db.fetch('mastodon', single=False)
@ -71,10 +83,10 @@ def mastoaccount(name=None):
for row in rows:
cache.store(row.fullname, row)
if not name:
if not name and not default:
return cache.values()
row = db.cache['mastodon'].fetch(name)
row = db.cache['mastodon'].fetch(name if not name and default else get.config('defaccount'))
if not row:
row = db.fetch('mastodon', fullname=name)
@ -85,14 +97,15 @@ def mastoaccount(name=None):
def search(keyword):
cache = db.cache['search']
default = db.fetch('search', keyword=config('search'))
row = cache.fetch(keyword)
default = config('search')
if not row:
row = db.fetch('search', keyword=keyword)
if not row:
row = default
row = db.fetch('search', keyword=default)
keyword = default
if not row:
logging.error('Not a search keyword:', keyword)
@ -101,3 +114,24 @@ def search(keyword):
cache.store(keyword, row)
return row
def theme(name=None):
if name:
row = db.fetch('themes', name=name)
if not row:
logging.error('Cannot find color theme:', name)
return
row.palette = db.window.themes.ColorToPalette(row.asdict())
return row
else:
rows = db.fetch('themes', single=False)
for row in rows:
row.palette = db.window.themes.ColorToPalette(row.asdict())
return rows

View file

@ -22,7 +22,6 @@ from PyQt5.QtCore import QDateTime
def run(version):
global db
backupdb = var.database + f'.backup-{version}'
version_row = db.fetch('config', key='version')
version = int(version)
if not isfile(backupdb):
@ -89,4 +88,5 @@ def run(version):
db.UpVersion(202008150250)
logging.info(f'Updated database to version {dbversion} :3')

View file

@ -106,8 +106,18 @@ def tab(tabid, url, title):
tabrow = db.fetch('tabs', url=url)
if not tabrow:
result = db.insert('tabs', {'title': title, 'url': url, 'tabid': tabid})
op = 'insert'
db.insert('tabs', {'title': title, 'url': url, 'tabid': tabid})
else:
result = db.update('tabs', tabrow.id, {'title': title, 'url': url, 'tabid': tabid})
op = 'update'
tabrow.Update({'title': title, 'url': url, 'tabid': tabid})
def theme(name, palette):
row = get.theme(name)
data = db.window.themes.PaletteToColor(palette)
if row:
row.Update(data)
else:
db.insert('themes', data)

View file

@ -15,10 +15,13 @@ from PyQt5.QtWidgets import *
from PyQt5 import uic
class EditBookmark(object):
def __init__(self, title=None, url=None, row=None):
self.ui = uic.loadUi(join(var.resources, 'editbookmark.ui'))
self.ui.setWindowTitle('Add/Edit Bookmark')
class EditBookmark(QDialog):
def __init__(self, parent, title=None, url=None, row=None):
super().__init__(parent)
self.ui = uic.loadUi(join(var.resources, 'dialog-bookmark.ui'))
self.parent = parent
self.setLayout(self.ui.layout())
self.setWindowTitle('Add/Edit Bookmark')
self.row = row if row else db.fetch('bookmarks', url=url)
@ -57,19 +60,23 @@ class EditBookmark(object):
if action:
action['cmd'](*action.get('args', []), **action.get('kwargs', {}))
self.ui.deleteLater()
del self
self.close()
self.deleteLater()
del self.parent.open_dialogs['bookmarkedit']
class SiteSettings(object):
def __init__(self, host):
self.ui = uic.loadUi(join(var.resources, 'sitesettings.ui'))
class SiteSettings(QDialog):
def __init__(self, parent, host):
super().__init__(parent)
self.ui = uic.loadUi(join(var.resources, 'dialog-sitesettings.ui'))
self.setLayout(self.ui.layout())
self.parent = parent
self.domain = host
self.ui.setWindowTitle(self.domain.title())
self.setWindowTitle(self.domain.title())
self.row = get.permissions(self.domain, defaults=True)
if not self.row:
if not self.row.domain:
self.ui.reset.setEnabled(False)
else:
@ -82,6 +89,7 @@ class SiteSettings(object):
self.ui.location.setChecked(self.row.location)
self.ui.camera.setChecked(self.row.camera)
self.ui.allowhttp.setChecked(self.row.allowhttp)
self.ui.mastodon.setChecked(self.row.mastodon)
self.ui.save.clicked.connect(lambda : self._button_click('save'))
self.ui.reset.clicked.connect(lambda : self._button_click('reset'))
@ -100,7 +108,8 @@ class SiteSettings(object):
'location': self.ui.location.isChecked(),
'fullscreen': self.ui.fullscreen.isChecked(),
'adblock': self.ui.adblock.isChecked(),
'allowhttp': self.ui.allowhttp.isChecked()
'allowhttp': self.ui.allowhttp.isChecked(),
'mastodon': self.ui.mastodon.isChecked()
}
actions = {
@ -113,17 +122,20 @@ class SiteSettings(object):
if action:
action['cmd'](*action.get('args', []), **action.get('kwargs', {}))
self.close()
self.ui.deleteLater()
del self
del self.parent.open_dialogs['siteedit']
class Toot():
def __init__(self, window, mastodon, data=None, reply=None):
self.window = window
self.mastodon = mastodon
class Toot(QDialog):
def __init__(self, parent, data=None, reply=None):
super().__init__(parent)
self.ui = uic.loadUi(join(var.resources, 'dialog-toot.ui'))
self.setLayout(self.ui.layout())
self.setWindowTitle('New Toot')
self.ui = uic.loadUi(join(var.resources, 'toot.ui'))
self.ui.setWindowTitle('New Toot')
self.parent = parent
self.mastodon = parent.mastodon
self.accounts = {}
self.reply = reply
self.post = None
@ -196,7 +208,7 @@ class Toot():
def _get_post_data(self):
currentacct = self.ui.account.currentData()
data = self.window.mastodon.GetPostData(self.reply, currentacct)
data = self.mastodon.GetPostData(self.reply, currentacct)
if not data:
return
@ -253,34 +265,34 @@ class Toot():
if action:
action['cmd'](*action.get('args', []), **action.get('kwargs', {}))
self.close()
self.ui.deleteLater()
del self
def show(self):
self.ui.exec_()
del self.parent.open_dialogs['toot']
class NewDownload(QDialog):
def __init__(self, download, profile):
super().__init__()
def __init__(self, profile, download):
super().__init__(profile.window)
self.ui = uic.loadUi(join(var.resources, 'newdownload.ui'))
self.ui = uic.loadUi(join(var.resources, 'dialog-download.ui'))
self.setWindowTitle('Download File: ')
self.setModal(True)
self.setFixedHeight(200)
self.setLayout(self.ui.layout())
self.download = download[0]
self.md5 = GetMD5(self.download.url().toString() + '.md5') if get.config('automd5') else None
self.download = download
self.md5 = GetMD5(self.download.item.url().toString() + '.md5') if get.config('automd5') else None
download.md5 = self.md5
self.profile = profile
self.finished = False
if debstable:
## This is a half-assed fix and needs to be improved later
self.file = Path(self.download.url().path()).name
self.file = Path(self.download.item.url().path()).name
else:
self.file = self.download.suggestedFileName()
self.file = self.download.item.suggestedFileName()
self.dir = get.config('dlpath', var.downloadpath)
self.ui.md5.setText(self.md5)
@ -292,12 +304,11 @@ class NewDownload(QDialog):
self.ui.reset.clicked.connect(self._reset)
self.ui.cancel.clicked.connect(lambda *args: self._handle_close_button('cancel'))
self.ui.save.clicked.connect(lambda *args: self._handle_close_button('save'))
self.download.finished.connect(lambda: profile._download_finished([self.download, self.md5]))
self._reset()
self.show()
url = self.download.url().toString()
url = self.download.item.url().toString()
urltext = QFontMetrics(self.ui.urlfilename.font()).elidedText(url, Qt.TextElideMode.ElideMiddle, self.ui.urlfilename.width() - 20)
self.ui.urlfilename.setText(urltext)
@ -312,7 +323,7 @@ class NewDownload(QDialog):
def _hash_changed(self):
text = self.ui.md5.text()
self.md5 = None if text == '' else text
self.download.item.md5 = None if text == '' else text
def _pick_filename(self):
@ -328,17 +339,17 @@ class NewDownload(QDialog):
def _handle_close_button(self, button):
url = self.download.url().toString()
url = self.download.item.url().toString()
if button == 'cancel':
logging.verbose('Cancelled download of', url)
self.download.cancel()
self.download.item.cancel()
elif button == 'save':
logging.verbose('Starting download of', url)
self.SetFilename()
self.download.accept()
self.profile.notif.New('Starting Download', url)
self.download.item.accept()
self.download.item.finished.connect(lambda: self.profile._download_finished(self.download))
self.finished = True
self.close()
@ -349,8 +360,40 @@ class NewDownload(QDialog):
filename = Path(join(self.ui.filename.text()))
if debstable:
self.download.setPath(str(filename))
self.download.item.setPath(str(filename))
else:
self.download.setDownloadDirectory(str(filename.parent))
self.download.setDownloadFileName(str(filename.name))
self.download.item.setDownloadDirectory(str(filename.parent))
self.download.item.setDownloadFileName(str(filename.name))
class HttpAuth(QDialog):
def __init__(self, url, authenticator, parent):
super().__init__(parent.window)
self.ui = uic.loadUi(join(var.resources, 'dialog-authentication.ui'))
self.ui.savepass.setEnabled(False)
self.setLayout(self.ui.layout())
self.webview = parent
self.url = url
self.auth = authenticator
self.username = None
self.password = None
self.result = False
text = self.ui.realm.text().format(host=url.host(), msg=self.auth.realm())
self.ui.realm.setText(text)
self.ui.login.clicked.connect(lambda : self._handle_close_button('login'))
self.ui.cancel.clicked.connect(self._handle_close_button)
def _handle_close_button(self, button=None):
if button == 'login':
self.auth.setUser(self.ui.username.text())
self.auth.setPassword(self.ui.password.text())
self.result = True
self.close()
self.deleteLater()

View file

@ -1,106 +0,0 @@
import threading, sys, os
from datetime import datetime
from os.path import join
from .Lib.IzzyLib import logging
from publicsuffixlist import PublicSuffixList
from publicsuffixlist.update import updatePSL
from . import pyqtver, debstable
from .database import db, get, put
from .functions import GetSoftware, httpclient
from PyQt5.QtCore import QUrl
from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInterceptor
#if hasattr(sys, '_MEIPASS'):
#os.makedirs(join(sys._MEIPASS, 'publicsuffixlist'), exist_ok=True)
#with open(join(sys._MEIPASS, 'publicsuffixlist', 'public_suffix_list.dat'), 'w') as fd:
#resp = httpclient.request('GET', 'https://publicsuffix.org/list/public_suffix_list.dat')
#fd.write(resp.data.decode())
psl = PublicSuffixList()
extract = psl.privatesuffix
## blocked domains and urls (will be optional in the future)
blockedDomain = [
'doubleclick.net',
'rubiconproject.com',
'outbrain.com',
'googletagservices.com',
'amazon-adsystem.com',
'optimizely.com',
'adswizz.com'
]
blockedURL = {
'youtube.com': [
'/api/stats/ads',
'/pagead/adview',
'/pagead/lvz'
'/get_midroll/info'
],
'soundcloud.com': [
'/audio-ads'
],
'facebook.com': [
'/tr']
}
class WebFilter(QWebEngineUrlRequestInterceptor):
def __init__(self, window):
super().__init__()
self.window = window
self.threads = {}
def interceptRequest(self, info):
requrl = info.requestUrl()
reqdomain = requrl.host()
reqtopdomain = extract(reqdomain)
reqpath = requrl.path()
pageurl = info.firstPartyUrl()
pagedomain = pageurl.host()
pagetopdomain = extract(pagedomain)
pagepath = pageurl.path()
pagerow = get.permissions(pagedomain, defaults=True)
## Prevent secure pages from loading insecure content
if not pagerow.allowhttp and requrl.scheme() == 'http' and pageurl.scheme() == 'https':
logging.verbose('Blocked insecure content:', requrl.toString().split('?', 1)[0])
info.block(True)
return
## Block requests if they're in the block list
if self.window.adblock and pagerow.adblock and (reqpath in blockedURL.get(reqdomain, []) or reqdomain in blockedDomain or reqtopdomain in blockedDomain):
logging.verbose('Blocked content:', requrl.toString().split('?', 1)[0])
info.block(True)
return
## Set custom headers
info.setHttpHeader("trans-rights".encode(), "are human rights".encode())
## Check if domain is a mastodon instance
if not pagerow.domain and not pagerow.mastodon and not any(map(requrl.toString().startswith, ['about:', 'chrome://'])):
if not pagedomain:
return
if self.threads.get(pagedomain):
return
self.threads[pagedomain] = threading.Thread(target=GetSoftware, args=[pagedomain])
self.threads[pagedomain].start()
## Delete the thread object once it's finished
elif self.threads.get(pagedomain):
del self.threads[pagedomain]
## Google is a fuck and is very picky about what browsers are supported
#if 'google.com' in [pagetopdomain, reqtopdomain]:
#info.setHttpHeader('user-agent'.encode(), 'Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0'.encode())

View file

@ -7,6 +7,7 @@ from urllib.parse import urlparse
from datetime import datetime
from configparser import ConfigParser
from pathlib import Path
from collections import OrderedDict
import urllib3
@ -47,14 +48,19 @@ class Notifications(object):
self.New('Error', message)
def Icon(name):
def Icon(name, size=None):
filename = join(var.iconpath, name + '.svg')
if not isfile(filename):
logging.error('Invalid icon name:', name)
return QIcon(join(var.iconpath, 'missing.svg'))
icon = QIcon(join(var.iconpath, 'missing.svg'))
return QIcon(filename)
icon = QIcon(filename)
if size:
icon = QIcon(icon.pixmap(16))
return icon
def AddShortcut(key, action, widget):
@ -243,25 +249,162 @@ def Decode(byte):
return TextCodec.toUnicode(byte)
## subclass QMenu because there's no way to get the number of items
## subclass QWebEngineScript to add extra functions
class WebEngineScript(QWebEngineScript):
def __init__(self, collection=None, filename=None, user=True):
super().__init__()
self.collection = collection
self.user = user
self.filename = None
self.enabled = False
if filename:
self.initLoad(filename)
def initLoad(self, filename):
self.filename = Path(filename)
with self.filename.open() as fd:
self.setSourceCode(fd.read())
def enable(self):
if not self.enabled:
logging.verbose('Enabling script:', self.name())
self.enabled = True
return self.collection.insert(self)
def disable(self):
if self.enabled:
logging.debug('Disabling script:', self.name())
self.enabled = False
return self.collection.remove(self)
## subclass QMenu because there's no easy way to get specific items
class Menu(QMenu):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.count = 0
def __init__(self, parent, title=None):
super().__init__(parent)
self.items = OrderedDict()
def AddAction(self, name, callback):
self.addAction(name, callback)
self.count += 1
if title:
self.setTitle(title)
def AddItem(self, label, callback, *args, icon=None, **kwargs):
if self.items.get(label):
logging.error('An action with this name already exists:', label)
return
self.items[label] = (self.addAction(label, lambda : callback(*args, **kwargs)))
if icon:
self.items[label].setIcon(icon if type(icon) == QIcon else QIcon(icon))
return True
def AddItemAfter(self, indexLabel, label, callback, *args, **kwargs):
index = self.items.get(indexLabel)
newdict = self.items.__class__()
if not index:
self.error('Not a menu action:', indexLabel)
return
self.clear()
for name, action in self.items.items():
newdict[name] = action
if name == indexLabel:
newdict[label] = self.AddItem(label, callback, *args, **kwargs)
self.items.clear()
self.items.update(newdict)
return True
def AddActionAfter(self, action, indexAction):
label = action.text()
indexLabel = index.text()
index = self.items.get(indexLabel)
newdict = self.items.__class__()
if not index:
self.error('Not a menu action:', indexLabel)
return
self.clear()
for name, action in self.items.items():
newdict[name] = self.addAction(name, action)
if name == indexLabel:
newdict[label] = self.AddItem(label, callback, *args, **kwargs)
self.items.clear()
self.items.update(newdict)
return True
def MoveItem(self, label, indexLabel):
action = self.items.get(label)
index = self.items.get(indexLabel)
newdict = self.items.__class__()
if None in [action, index]:
logging.error(f'Invalid action or index action: {label}: {action}, {indexLabel}: {index}')
return
self.clear()
for name, action in self.items.items():
newdict[name] = self.addAction(name, action)
if name == indexLabel:
newdict[label] = self.addAction(label, action)
self.items.clear()
self.items.update(newdict)
return True
def RemoveItem(self, label):
action = self.items.get(label)
if not action:
logging.error('Invalid action:', label)
return
self.removeAction(action)
action.DeleteLater()
del action
return True
def AddSeparator(self, name='separator'):
item = self.items.get(name)
count = 0
while item:
count += 1
item = self.items.get(name + str(count))
label = name + str(count) if count > 0 else name
self.items[label] = self.addSeparator()
return True
def AddSeparator(self):
self.addSeparator()
self.count += 1
def MoreThan(self, num):
return self.count > num
return len(self.items) > num
def LessThan(self, num):
return self.count < num
return len(self.items) < num
def DateTime(timestamp):
@ -288,38 +431,98 @@ def PrintMethods(obj):
def ParseSoundcloudInfo(data):
if not data:
return False
data = data.split(':::')
if len(data) != 6:
logging.debug('Wrong number of soundcloud data slices:', data)
return
return False
artist, artistLink, title, titleLink, progress, length = data
info = {
info = DotDict({
'artist': unescape(artist),
'artistLink': artistLink,
'title': unescape(title.replace('Current track: ', '')),
'titleLink': titleLink,
'progress': progress,
'length': length
}
})
return info
class DotDict(dict):
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(value)
elif value:
raise TypeError('The value must be a JSON string, dict, or another DotDict object, not', value.__class__)
if kwargs:
print('From kwargs')
self.update(kwargs)
def ToJson(self, **kwargs):
return self.__str__(**kwargs)
def FromJson(self, string):
data = json.loads(string)
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):
lines = [line for line in traceback.format_exception(exctype, value, tb)]
sys._excepthook(exctype, value, tb)
if lines[-1] in ['\n', '\n ']:
lines.remove(lines)
# Apparently the line above is enough to not bring down the whole browser
# Gonna keep custom handler here for now though
#lines = [line for line in traceback.format_exception(exctype, value, tb)]
error = ''.join(lines)
print(f'\n------------\n{error}------------')
#if lines[-1] in ['\n', '\n ']:
#lines.remove(lines)
#error = ''.join(lines)
#print(f'\n------------\n{error}------------')
TextCodec = QTextCodec.codecForMib(106)
useragent = UserAgentGen()
httptimeout = urllib3.Timeout(connect=5, read=2)
httpclient = urllib3.PoolManager(num_pools=100, timeout=httptimeout, headers={'user-agent': useragent})
httpclient = urllib3.PoolManager(num_pools=100, timeout=httptimeout, headers={'user-agent': f'{__application__}/{__version__}'})
psl = PublicSuffixList()

View file

@ -1,29 +1,27 @@
import sys, os, time, json, signal, asyncio, tracemalloc
import sys, os, json, signal, asyncio, tracemalloc
from optparse import OptionParser
from urllib.parse import urlencode, urlparse, quote_plus
from urllib.parse import urlparse, quote_plus
from os.path import join, abspath
from base64 import b64encode, b64decode
from pathlib import Path
from .Lib.IzzyLib import logging
from .Lib.IzzyLib.misc import boolean
from . import isWindows, isLinux, debstable, dialogs, tabs, __version__
from . import isWindows, debstable, dialogs, tabs, __version__
from .urlhandlers import LocalHandler, RegisterScheme
from .webview import WebEngineView
from .profile import WebEngineProfile
from .webprofile import WebEngineProfile
from .config import var
from .database import db, get, put
from .mastodon import Mastodon
from .mbus import Client, Server
from .themes import SetupThemes, ChangeTheme
#from .themes import ColorTheme
from .arguments import args
from .functions import (
Icon, AddShortcut, UserAgentGen,
ExceptionHandler, PrintMethods, Notifications, Menu
Icon, AddShortcut, UserAgentGen, DotDict,
ExceptionHandler, Notifications, Menu
)
if isWindows:
import ctypes
@ -42,6 +40,7 @@ from PyQt5 import uic
## Prevent errors from bringing down the whole browser
sys._excepthook = sys.excepthook
sys.excepthook = ExceptionHandler
logging.setConfig({'level': get.config('loglevel', 'INFO')})
@ -62,11 +61,15 @@ class Browser(QMainWindow):
else:
self.snapshot = None
SetupThemes(app)
if not debstable:
RegisterScheme(var.local)
db.window = self
self.jsdebug = False
self.darktheme = True if get.config('darktheme') in [1, '1'] else False
self.app = app
self.default_style = app.style().objectName().lower()
self.default_palette = app.style().standardPalette()
#self.themes = ColorTheme(self)
self.clipboard = app.clipboard()
self.threadpool = QThreadPool()
self.notif = Notifications()
@ -77,7 +80,7 @@ class Browser(QMainWindow):
'y': 100,
'maximized': True
}
self.statedata.update(json.loads(get.config('state', "{}")))
## Setup empty variables for use later
@ -86,17 +89,22 @@ class Browser(QMainWindow):
self.socket = None
self.urlhover = None
self.fullscreen = False
self.widgettabs = {}
self.shutdown = False
self.widgettabs = DotDict()
self.open_dialogs = {}
WebEngineProfile(self)
#app.setStyle('fusion')
#ChangeTheme()
self.profile = WebEngineProfile(self)
self.local_scheme_handler = LocalHandler(self)
self.mastodon = Mastodon()
#logging.setConfig({'systemnotif': self.notif})
if self.darktheme:
ChangeTheme('dark')
#app.setStyle('fusion')
#self.themes.ChangeTheme()
# Re-enable when color themes are fixed
#if theme != 'Default':
#self.themes.ChangeTheme(theme)
self.ui = uic.loadUi(join(var.resources, 'window.ui'))
self.setCentralWidget(self.ui)
@ -142,19 +150,15 @@ class Browser(QMainWindow):
self.ui.toot.clicked.connect(lambda : self._open_dialog('toot'))
## Keyboard shortcuts
AddShortcut('Ctrl+L', self._focus_urlbar, self.ui.url)
AddShortcut('Ctrl+T', lambda *args: self.NewWebTab(switch=True), self.ui)
AddShortcut('Ctrl+R', lambda *args: self.ui.tabs.currentWidget().reload(), self.ui)
AddShortcut('Ctrl+W', lambda *args: self.CloseTab(), self.ui)
AddShortcut('Ctrl+N', lambda *args: self.NewWebTab('https://www.youtube.com/watch?v=dQw4w9WgXcQ', switch=True), self.ui)
AddShortcut('Ctrl+F', lambda *args: self.ui.tabs.currentWidget().searchbar.Search('toggle'), self.ui)
AddShortcut('F3', lambda *args: self.ui.tabs.currentWidget().searchbar.Search('toggle'), self.ui)
AddShortcut('F11', lambda *args: self.Fullscreen(), self.ui)
# keeping this here for testing purposes
#self.profile.setHttpUserAgent('Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0')
self.mastodon = Mastodon(self.profile.httpUserAgent())
AddShortcut('Ctrl+L', self._focus_urlbar, self)
AddShortcut('Ctrl+T', lambda *args: self.NewWebTab(switch=True), self)
AddShortcut('Ctrl+R', lambda *args: self.ui.tabs.currentWidget().reload(), self)
AddShortcut('Ctrl+W', lambda *args: self.CloseTab(), self)
AddShortcut('Ctrl+N', lambda *args: self.NewWebTab('https://www.youtube.com/watch?v=dQw4w9WgXcQ', switch=True), self)
AddShortcut('Ctrl+F', lambda *args: self.ui.tabs.currentWidget().searchbar.Search('toggle'), self)
AddShortcut('F3', lambda *args: self.ui.tabs.currentWidget().searchbar.Search('toggle'), self)
AddShortcut('F5', lambda *args: self.ui.tabs.currentWidget().reload(), self)
AddShortcut('F11', lambda *args: self.Fullscreen(), self)
for url in urls:
self.NewWebTab(url)
@ -162,13 +166,8 @@ class Browser(QMainWindow):
for row in db.fetch('tabs', sort='tabid', single=False):
self.NewWebTab(row.url)
try:
activetab = int(get.config('activetab', 0))
self.ui.tabs.setCurrentIndex(activetab)
except Exception as e:
print(e)
activetab = int(get.config('activetab', 0))
self.ui.tabs.setCurrentIndex(activetab)
self._prevent_empty_tabview()
## Restore the window position and size
@ -203,49 +202,14 @@ class Browser(QMainWindow):
self.ui.url.selectAll()
def _close_window(self, event=None):
lock = Path(var.lock)
opentabs = []
tabrange = list(reversed(range(0, self.ui.tabs.count())))
put.config('activetab', str(self.ui.tabs.currentIndex()))
self.SaveWinState()
for tabid in ([0] if len(tabrange) < 1 else tabrange):
widget = self.ui.tabs.widget(tabid)
if widget.windowTitle():
continue
title = widget.title()
url = widget.url().toString()
opentabs.append(url)
put.tab(tabid, url, title)
self.CloseTab(tabid, shutdown=True)
for row in db.fetch('tabs', single=False):
if row.url not in opentabs:
db.remove('tabs', row.id)
db.close()
logging.verbose('Saved state and tabs. Exiting...')
for srv in [self.server, self.socket]:
if srv:
srv.stop()
if event:
event.accept()
if lock.exists():
lock.unlink()
def _button_click(self, button):
widget = self.ui.tabs.currentWidget()
homepage = get.config('homepage', 'https://ddg.gg')
if button == 'newtab':
self.NewWebTab(switch=True)
return
if hasattr(widget, 'LoadUrl'):
actions = {
'back': {'cmd': widget.back},
@ -253,7 +217,6 @@ class Browser(QMainWindow):
'stop': {'cmd': widget.stop},
'refresh': {'cmd': widget.reload},
'home': {'cmd': widget.LoadUrl, 'args': [homepage]},
'newtab': {'cmd': self.NewWebTab, 'kwargs': {'switch': True}},
'go': {'cmd': widget.LoadUrl, 'args': [self.ui.url.text()]}
}
@ -277,8 +240,10 @@ class Browser(QMainWindow):
logging.error('No mastodon accounts')
return
widget = dialogs.Toot(self, self.mastodon)
widget.show()
widget = self.open_dialogs.get(dialog)
if widget:
widget.activateWindow()
return
webview = self.ui.tabs.currentWidget()
@ -292,6 +257,10 @@ class Browser(QMainWindow):
'siteedit': {
'cmd': dialogs.SiteSettings,
'args': [webview.url().host()]
},
'toot': {
'cmd': dialogs.Toot,
'args': [self]
}
}
@ -300,7 +269,8 @@ class Browser(QMainWindow):
if not action:
return
action['cmd'](*action.get('args', []), **action.get('kwargs', {})).ui.show()
self.open_dialogs[dialog] = action['cmd'](self, *action.get('args', []), **action.get('kwargs', {}))
self.open_dialogs[dialog].show()
def _update_webaction_buttons(self, name=None, state=None):
@ -360,6 +330,55 @@ class Browser(QMainWindow):
self.NewWebTab()
def closeEvent(self, event=None):
## Make sure this function only fires once
if self.shutdown:
return
self.shutdown = True
if event:
event.accept()
lock = Path(var.lock)
opentabs = []
tabrange = list(reversed(range(0, self.ui.tabs.count())))
put.config('activetab', str(self.ui.tabs.currentIndex()))
self.SaveWinState()
for tabid in ([0] if len(tabrange) < 1 else tabrange):
widget = self.ui.tabs.widget(tabid)
if widget.windowTitle():
continue
title = widget.title()
url = widget.url().toString()
if widget.devpage.view:
widget.devpage.closeEvent()
widget.devpage.destroy()
opentabs.append(url)
put.tab(tabid, url, title)
self.CloseTab(tabid, shutdown=True)
for row in db.fetch('tabs', single=False):
if row.url not in opentabs:
db.remove('tabs', row.id)
db.close()
logging.verbose('Saved state and tabs. Exiting...')
for srv in [self.server, self.socket]:
if srv:
srv.stop()
if lock.exists():
lock.unlink()
def SaveWinState(self, width=None, height=None, x=None, y=None):
winstate = self.windowState() in [Qt.WindowActive, Qt.WindowNoState]
maximized = self.windowState() == Qt.WindowMaximized
@ -413,82 +432,82 @@ class Browser(QMainWindow):
def NewWebTab(self, url=None, switch=False, nexttab=False):
#layout = QVBoxLayout()
webview = WebEngineView(self, url)
#layout.addWidget(webview.search)
#layout.addWidget(webview)
webview = WebEngineView(self, url if url else get.config('homepage', 'https://ddg.gg'))
if nexttab and get.config('nexttab', True):
index = self.ui.tabs.currentIndex() + 1
self.ui.tabs.insertTab(index, webview, 'New Tab')
current_index = self.ui.tabs.currentIndex() + 1
index = self.ui.tabs.insertTab(current_index, webview, 'New Tab')
else:
self.ui.tabs.addTab(webview, 'New Tab')
index = self.ui.tabs.addTab(webview, 'New Tab')
if switch:
index = self.ui.tabs.indexOf(webview)
self.ui.tabs.setCurrentIndex(index)
if not url:
self.ui.url.setFocus()
self.ui.url.selectAll()
return webview
def NewWidgetTab(self, widgetname, switch=True):
widgetname = widgetname.title()
currwidget = self.widgettabs.get(widgetname)
if currwidget:
index = self.ui.tabs.indexOf(currwidget.ui)
index = self.ui.tabs.indexOf(currwidget)
self.ui.tabs.setCurrentIndex(index)
return
widgettabs = {
'bookmarks': tabs.Bookmarks,
'history': tabs.History,
'preferences': tabs.Preferences
'Bookmarks': tabs.Bookmarks,
'History': tabs.History,
'Downloads': tabs.Downloads,
'Preferences': tabs.Preferences
}
widget = widgettabs.get(widgetname)
if not widget:
logging.error('OOF!')
logging.error('Failed to open widget tab:', widgetname)
return
self.widgettabs[widgetname] = widget(self)
newwidget = self.widgettabs[widgetname]
self.ui.tabs.addTab(newwidget.ui, newwidget.title)
widget = self.widgettabs[widgetname]
widget.setWindowTitle(widgetname)
index = self.ui.tabs.addTab(widget, widget.windowTitle())
if switch:
index = self.ui.tabs.indexOf(newwidget.ui)
self.ui.tabs.setCurrentIndex(index)
return widget
def CloseTab(self, index=None, shutdown=False):
widgettabs = self.widgettabs
tabs = self.ui.tabs
if index == None:
index = -1
index = int(index)
if index < 0:
index = self.ui.tabs.currentIndex()
index = tabs.currentIndex()
widget = self.ui.tabs.widget(index)
tabtitle = self.ui.tabs.tabText(index).lower()
widget = tabs.widget(index)
tabtitle = widget.windowTitle()
# not even sure if this helps with clearing memory, so it's staying for now
try:
widget.ui.deleteLater()
except:
'heck'
if widgettabs.get(tabtitle):
del widgettabs[tabtitle]
tabs.removeTab(index)
widget.deleteLater()
if tabtitle in ['preferences', 'bookmarks', 'history']:
del self.widgettabs[tabtitle]
self.ui.tabs.removeTab(index)
if not shutdown:
self._prevent_empty_tabview()
tabs.currentWidget().setFocus()
class UrlBarEsc(QObject):
@ -513,7 +532,7 @@ class UrlBarEsc(QObject):
## Custom tabbar class
class TabBar(QTabBar):
def __init__(self, window):
super().__init__()
super().__init__(window)
self.setElideMode(Qt.TextElideMode.ElideRight)
self.setAutoHide(int(get.config('hidetabs', 1)))
self.setMovable(True)
@ -523,6 +542,11 @@ class TabBar(QTabBar):
self.setDocumentMode(True)
self.window = window
self.setStyleSheet('''
QTabBar::tab:selected {font: bold;}
QTabBar::tab-bar {alignment: center;}
''')
## Set tabs to even take up full width
def tabSizeHint(self, index):
compensation = 0 #I'll need this later
@ -586,6 +610,11 @@ def main():
app.setOrganizationDomain('barkshark.xyz')
app.setWindowIcon(Icon('icon'))
if get.config('remotedebug', False):
debugport = get.config('remotedebugport', '8018')
debughost = 'still gotta sort this out'
os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = f'127.0.0.1:{remotedebugport}'
host = get.config('mhost', 'localhost')
port = get.config('mport', 8008)
@ -595,7 +624,7 @@ def main():
mbus = Client(host, port)
else:
mbus = Client(sock=True)
mbus = Client(sock=Path(var.profilepath, 'mbus.sock'))
if mbus.setup():
logging.info('QtWeb already open. Bringing up window...')
@ -614,9 +643,10 @@ def main():
SetPid(lock)
window = Browser(app)
window.server = Server(window)
app.aboutToQuit.connect(window._close_window)
app.aboutToQuit.connect(window.close)
if not isWindows:
window.socket = Server(window, True)

View file

@ -1,15 +1,13 @@
import json, shutil
from urllib import request
from urllib.parse import urlparse, urlencode
from urllib.error import HTTPError
from urllib.parse import urlparse
from os.path import join, isfile, basename
from os import makedirs, remove
from .Lib.IzzyLib import logging
from .config import var
from .database import db, put, get
from .functions import httpclient, UserAgentGen
from .functions import httpclient
from PyQt5.QtCore import *
from PyQt5.QtGui import *
@ -17,9 +15,8 @@ from PyQt5.QtGui import *
# I was thinking about replacing this with Mastodon.py, but it works fine, so I'm not sure now
class Mastodon(object):
def __init__(self, ua):
def __init__(self):
self.accounts = {}
self.ua = ua
def _request(self, api, fullname=None, method='GET', data=None, domain=None, apikey=None):
@ -41,8 +38,7 @@ class Mastodon(object):
options = {
'headers': {
'Content-Type': 'application/json',
'User-Agent': UserAgentGen()
'Content-Type': 'application/json'
}
}

View file

@ -1,4 +1,4 @@
import asyncio, socket, json, sys, traceback, tracemalloc, argparse, datetime, time, linecache
import asyncio, socket, json, sys, traceback, tracemalloc, argparse, datetime, linecache
from os.path import join
from pathlib import Path
@ -78,7 +78,10 @@ class Server(object):
self.loop.close()
if self.socket:
Path(self.socket).unlink()
sock = Path(self.socket)
if sock.exists():
sock.unlink()
def restart(self):
@ -113,11 +116,11 @@ class Server(object):
return False
if action == 'info':
return webview.JsAction.SoundcloudInfo(self.get_soundcloud_info)
webview.JsAction.SoundcloudInfo(self.get_soundcloud_info)
await asyncio.sleep(0.1)
text = self.scinfo
text = self.scinfo.copy() if self.scinfo else None
self.scinfo = None
return text
@ -136,8 +139,16 @@ class Server(object):
self.scinfo = ParseSoundcloudInfo(value)
async def search(self, *args, **kwargs):
self.GetWebview().search.Search(*args, **kwargs)
async def search(self, *args):
searchbar = self.GetWebview().searchbar
if not args:
searchbar.Search('reset')
searchbar.hide()
return
searchbar.Search(text=' '.join(args))
searchbar.show()
async def new_tab(self, *args, **kwargs):
@ -219,9 +230,8 @@ class Server(object):
def trace_top(self, num=15, key_type='lineno'):
if not self.window.snapshot:
self.trace_snapshot()
snapshot = self.window.snapshot
snapshot = snapshot.filter_traces((
snapshot = self.window.snapshot.filter_traces((
tracemalloc.Filter(False, '<frozen importlib._bootstrap>'),
tracemalloc.Filter(False, '<unknown>'),
))
@ -266,7 +276,7 @@ class Server(object):
try:
reqdata = (await reader.read(81920)).decode('utf8')
request = json.loads(reqdata)
response = {'msg': 'OK'}
response = {}
cmd = request.get('cmd')
args = request.get('args', [])
@ -290,11 +300,14 @@ class Server(object):
output = await command(*args, **kwargs)
if type(output) == dict:
response = output
response = {'msg': output}
elif output == False:
response = {'err': 'Server error'}
elif output == None:
response = {'msg': 'OK'}
elif output:
response['msg'] = output
@ -322,8 +335,8 @@ class Server(object):
class Client(object):
def __init__(self, host=None, port=None, sock=None):
self.socket = join(var.profilepath, 'mbus.sock') if sock else None
self.conn = (socket.gethostbyname(host), port) if not sock else self.socket
self.socket = Path(sock) if type(sock) == str else sock
self.conn = (socket.gethostbyname(host), port) if not sock else str(self.socket)
def send(self, cmd, *args, **kwargs):
@ -361,7 +374,7 @@ class Client(object):
def setup(self):
if self.socket and not Path(self.socket).exists():
if self.socket and not self.socket.exists():
logging.verbose('Unix socket does not exist')
return False
@ -386,6 +399,7 @@ def main(args=sys.argv[1:]):
parser.add_argument('args', nargs=argparse.REMAINDER, help='The arguments to pass to the command')
parser.add_argument('-a', '--address', help='Address to connect to. Default: localhost')
parser.add_argument('-p', '--port', help='Port number to connect to. Default: 8008')
parser.add_argument('-e', '--profile', help='Profile to use for a unix socket connection')
pargs = parser.parse_args(args)
host = '127.0.0.1' if not pargs.address else pargs.address
@ -411,7 +425,8 @@ def main(args=sys.argv[1:]):
api = Client(host, port)
else:
api = Client(sock=join(var.profilepath, 'mbus.sock'))
profilepath = Path(var.datapath).joinpath('Default' if not pargs.profile else pargs.profile)
api = Client(sock=profilepath.joinpath('mbus.sock'))
if not api.setup():
logging.error('Failed to connect to browser')
@ -435,6 +450,9 @@ def main(args=sys.argv[1:]):
if msg:
logging.info(msg)
else:
logging.error('No output')
if __name__ == '__main__':
main()

View file

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>479</width>
<height>292</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="1" colspan="3">
<widget class="QLineEdit" name="username"/>
</item>
<item row="2" column="1" colspan="3">
<widget class="QLineEdit" name="password">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Username</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Password</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="savepass">
<property name="statusTip">
<string>Does nothing rn</string>
</property>
<property name="text">
<string>Save Password</string>
</property>
</widget>
</item>
<item row="4" column="3">
<widget class="QPushButton" name="login">
<property name="text">
<string>Login</string>
</property>
</widget>
</item>
<item row="3" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>169</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="2">
<widget class="QPushButton" name="cancel">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
<item row="4" column="1">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="0" colspan="4">
<widget class="QLabel" name="realm">
<property name="text">
<string>Authentication requested from {host}. Server message: {msg}</string>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>username</tabstop>
<tabstop>password</tabstop>
<tabstop>savepass</tabstop>
<tabstop>cancel</tabstop>
<tabstop>login</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View file

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>568</width>
<height>447</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="titlelabel">
<property name="text">
<string>Title</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="urllabel">
<property name="text">
<string>Url</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QLabel" name="categorylabel">
<property name="text">
<string>Category</string>
</property>
</widget>
</item>
<item row="2" column="2" colspan="4">
<widget class="QLineEdit" name="category"/>
</item>
<item row="3" column="0" colspan="2">
<widget class="QLabel" name="descriptionlabel">
<property name="text">
<string>Description</string>
</property>
</widget>
</item>
<item row="3" column="2" colspan="4">
<widget class="QPlainTextEdit" name="description">
<property name="minimumSize">
<size>
<width>230</width>
<height>170</height>
</size>
</property>
</widget>
</item>
<item row="4" column="3">
<widget class="QPushButton" name="discard">
<property name="text">
<string>Cancel</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item row="4" column="4">
<widget class="QPushButton" name="remove">
<property name="text">
<string>Remove</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item row="4" column="5">
<widget class="QPushButton" name="save">
<property name="text">
<string>Save</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="2" colspan="4">
<widget class="QLineEdit" name="url">
<property name="maxLength">
<number>2048</number>
</property>
</widget>
</item>
<item row="0" column="2" colspan="4">
<widget class="QLineEdit" name="title"/>
</item>
<item row="4" column="2">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>228</width>
<height>28</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -1,21 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>324</width>
<height>345</height>
<height>434</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>324</width>
@ -29,15 +23,32 @@
</size>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<property name="modal">
<bool>true</bool>
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>55</width>
<height>28</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="discard">
<property name="text">
<string>Close</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="0" colspan="4">
<widget class="QScrollArea" name="scrollArea">
<property name="frameShape">
@ -52,7 +63,7 @@
<x>0</x>
<y>0</y>
<width>316</width>
<height>304</height>
<height>393</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
@ -179,6 +190,16 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="mastodon">
<property name="toolTip">
<string>Is this domain a mastodon instance? Hint: Enable 'Auto-detect Mastodon instances' under advanced settings to enable auto-detection</string>
</property>
<property name="text">
<string>Mastodon Instance</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
@ -196,39 +217,6 @@
</widget>
</widget>
</item>
<item row="1" column="0">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>55</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="discard">
<property name="text">
<string>Close</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="reset">
<property name="text">
<string>Reset</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QPushButton" name="save">
<property name="text">
@ -242,23 +230,18 @@
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="reset">
<property name="text">
<string>Reset</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>scrollArea</tabstop>
<tabstop>adblock</tabstop>
<tabstop>allowhttp</tabstop>
<tabstop>fullscreen</tabstop>
<tabstop>cookies</tabstop>
<tabstop>notifications</tabstop>
<tabstop>capture</tabstop>
<tabstop>microphone</tabstop>
<tabstop>location</tabstop>
<tabstop>camera</tabstop>
<tabstop>discard</tabstop>
<tabstop>reset</tabstop>
<tabstop>save</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View file

@ -1,44 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="windowModality">
<enum>Qt::WindowModal</enum>
</property>
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>486</width>
<height>306</height>
<width>516</width>
<height>337</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<property name="modal">
<bool>true</bool>
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="6">
<widget class="QLabel" name="replytext">
<property name="text">
<string/>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::NoTextInteraction</set>
</property>
</widget>
</item>
<item row="1" column="0" colspan="6">
<widget class="QLineEdit" name="warning">
<property name="placeholderText">
@ -46,57 +21,6 @@
</property>
</widget>
</item>
<item row="2" column="0" colspan="6">
<widget class="QPlainTextEdit" name="content">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustIgnored</enum>
</property>
<property name="tabChangesFocus">
<bool>true</bool>
</property>
<property name="backgroundVisible">
<bool>true</bool>
</property>
<property name="centerOnScroll">
<bool>false</bool>
</property>
<property name="placeholderText">
<string>Message body</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="count">
<property name="text">
<string>500</string>
</property>
</widget>
</item>
<item row="3" column="1">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="2">
<widget class="QComboBox" name="account"/>
</item>
<item row="3" column="3">
<widget class="QComboBox" name="privacy">
<item>
@ -121,16 +45,6 @@
</item>
</widget>
</item>
<item row="3" column="4">
<widget class="QPushButton" name="cancel">
<property name="text">
<string>Cancel</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item row="3" column="5">
<widget class="QPushButton" name="post">
<property name="text">
@ -141,16 +55,85 @@
</property>
</widget>
</item>
<item row="3" column="1">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>125</width>
<height>28</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="2">
<widget class="QComboBox" name="account"/>
</item>
<item row="3" column="4">
<widget class="QPushButton" name="cancel">
<property name="text">
<string>Cancel</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="0" colspan="6">
<widget class="QLabel" name="replytext">
<property name="text">
<string/>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::NoTextInteraction</set>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="count">
<property name="text">
<string>500</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="6">
<widget class="QPlainTextEdit" name="content">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustIgnored</enum>
</property>
<property name="tabChangesFocus">
<bool>true</bool>
</property>
<property name="backgroundVisible">
<bool>false</bool>
</property>
<property name="centerOnScroll">
<bool>false</bool>
</property>
<property name="placeholderText">
<string>Message body</string>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>warning</tabstop>
<tabstop>content</tabstop>
<tabstop>account</tabstop>
<tabstop>privacy</tabstop>
<tabstop>cancel</tabstop>
<tabstop>post</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

Binary file not shown.

View file

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>461</width>
<height>132</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>132</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>132</height>
</size>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="1" colspan="2">
<widget class="QLabel" name="url">
<property name="text">
<string>OwO what's this?</string>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QLabel" name="percent">
<property name="text">
<string>OwO %</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Location</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QProgressBar" name="progress">
<property name="value">
<number>0</number>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="invertedAppearance">
<bool>false</bool>
</property>
<property name="textDirection">
<enum>QProgressBar::TopToBottom</enum>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QLabel" name="location">
<property name="text">
<string>*notices your download*</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>URL</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Progress</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>MD5 Hash</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="md5"/>
</item>
<item row="3" column="2">
<widget class="QPushButton" name="update">
<property name="text">
<string>Update</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -1,149 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>467</width>
<height>320</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="4">
<layout class="QFormLayout" name="formLayout">
<property name="horizontalSpacing">
<number>5</number>
</property>
<property name="verticalSpacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item row="1" column="0">
<widget class="QLabel" name="urllabel">
<property name="text">
<string>Url</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="url">
<property name="maxLength">
<number>2048</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="categorylabel">
<property name="text">
<string>Category</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="category"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="descriptionlabel">
<property name="text">
<string>Description</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QPlainTextEdit" name="description">
<property name="minimumSize">
<size>
<width>230</width>
<height>170</height>
</size>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="title"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="titlelabel">
<property name="text">
<string>Title</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>198</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="discard">
<property name="text">
<string>Cancel</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="remove">
<property name="text">
<string>Remove</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QPushButton" name="save">
<property name="text">
<string>Save</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>title</tabstop>
<tabstop>url</tabstop>
<tabstop>category</tabstop>
<tabstop>description</tabstop>
<tabstop>discard</tabstop>
<tabstop>remove</tabstop>
<tabstop>save</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>481</width>
<height>418</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QPushButton" name="pause">
<property name="text">
<string>Pause All</string>
</property>
</widget>
</item>
<item row="0" column="3">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="clear">
<property name="text">
<string>Clear Finished</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="resume">
<property name="text">
<string>Resume All</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="4">
<widget class="QListWidget" name="downloads"/>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>677</width>
<height>420</height>
<width>696</width>
<height>489</height>
</rect>
</property>
<property name="windowTitle">
@ -30,7 +30,7 @@
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QTabWidget" name="tabWidget">
<widget class="QTabWidget" name="tabs">
<property name="tabPosition">
<enum>QTabWidget::West</enum>
</property>
@ -42,13 +42,141 @@
<string>General</string>
</attribute>
<layout class="QGridLayout" name="heckinfuck">
<item row="6" column="0">
<widget class="QCheckBox" name="hidetabs">
<item row="3" column="0" colspan="4">
<layout class="QGridLayout" name="gridLayout_5">
<item row="4" column="2">
<widget class="QPushButton" name="nitterreset">
<property name="text">
<string>Reset</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="reusetab">
<property name="toolTip">
<string>If a web page tries to create a new tab, use the current tab instead</string>
</property>
<property name="text">
<string>Reuse tabs</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="hidetabs">
<property name="toolTip">
<string>Hide the tab bar when only one tab is open</string>
</property>
<property name="text">
<string>Auto-hide tabs</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="nitter">
<property name="toolTip">
<string>Redirect Twitter links to a Nitter instance</string>
</property>
<property name="text">
<string>Enable Nitter redirect</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="nitterurl">
<property name="placeholderText">
<string>ex. https://nitter.net</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Please don't use this. It's not fully implemented yet</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="6" column="2">
<widget class="QPushButton" name="setdefault">
<property name="toolTip">
<string>Set QtWeb as the default web browser</string>
</property>
<property name="text">
<string>Set Default</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="adblock">
<property name="toolTip">
<string>Block ads on web pages by denying hosts. Can be turned off for individual sites</string>
</property>
<property name="text">
<string>Enable ad blocker</string>
</property>
</widget>
</item>
<item row="5" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="2" column="2">
<widget class="QPushButton" name="createdownloaddir">
<property name="text">
<string>Create Dir</string>
</property>
</widget>
</item>
<item row="0" column="1" colspan="3">
<widget class="QLineEdit" name="homepage">
<property name="toolTip">
<string>Hide the tab bar when only one tab is open</string>
<string>The default web page to load in a new tab</string>
</property>
<property name="maxLength">
<number>2048</number>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QPushButton" name="selectdownloaddir">
<property name="text">
<string>Pick Directory</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="downloaddir">
<property name="toolTip">
<string>Default location for downloads</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Auto-hide tabs</string>
<string>Cache Size</string>
</property>
</widget>
</item>
@ -68,26 +196,6 @@
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Homepage</string>
</property>
</widget>
</item>
<item row="0" column="1" colspan="3">
<widget class="QLineEdit" name="homepage">
<property name="toolTip">
<string>The default web page to load in a new tab</string>
</property>
<property name="maxLength">
<number>2048</number>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
@ -95,107 +203,61 @@
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QCheckBox" name="adblock">
<property name="toolTip">
<string>Block ads on web pages. Can be turned off for individual sites</string>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Enable ad blocker</string>
<string>Homepage</string>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QCheckBox" name="darktheme">
<property name="toolTip">
<string>Set the browser to a dark theme. Does not affect web pages</string>
</property>
<property name="text">
<string>Use dark theme</string>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QPushButton" name="selectdownloaddir">
<property name="text">
<string>Pick Directory</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Cache Size</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="downloaddir">
<property name="toolTip">
<string>Default location for downloads</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QPushButton" name="createdownloaddir">
<property name="text">
<string>Create Dir</string>
</property>
</widget>
</item>
<item row="11" column="0" colspan="4">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_10">
<property name="text">
<string>Please don't use this. It's not fully implemented yet</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="setdefault">
<property name="toolTip">
<string>Set QtWeb as the default web browser</string>
</property>
<property name="text">
<string>Set Default</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="10" column="1" colspan="3">
<spacer name="verticalSpacer">
</layout>
</widget>
<widget class="QWidget" name="appearance">
<attribute name="title">
<string>Appearance</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="3">
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="themedelete">
<property name="text">
<string>Delete</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QPushButton" name="themenew">
<property name="text">
<string>New</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="themeedit">
<property name="text">
<string>Edit</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="5">
<widget class="QListWidget" name="theme">
<property name="uniformItemSizes">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="search">
@ -325,48 +387,13 @@
<string>Mastodon</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Domain</string>
</property>
</widget>
</item>
<item row="3" column="7">
<widget class="QPushButton" name="login">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item row="3" column="6">
<widget class="QPushButton" name="reset">
<property name="toolTip">
<string>Attempt to login to the account</string>
<string>Empty the form fields</string>
</property>
<property name="text">
<string>Login</string>
</property>
</widget>
</item>
<item row="5" column="7">
<widget class="QPushButton" name="logout">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Logout of the selected account</string>
</property>
<property name="text">
<string>Logout</string>
<string>Reset</string>
</property>
</widget>
</item>
@ -380,27 +407,22 @@
</property>
</widget>
</item>
<item row="2" column="1" colspan="7">
<widget class="QLineEdit" name="password">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
<item row="5" column="7">
<widget class="QPushButton" name="logout">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>E-Mail</string>
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="3" column="6">
<widget class="QPushButton" name="reset">
<property name="toolTip">
<string>Empty the form fields</string>
<string>Logout of the selected account</string>
</property>
<property name="text">
<string>Reset</string>
<string>Logout</string>
</property>
</widget>
</item>
@ -472,6 +494,9 @@
</item>
<item row="5" column="6">
<widget class="QPushButton" name="mdefault">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
@ -486,10 +511,32 @@
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_4">
<item row="3" column="7">
<widget class="QPushButton" name="login">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Attempt to login to the account</string>
</property>
<property name="text">
<string>Password</string>
<string>Login</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Domain</string>
</property>
</widget>
</item>
@ -509,8 +556,25 @@
</property>
</widget>
</item>
<item row="2" column="1" colspan="7">
<widget class="QLineEdit" name="password">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Password</string>
</property>
</widget>
</item>
<item row="5" column="5">
<widget class="QPushButton" name="refresh">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
@ -525,6 +589,13 @@
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>E-Mail</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="advanced">
@ -532,95 +603,10 @@
<string>Advanced</string>
</attribute>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="2">
<widget class="QComboBox" name="mbushost">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>125</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>The host MBus listens on</string>
</property>
<item>
<property name="text">
<string>127.0.0.1</string>
</property>
</item>
<item>
<property name="text">
<string>0.0.0.0</string>
</property>
</item>
<item>
<property name="text">
<string>False</string>
</property>
</item>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_6">
<item row="2" column="3">
<widget class="QPushButton" name="logreset">
<property name="text">
<string>MBus Host</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QSpinBox" name="mbusport">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>125</width>
<height>28</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>The port MBus listens on</string>
</property>
<property name="accelerated">
<bool>true</bool>
</property>
<property name="minimum">
<number>1024</number>
</property>
<property name="maximum">
<number>25565</number>
</property>
<property name="value">
<number>8008</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Stdout Log Level</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="jsdebug">
<property name="text">
<string>Send JS output to stdout</string>
<string>Reset</string>
</property>
</widget>
</item>
@ -664,26 +650,6 @@
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>MBus Port</string>
</property>
</widget>
</item>
<item row="6" column="1">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="3">
<widget class="QPushButton" name="portreset">
<property name="sizePolicy">
@ -697,6 +663,77 @@
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QSpinBox" name="mbusport">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>125</width>
<height>28</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>The port MBus listens on</string>
</property>
<property name="accelerated">
<bool>true</bool>
</property>
<property name="minimum">
<number>1024</number>
</property>
<property name="maximum">
<number>25565</number>
</property>
<property name="value">
<number>8008</number>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QComboBox" name="mbushost">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>125</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>The host MBus listens on</string>
</property>
<item>
<property name="text">
<string>127.0.0.1</string>
</property>
</item>
<item>
<property name="text">
<string>0.0.0.0</string>
</property>
</item>
<item>
<property name="text">
<string>False</string>
</property>
</item>
</widget>
</item>
<item row="0" column="3">
<widget class="QPushButton" name="hostreset">
<property name="sizePolicy">
@ -717,12 +754,18 @@
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QPushButton" name="logreset">
<property name="text">
<string>Reset</string>
<item row="7" column="1">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="automd5">
@ -734,6 +777,44 @@
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>MBus Host</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Stdout Log Level</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="jsdebug">
<property name="text">
<string>Send JS output to stdout</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>MBus Port</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="mastocheck">
<property name="toolTip">
<string>Query nodeinfo on previously unvisited sites to check if the domain is a mastodon instance</string>
</property>
<property name="text">
<string>Auto-detect Mastodon instances</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
@ -741,17 +822,22 @@
</layout>
</widget>
<tabstops>
<tabstop>tabWidget</tabstop>
<tabstop>tabs</tabstop>
<tabstop>homepage</tabstop>
<tabstop>cachesize</tabstop>
<tabstop>downloaddir</tabstop>
<tabstop>createdownloaddir</tabstop>
<tabstop>selectdownloaddir</tabstop>
<tabstop>hidetabs</tabstop>
<tabstop>adblock</tabstop>
<tabstop>darktheme</tabstop>
<tabstop>reusetab</tabstop>
<tabstop>nitter</tabstop>
<tabstop>nitterurl</tabstop>
<tabstop>nitterreset</tabstop>
<tabstop>themenew</tabstop>
<tabstop>themeedit</tabstop>
<tabstop>themedelete</tabstop>
<tabstop>theme</tabstop>
<tabstop>sdefault</tabstop>
<tabstop>setdefault</tabstop>
<tabstop>snew</tabstop>
<tabstop>sdelete</tabstop>
<tabstop>sengines</tabstop>
@ -772,6 +858,8 @@
<tabstop>logreset</tabstop>
<tabstop>jsdebug</tabstop>
<tabstop>tracemalloc</tabstop>
<tabstop>automd5</tabstop>
<tabstop>mastocheck</tabstop>
</tabstops>
<resources/>
<connections/>

View file

@ -0,0 +1,12 @@
// ==UserScript==
// @name FLO Sidebar Collapser
// @namespace barkshark.xyz
// @version 1.0
// @author Zoey Mae
// @description Collapse the FLO sidebar by default
// @match https://furrylife.online/*
// ==/UserScript==
if (document.getElementsByClassName('ta_wrapperSidebar')[0].offsetWidth === 300) {
document.getElementsByClassName('ta_menuToggle')[0].click()
}

View file

@ -0,0 +1,13 @@
// ==UserScript==
// @name Tab Reuser
// @namespace barkshark.xyz
// @version 1.0
// @author Zoey Mae
// @description Prevent links from opening tabs
// @match https://*
// ==/UserScript==
for (let link of document.getElementsByTagName("a")) {
if (link.target === "_blank")
link.removeAttribute("target")
}

View file

@ -0,0 +1,17 @@
// ==UserScript==
// @name SoundCloud hide reposts
// @namespace http://abs.ezw.me
// @version 1.2
// @author ABS
// @description Only see new songs in your SoundCloud stream
// @match https://soundcloud.com/stream
// ==/UserScript==
function norepost(){
for (let repost of document.getElementsByClassName("soundList__item")) {
if (repost.getElementsByClassName("sc-ministats-reposts").length > 0)
repost.remove();
// console.log(repost);
}
}
window.addEventListener("DOMNodeInserted", norepost, false);

View file

@ -1,16 +1,15 @@
import re, json
from pathlib import Path, PurePosixPath, PureWindowsPath
from pathlib import Path
from os.path import join, isfile, abspath
from urllib.parse import urlparse
from configparser import ConfigParser
from . import isLinux, isWindows
from .config import var
from . import isWindows
from .config import var, default
from .database import db, put, get, delete
from .functions import Icon, NewDesktopEntry
from .dialogs import EditBookmark
from .themes import ChangeTheme
from .Lib.IzzyLib import logging
from .Lib.IzzyLib.misc import boolean
@ -21,20 +20,21 @@ from PyQt5.QtWidgets import *
from PyQt5 import uic
class Bookmarks(object):
def __init__(self, window):
self.ui = uic.loadUi(join(var.resources, 'bookmarktab.ui'))
self.title = 'Bookmarks'
self.url = self.title
self.window = window
class Bookmarks(QWidget):
def __init__(self, parent):
super().__init__(parent)
self.ui = uic.loadUi(join(var.resources, 'tab-bookmarks.ui'))
self.setLayout(self.ui.layout())
self.window = parent
self.ui.add.clicked.connect(lambda *args: self._row_action('new'))
self.ui.edit.clicked.connect(lambda *args: self._row_action('edit'))
self.ui.open.clicked.connect(lambda *args: self._row_action('open'))
self.ui.refresh.clicked.connect(lambda *args: self._row_action('refresh'))
self.ui.table.cellDoubleClicked.connect(lambda *args: self._row_action('edit'))
self.ui.table.cellDoubleClicked.connect(lambda *args: self._row_action('open'))
for size in [(0, 150), (1, 350), (2, 80)]:
for size in [(0, 350), (1, 200), (2, 80)]:
self.ui.table.setColumnWidth(*size)
self._populate_table()
@ -98,26 +98,61 @@ class Bookmarks(object):
'maybe later'
class History(object):
def __init__(self, window):
self.title = 'History'
self.window = window
class History(QWidget):
def __init__(self, parent):
self.window = parent
class Preferences(object):
def __init__(self, window):
self.ui = uic.loadUi(join(var.resources, 'preferences.ui'))
self.title = 'Preferences'
self.window = window
def CloseTab(self):
del self.window.widgettabs.history
## I'll get this working later
class Downloads(QWidget):
def __init__(self, parent):
super().__init__(parent)
self.ui = uic.loadUi(join(var.resources, 'tab-downloads.ui'))
self.setLayout(self.ui.layout())
self.setWindowTitle('Downloads')
self.window = parent
def populate_table():
self.ui.downloads.setRowCount(0)
for download, md5 in self.window.profile.downloads:
listItem = QListWidgetItem()
dlItem = DownloadItem(self, download, md5)
self.ui.downloads.addItem(listItem)
self.ui.downloads.setItemWidget(listItem, dlItem)
def CloseTab(self):
del self.window.widgettabs.downloads
class Preferences(QWidget):
def __init__(self, parent):
super().__init__(parent)
self.ui = uic.loadUi(join(var.resources, 'tab-preferences.ui'))
self.setLayout(self.ui.layout())
self.window = parent
dldir = get.config('dlpath', var.downloadpath)
nitter_state = get.config('nitter', 0)
## General tab
self.ui.homepage.setText(get.config('homepage', 'http://ddg.gg'))
self.ui.downloaddir.setText(dldir)
self.ui.adblock.setChecked(int(get.config('adblock', 1)))
self.ui.hidetabs.setChecked(int(get.config('hidetabs', 1)))
self.ui.reusetab.setChecked(int(get.config('reusetab', False)))
self.ui.cachesize.setValue(int(get.config('cache', 100)))
self.ui.darktheme.setChecked(int(get.config('darktheme', 0)))
self.ui.nitter.setChecked(int(nitter_state))
self.ui.nitterurl.setText(get.config('nitterurl', 'https://nitter.net'))
self._general_set_nitterurl_state(nitter_state)
if isWindows:
self.ui.setdefault.setDisabled(True)
@ -142,10 +177,7 @@ class Preferences(object):
self.ui.jsdebug.setChecked(get.config('jsdebug', False))
self.ui.tracemalloc.setChecked(get.config('tracestart', False))
self.ui.automd5.setChecked(get.config('automd5', False))
# this breaks for some reason, so keeping it disabled until I figure out what's going on
self.ui.hostreset.setToolTip('Button broken. Just set the host to "127.0.0.1"')
self.ui.hostreset.setDisabled(True)
self.ui.mastocheck.setChecked(get.config('mastocheck', False))
## General tab signals
self.ui.setdefault.clicked.connect(self._general_set_default)
@ -157,7 +189,13 @@ class Preferences(object):
self.ui.cachesize.editingFinished.connect(self._general_update_cachesize)
self.ui.hidetabs.stateChanged.connect(lambda state: self._general_handle_checkbox(state, 'hidetabs'))
self.ui.adblock.stateChanged.connect(lambda state: self._general_handle_checkbox(state, 'adblock'))
self.ui.darktheme.stateChanged.connect(lambda state: self._general_handle_checkbox(state, 'darktheme'))
self.ui.nitter.stateChanged.connect(lambda state: self._general_handle_checkbox(state, 'nitter'))
self.ui.reusetab.stateChanged.connect(lambda state: self._general_handle_checkbox(state, 'reusetab'))
self.ui.nitterurl.editingFinished.connect(self._general_update_nitter_url)
self.ui.nitterreset.clicked.connect(self._general_reset_nitter_url)
## Appearance tab signals
#self.ui.theme.itemClicked.connect(self._appearance_theme_change)
## Search engine tab signals
self.ui.snew.clicked.connect(lambda *args: self._search_button_handler('new'))
@ -175,21 +213,28 @@ class Preferences(object):
self.ui.domain.returnPressed.connect(lambda *args: self.ui.username.setFocus())
self.ui.username.returnPressed.connect(lambda *args: self.ui.password.setFocus())
self.ui.password.returnPressed.connect(self.mastodon_login)
self.ui.accounts.itemSelectionChanged.connect(self.mastodon_row_selected)
## Advanced tab signals
self.ui.mbushost.currentIndexChanged.connect(self._adv_mbushost_changed)
self.ui.mbusport.editingFinished.connect(self._adv_mbusport_changed)
self.ui.loglevel.currentIndexChanged.connect(lambda index: self._adv_changed('loglevel', index))
self.ui.hostreset.clicked.connect(lambda *args: self._adv_reset('mhost'))
self.ui.portreset.clicked.connect(lambda *args: self._adv_reset('mport'))
self.ui.hostreset.clicked.connect(lambda *args: self._adv_mbushost_changed(int(default.mhost)))
self.ui.portreset.clicked.connect(lambda *args: self._adv_mbusport_changed(default.mport))
self.ui.logreset.clicked.connect(lambda *args: self._adv_reset('loglevel'))
self.ui.jsdebug.stateChanged.connect(lambda state: self._adv_handle_checkbox('jsdebug', state))
self.ui.tracemalloc.stateChanged.connect(lambda state: self._adv_handle_checkbox('tracestart', state))
self.ui.automd5.stateChanged.connect(lambda state: self._adv_handle_checkbox('automd5', state))
self.ui.mastocheck.stateChanged.connect(lambda state: self._adv_handle_checkbox('mastocheck', state))
## Populate tables
self.mastodon_populate_table()
self.populate_search_table()
#self._appearance_populate_view()
# Remove the appearance tab until I can get it fixed
self.ui.tabs.widget(1).deleteLater()
self.ui.tabs.removeTab(1)
### General Tab ###
@ -293,10 +338,6 @@ class Preferences(object):
self.window.profile.setHttpCacheMaximumSize(1024 * 1024 * value)
def _general_homepage_reset(self, *args):
self.ui.homepage.setText(self.window.homepage)
def _general_update_homepage(self, *args):
newurl = self.ui.homepage.text()
@ -314,21 +355,81 @@ class Preferences(object):
if name == 'hidetabs':
self.window.tabbar.setAutoHide(state.real)
if name == 'adblock':
self.window.adblock = value
if name == 'darktheme':
if name == 'nitter':
value = boolean(value)
self._general_set_nitterurl_state(value)
if value:
ChangeTheme('dark')
else:
ChangeTheme()
def _general_set_nitterurl_state(self, value=None):
if not value:
value = get.config('nitter', False)
self.ui.nitterurl.setEnabled(value)
self.ui.nitterreset.setEnabled(value)
def _general_update_nitter_url(self):
url = self.ui.nitterurl.text()
if not any(map(url.startswith, ['http://', 'https://'])):
url = 'https://' + url
self.ui.nitterurl.setText(url)
if not put.config('nitterurl', url):
logging.error('Failed to update "nitterurl" config')
def _general_reset_nitter_url(self):
if not put.config('nitterurl', default.nitter):
logging.error('Failed to update "nitterurl" config')
return
self.ui.nitterurl.setText(default.nitter)
### Appearance Tab ###
def _appearance_populate_view(self):
self.ui.theme.clear()
size = QSize()
size.setHeight(24)
default = QListWidgetItem('Default')
default.setSizeHint(size)
if get.config('theme', 'Default') == 'Default':
default.setIcon(Icon('okay', 16))
self.ui.theme.addItem(default)
for theme in get.theme():
item = QListWidgetItem(theme.name)
item.setForeground(theme.palette.text())
item.setBackground(theme.palette.base())
item.setSizeHint(size)
if get.config('theme') == theme.name:
item.setIcon(Icon('okay', 16))
self.ui.theme.addItem(item)
def _appearance_theme_change(self, item):
listwidget = self.ui.theme
theme = item.text()
if put.config('theme', theme):
items = [listwidget.item(i) for i in range(listwidget.count())]
for item in items:
if item.text() != theme:
item.setIcon(QIcon())
else:
item.setIcon(Icon('okay', 16))
self.window.themes.ChangeTheme(theme)
### Search Tab ###
# todo: prefix functions with 'search'
def _search_button_handler(self, button):
table = self.ui.sengines
tablerow = table.currentRow()
@ -368,7 +469,6 @@ class Preferences(object):
return
table = self.ui.sengines
cell = table.item(x, y)
cells = [0, table.item(x, 1), table.item(x, 2), table.item(x, 3)]
data = {
@ -429,6 +529,19 @@ class Preferences(object):
logging.error('Invalid message type:', msgtype)
def mastodon_row_selected(self):
row = self.mastodon_selected_row_data()
logging.debug(row)
if not row:
self.ui.mdefault.setEnabled(False)
self.ui.logout.setEnabled(False)
else:
self.ui.mdefault.setEnabled(row['default'])
self.ui.logout.setEnabled(True)
def mastodon_selected_row_data(self):
table = self.ui.accounts
tablerow = table.currentRow() if table.currentRow() >= 0 else None
@ -436,9 +549,10 @@ class Preferences(object):
if tablerow == None:
return
cells = [0, table.item(tablerow, 1), table.item(tablerow, 2), table.item(tablerow, 3)]
cells = [table.item(tablerow, 0), table.item(tablerow, 1), table.item(tablerow, 2), table.item(tablerow, 3)]
data = {
'default': cells[0].icon().isNull(),
'displayname': cells[1].text() if cells[1] else None,
'username': cells[2].text() if cells[2] else None,
'domain': cells[3].text() if cells[3] else None
@ -450,9 +564,13 @@ class Preferences(object):
def mastodon_populate_table(self):
self.ui.accounts.setRowCount(0)
accounts = get.mastoaccount()
for widget in [self.ui.mdefault, self.ui.logout]:
widget.setEnabled(False)
#for row in db.get('mastodon', single=False):
for row in get.mastoaccount():
for row in accounts:
self.mastodon_insert_row(row)
@ -534,7 +652,7 @@ class Preferences(object):
## Advanced tab
def _adv_mbushost_changed(self, index):
new = self.ui.mbushost.itemText(index)
old = get.config('mhost')
old = get.config('mhost', default.mhost)
if old != new:
put.config('mhost', new)
@ -546,8 +664,10 @@ class Preferences(object):
self.window.server.restart()
def _adv_mbusport_changed(self):
value = self.ui.mbusport.value()
def _adv_mbusport_changed(self, value=None):
if not value:
value = self.ui.mbusport.value()
put.config('mport', value)
self.window.server.restart()
@ -574,20 +694,44 @@ class Preferences(object):
db.remove('config', row.id)
if field == 'mhost':
self.ui.mbushost.setCurrentIndex(0)
if field == 'mport':
self.ui.mbusport.setValue(8008)
if field in ['mhost', 'mport']:
self.window.server.restart()
if field == 'loglevel':
self.mbushost.setCurrentIndex(3)
logging.setConfig({'level': 'INFO'})
def CloseTab(self):
del self.window.widgettabs.preferences
class DownloadItem(QWidget):
def __init__(self, tab, download, md5=None):
self.ui = uic.loadUi(join(var.resources, 'download-item.ui'))
self.setLayout(self.ui.layout())
self.tab = tab
self.finished = False
location = Path(download.downloadDirectory()).joinpath(download.downloadFileName())
url = download.url().toString()
self.ui.location.setText(str(location))
self.ui.url.setText(url)
download.downloadProgress.connect(self._update_progress)
def _update_progress(self, current, total):
precent = current / total * 100
current_mb = current / (1024 ** 2)
total_mb = total / (1024 ** 2)
for value in [percent, current_mb, total_mb]:
if type(value) == float:
value = round(value, 2)
progress = f'{percent}% - {current_mb}MiB/{total_mb}MiB'
self.ui.percent.setText(progress)
class RefreshAccounts(QRunnable):
def __init__(self, *args, tab=None, **kwargs):
super().__init__(*args, **kwargs)

View file

@ -1,111 +1,74 @@
from .Lib.IzzyLib import logging
from . import isWindows
from .database import get, put
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import QStyleFactory
from PyQt5.QtWidgets import QStyleFactory, QApplication
color_themes = {}
default_style = None
current_theme = None
app = None
class ColorTheme(object):
def __init__(self, window):
self.app = window.app
self.window = window
self.default_style = self.app.style().objectName().lower()
self.default_palette = self.app.style().standardPalette()
self.appcolor = self.app.palette()
self.colors = [
'Window', 'WindowText', 'Base', 'AlternateBase', 'ToolTipBase', 'ToolTipText',
'Button', 'ButtonText', 'Text', 'Highlight', 'HighlightedText', 'Link',
'Light', 'Midlight', 'Dark', 'Mid', 'Shadow', 'LinkVisited', 'BrightText'
]
self.colors.sort()
logging.verbose('Default style:', self.default_style)
logging.verbose('Available styles:', ', '.join(QStyleFactory.keys()))
dark = {
'AlternateBase': QColor(53, 53, 53),
'Base': QColor(25, 25, 25),
'Button': QColor(53, 53, 53),
'ButtonText': Qt.white,
'BrightText': Qt.red,
'Highlight': QColor(42, 130, 218),
'HighlightedText': Qt.black,
'Link': QColor(42, 130, 218),
'Text': Qt.white,
'ToolTipBase': Qt.black,
'ToolTipText': Qt.white,
'Window': QColor(53, 53, 53),
'WindowText': Qt.white,
}
def ChangeTheme(self, theme=None):
theme = 'Default' if not theme else theme
if theme == 'Default':
palette = self.default_palette
def AddTheme(name, colors):
name = name.title()
theme = QPalette()
for colname, color in colors.items():
theme.setColor(getattr(QPalette, colname), QColor(color) if type(color) == str else color)
color_themes[name] = theme
def RemoveTheme(name):
name = name.title()
if not color_themes.get(name):
logging.error('Not a valid theme:', name)
return
del color_themes[name]
def GetTheme(name=None):
if not name:
return color_themes
name = name.title()
theme = color_themes.get(name)
if not theme:
logging.error('Not a valid theme:', name)
return
return theme
def ChangeTheme(theme=None):
global current_theme
theme = 'Default' if not theme else theme.title()
newtheme = color_themes.get(theme)
if not newtheme:
logging.error('Not a valid theme:', theme)
return
if isWindows:
if theme != 'Default':
app.setStyle('fusion')
else:
app.setStyle(default_style)
row = get.theme(theme)
logging.verbose('Changing color theme to', theme)
if not row:
logging.error('Not a valid theme:', theme)
return
current_theme = theme
app.setPalette(newtheme)
palette = row.palette
logging.verbose('Changing color theme to', theme)
put.config('theme', theme)
self.app.setPalette(palette)
if isWindows:
if theme != 'Default':
self.app.setStyle('fusion')
else:
self.app.setStyle(self.default_style)
return
def SetupThemes(appclass):
global default_style
global app
def ColorToPalette(self, data):
palette = self.app.style().standardPalette()
app = appclass
default_style = app.style().objectName().lower()
appcolor = app.palette()
for name, color in data.items():
if name in self.colors and color != None:
palette.setColor(getattr(QPalette, name), QColor(color))
colors = {}
defcolors = [
'Window', 'WindowText', 'Base', 'AlternateBase', 'ToolTipBase', 'ToolTipText', 'Button', 'ButtonText',
'BrightText', 'Light', 'Midlight', 'Dark', 'Mid', 'Shadow', 'Highlight', 'HighlightedText', 'Link', 'LinkVisited'
]
return palette
for color in defcolors:
lowercolor = color[0].lower() + color[1:]
colors[color] = getattr(appcolor, lowercolor)().color()
AddTheme('Default', colors)
AddTheme('dark', dark)
def PaletteToColor(self, palette):
data = {}
logging.verbose('Default style:', default_style)
logging.verbose('Available styles:', ', '.join(QStyleFactory.keys()))
logging.verbose('Available themes:', ', '.join(color_themes.keys()))
for color in self.colors:
data[color] = getattr(palette, color[0].lower() + ''.join(color[1:]))().color().name()
return data

View file

@ -1,49 +1,71 @@
import sys
from . import debstable
from .config import var
from .database import db, put
from .Lib.IzzyLib import logging
from PyQt5.QtCore import *
from PyQt5.QtWebEngineCore import *
def RegisterScheme(name):
scheme = QWebEngineUrlScheme(name.encode())
scheme.setFlags(QWebEngineUrlScheme.SecureScheme)
QWebEngineUrlScheme.registerScheme(scheme)
scheme = QWebEngineUrlScheme(name.encode())
scheme.setFlags(
#QWebEngineUrlScheme.LocalScheme |
QWebEngineUrlScheme.LocalAccessAllowed |
QWebEngineUrlScheme.ViewSourceAllowed
)
QWebEngineUrlScheme.registerScheme(scheme)
class LocalHandler(QWebEngineUrlSchemeHandler):
def __init__(self, webview, window):
super().__init__(window)
self.webview = webview
def __init__(self, parent):
self.window = parent
def requestStarted(self, job):
url = job.requestUrl()
host = url.host()
if not debstable:
scheme = QWebEngineUrlScheme.schemeByName(var.local.encode())
if scheme.name() == b'':
logging.critical(f'Failed to register scheme: {var.local}. Exiting...')
parent.close()
super().__init__(parent.profile)
parent.profile.installUrlSchemeHandler(var.local.encode(), self)
def _check_initiator(self, request):
initiator = request.initiator()
requrl = request.requestUrl()
if requrl.scheme() != var.local:
logging.warning(f'Blocking malicious request from {initiator.toDisplayString()} to {request_url.toDisplayString()}')
request.fail(QWebEngineUrlRequestJob.RequestDenied)
return False
return True
def requestStarted(self, request):
url = request.requestUrl()
path = url.path()
job.fail(QWebEngineUrlRequestJob.UrlNotFound)
if not self._check_initiator(request):
return
if url.host() == 'home':
mimetype = 'text/html'
data = b'<html><head><title>MERP!</title><body>UvU</body></html>'
blockedDomain = [
'doubleclick.net',
'rubiconproject.com',
'outbrain.com',
'googletagservices.com',
'amazon-adsystem.com',
'optimizely.com',
'adswizz.com'
]
else:
mimetype = 'text/html'
data = '<html><head><title>QtWeb</title><body>heck</body></html>'
blockedURL = {
'youtube.com': [
'/api/stats/ads',
'/pagead/adview',
'/pagead/lvz'
'/get_midroll/info'
],
'soundcloud.com': [
'/audio-ads'
],
'facebook.com': [
'/tr']
}
buf = QBuffer(parent=self)
buf.open(QIODevice.WriteOnly)
buf.write(data)
buf.seek(0)
buf.close()
request.reply(mimetype.encode('ascii'), buf)

View file

@ -1,4 +1,4 @@
import threading, sys, os, time
import threading, sys, os
from datetime import datetime
from os.path import join
@ -6,16 +6,16 @@ from pathlib import Path
from .Lib.IzzyLib import logging
from . import pyqtver, debstable, isWindows
from . import __application__, __version__, pyqtver, debstable, isWindows
from . import dialogs
from .config import var
from .database import db, get, put, delete
from .functions import useragent, GetSoftware, httpclient, Decode, psl, DateTime, MD5Check
from .functions import useragent, GetSoftware, httpclient, Decode, psl, MD5Check, WebEngineScript, DotDict
from PyQt5.QtWebEngineWidgets import *
from PyQt5.QtCore import QUrl, QDateTime
from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInterceptor
from PyQt5.QtNetwork import QNetworkCookie, QNetworkCookieJar
from PyQt5.QtNetwork import QNetworkCookie
AddHeader = lambda info, k, v: info.setHttpHeader(k.encode(), v.encode())
@ -26,7 +26,8 @@ blockedDomain = [
'googletagservices.com',
'amazon-adsystem.com',
'optimizely.com',
'adswizz.com'
'adswizz.com',
'scorecardsearch.com'
]
blockedURL = {
@ -37,21 +38,24 @@ blockedURL = {
'/get_midroll/info'
],
'soundcloud.com': [
'/audio-ads'
'/audio-ads',
'/me'
],
'facebook.com': [
'/tr']
'/tr',
'/brandlift.php'
]
}
class WebEngineProfile(QWebEngineProfile):
def __init__(self, parent):
super().__init__(var.profile)
super().__init__(var.profile, parent)
parent.profile = self
self.interceptor = WebFilter(parent)
self.window = parent
self.notif = parent.notif
self.user_scripts = []
self.cookies = []
self.downloads = {}
@ -61,7 +65,7 @@ class WebEngineProfile(QWebEngineProfile):
self.setCachePath(var.profilepath)
self.setHttpCacheMaximumSize(int(get.config('cache', 100))*1024*1024)
self.setPersistentStoragePath(var.profilepath)
self.setHttpUserAgent(useragent)
self.setHttpUserAgent(f'{self.httpUserAgent()} {__application__}/{__version__}')
self.setSpellCheckEnabled(True)
self.setSpellCheckLanguages(['en-US'])
@ -87,6 +91,8 @@ class WebEngineProfile(QWebEngineProfile):
for k, v in self.attrs.items():
self.settings().setAttribute(k, v)
self.LoadAllScripts()
# Will enable this once I figure out how to properly handle cookies
#self.setPersistentCookiesPolicy(QWebEngineProfile.NoPersistentCookies)
#self.LoadCookies()
@ -114,35 +120,33 @@ class WebEngineProfile(QWebEngineProfile):
def _download_request(self, download):
self.downloads[download.id()] = [download, None]
dldialog = dialogs.NewDownload(self.downloads[download.id()], self)
self.downloads[download.id()] = DotDict({'item': download, 'md5': None})
dldialog = dialogs.NewDownload(self, self.downloads[download.id()])
dldialog.exec_()
def _download_finished(self, request):
download, md5 = request
def _download_finished(self, download):
if debstable:
filename = Path(download.path())
else:
filename = Path(join(download.downloadDirectory(), download.downloadFileName()))
filename = Path(download.item.downloadDirectory(), download.item.downloadFileName())
url = download.url().toString()
url = download.item.url().toString()
if md5 and filename.exists() and MD5Check(filename, md5) == False:
if download.md5 and filename.exists() and MD5Check(filename, download.md5) == False:
logging.debug(f'Download failed md5 check: {filename}')
self.window.notif.New('Failed md5 check', url)
self.window.notif.New('Download failed md5 check', filename)
filename.unlink()
elif download.state() == QWebEngineDownloadItem.DownloadCompleted:
elif download.item.state() == QWebEngineDownloadItem.DownloadCompleted:
logging.debug(f'Finished downloading {url} to {filename}')
self.window.notif.New('Finished Download', str(filename))
#if download.state() == QWebEngineDownloadItem.DownloadCancelled:
#if download.item.state() == QWebEngineDownloadItem.DownloadCancelled:
#self.notif.New('Cancelled Download', filename)
del self.downloads[download.id()]
del self.downloads[download.item.id()]
def _cookie_filter(self, request):
@ -154,13 +158,42 @@ class WebEngineProfile(QWebEngineProfile):
return True
def _new_cookie(self, cookie):
if not cookie.isSessionCookie():
put.cookie(cookie)
def GetScript(self, index):
return self.user_scripts[index]
def _rem_cookie(self, cookie):
delete.cookie(cookie)
def LoadScript(self, filename):
script = Path(filename)
webscript = WebEngineScript(self.scripts(), script, script.match(f'{var.scriptpath}/*'))
webscript.enable()
self.user_scripts.append(webscript)
def UnloadScript(self, index):
script = index if type(index) == WebEngineScript else self.user_scripts[index]
return script.disable()
def DeleteSCript(self, index):
script = index if type(index) == WebEngineScript else self.user_scripts[index]
if not script.user:
logging.error('Refusing to unload a system script')
script.disable()
script.filename.unlink()
self.user_scripts.remove(script)
def LoadAllScripts(self):
scripts = [s for s in Path(var.scriptpath, 'scripts').rglob('*.sys.js')]
scripts += [s for s in Path(var.profilepath, 'Scripts').rglob('*.user.js')]
for script in scripts:
self.LoadScript(script)
def LoadCookies(self):
@ -197,30 +230,28 @@ class WebFilter(QWebEngineUrlRequestInterceptor):
return
## Block requests if they're in the block list
if get.config('adblock') and pagerow.adblock and (reqpath in blockedURL.get(reqdomain, []) or reqdomain in blockedDomain or reqtopdomain in blockedDomain):
if get.config('adblock') and pagerow.adblock and (reqpath in blockedURL.get(reqtopdomain, []) or reqtopdomain in blockedDomain or reqdomain in blockedDomain):
logging.verbose('Blocked content:', requrl.toString().split('?', 1)[0])
info.block(True)
return
## Check if domain is a mastodon instance
if not pagerow.domain and not pagerow.mastodon and not any(map(requrl.toString().startswith, ['about:', 'chrome://'])):
if not pagedomain:
return
if get.config('mastocheck', False):
if not pagerow.domain and not pagerow.mastodon and not any(map(requrl.toString().startswith, ['about:', 'chrome://'])):
if not pagedomain:
return
if self.threads.get(pagedomain):
return
if self.threads.get(pagedomain):
return
self.threads[pagedomain] = threading.Thread(target=GetSoftware, args=[pagedomain])
self.threads[pagedomain].start()
logging.verbose('Checking for presence of nodeinfo')
self.threads[pagedomain] = threading.Thread(target=GetSoftware, args=[pagedomain])
self.threads[pagedomain].start()
## Delete the thread object once it's finished
elif self.threads.get(pagedomain):
del self.threads[pagedomain]
## Delete the thread object once it's finished
elif self.threads.get(pagedomain):
del self.threads[pagedomain]
## Set custom headers
AddHeader(info, 'Trans-Rights', 'Are Human Rights')
del pagerow
## Google is a fuck and is very picky about what browsers are supported
#if 'google.com' in [pagetopdomain, reqtopdomain]:
#info.setHttpHeader('user-agent'.encode(), 'Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0'.encode())
# This breaks gmx.com
#AddHeader(info, 'X-Trans-Rights', 'Are Human Rights')

View file

@ -5,12 +5,13 @@ import validators
from .Lib.IzzyLib import logging
from .Lib.IzzyLib.misc import boolean
from .Lib.IzzyLib.cache import LRUCache
from . import debstable, dialogs
from .config import var
from . import debstable, dialogs, debstable
from .config import var, default
from .database import db, get, put
from .mastodon import Mastodon
from .functions import Menu, Icon
from .functions import Menu, Icon, httpclient
from .webviewjs import JsFuncs
from PyQt5.QtCore import *
@ -40,6 +41,7 @@ class WebEngineView(QWebEngineView):
super().__init__()
self.window = window
self.jscache = LRUCache(64)
self.webview = True
self.tabs = window.ui.tabs
self.webpage = WebEnginePage(self.window.profile, self)
@ -54,15 +56,17 @@ class WebEngineView(QWebEngineView):
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._web_context_menu)
#self.loadStarted.connect(lambda *args: setattr(self, 'loading', True))
self.loadProgress.connect(self._progress_loading)
#self.loadStarted.connect(self._started_loading)
self.loadFinished.connect(self._finished_loading)
self.webpage.titleChanged.connect(self._set_tab_text)
self.webpage.iconChanged.connect(self._set_tab_icon)
self.webpage.urlChanged.connect(self._set_tab_url)
self.webpage.linkHovered.connect(self._link_hover)
self.webpage.fullScreenRequested.connect(lambda *args: self._permission_request('fullscreen', *args))
self.webpage.featurePermissionRequested.connect(self._feature_request)
self.webpage.fullScreenRequested.connect(self._fullscreen_request)
self.webpage.featurePermissionRequested.connect(self._permission_request)
self.webpage.authenticationRequired.connect(self._handle_auth)
self.webaction_state = {
'previous': False,
@ -79,15 +83,41 @@ class WebEngineView(QWebEngineView):
self.JsAction = JsFuncs(self)
if url != 'newtab':
self.LoadUrl(url if url else get.config('homepage', 'https://ddg.gg'))
self.LoadUrl(url if url else get.config('homepage', default.homepage))
def _progress_loading(self, progress):
current_index = self.window.ui.tabs.currentIndex()
tab_index = self.window.ui.tabs.indexOf(self)
loading = progress != 100
percent = progress / 100
self.webaction_state['stop'] = loading
self.webaction_state['reload'] = not loading
if current_index == tab_index:
#if loading:
#self.window.ui.url.setStyleSheet('background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop: 0 #1bb77b, stop: ' + str(percent) + ' #1bb77b, stop: ' + str(percent+ 0.001) + ' rgba(0, 0, 0, 0), stop: 1 white)')
#else:
#self.window.ui.url.setStyleSheet(None)
self.window._update_webaction_buttons('stop', self.webaction_state['stop'])
self.window._update_webaction_buttons('reload', self.webaction_state['reload'])
def _finished_loading(self, arg):
jsfile = Path(var.scriptpath, 'web', 'js', self.url().host() + '.js')
host = self.url().host()
jsfile = Path(var.scriptpath, 'scripts', host + '.js')
jscache = self.jscache.fetch(host)
if jsfile.exists():
with jsfile.open('r') as fd:
self.webpage.runJavaScript(fd.read())
if jsfile.is_file():
if not jscache:
with jsfile.open('r') as fd:
data = fd.read()
self.jscache.store(host, data)
jscache = data
self.webpage.runJavaScript(jscache)
def _set_tab_text(self, title):
@ -121,13 +151,16 @@ class WebEngineView(QWebEngineView):
action = recv_action.text().lower()
state = recv_action.isEnabled()
if action in ['stop', 'reload']:
return
self.webaction_state[action] = state
if current_index == tab_index:
self.window._update_webaction_buttons(action, state)
def _feature_request(self, origin, feature):
def _permission_request(self, origin, feature):
host = origin.host()
site = get.permissions(host)
@ -154,28 +187,43 @@ class WebEngineView(QWebEngineView):
logging.verbose(f'Rejected permission for {host}:', name)
def _permission_request(self, action, request):
host = self.url().host()
site = get.permissions(host)
permissions = {
'fullscreen': (site.fullscreen, 'Fullscreen')
}
def _fullscreen_request(self, request):
host = request.origin().host()
site = get.permissions(host, defaults=True)
permission, name = permissions.get(action, (None, 'Invalid permission'))
if action == 'fullscreen':
if site.fullscreen:
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()
login = dialogs.HttpAuth(location, authenticator, self)
login.exec_()
if login.result:
logging.verbose('Authenticated page:', url)
else:
logging.verbose('Canceled authentication for page:', url)
self.stop()
if self.window.ui.tabs.currentWidget() == self:
self.window.ui.url.setText(url)
def _web_context_menu(self, point):
cursor = QCursor().pos() #point puts the menu in the wrong location for some reason
cursor = QCursor().pos()
data = self.webpage.contextMenuData()
clipboard = self.window.clipboard
mimedata = clipboard.mimeData()
currenturl = self.url()
spliturl = currenturl.path().split('/')
selected = data.selectedText()
search = get.search(get.config('search', 'ddg'))
urlismasto = get.permissions(currenturl.host(), defaults=True).mastodon
urlispost = (len(spliturl) == 3 and spliturl[1].startswith('@'))
@ -188,16 +236,7 @@ class WebEngineView(QWebEngineView):
acctcount = db.count('mastodon') > 0
mediaurl = data.mediaUrl().toString()
menu = Menu('heck', self)
addItem = lambda label, name, data=None: menu.AddAction(label, lambda : self._menu_actions(name, data))
addMastoItem = lambda label, name, data=None: menu.AddAction(label, lambda : self._mastodon_action(name, data))
if mediaurl != '':
addItem('Open Media in New Tab', 'newtab', mediaurl)
addItem('Open Media in New Tab and Switch', 'newtabswitch', mediaurl)
menu.AddSeparator()
addItem('Download Media', 'download', mediaurl)
addItem('Copy Media URL', 'copy', mediaurl)
menu = Menu(self)
## Not sure if I wanna enable this
#if acctcount and urlismasto and urlispost:
@ -210,47 +249,69 @@ class WebEngineView(QWebEngineView):
linkurl = data.selectedText()
if linkurl != '':
addItem('Open Link in New Tab', 'newtab', linkurl)
addItem('Open Link in New Tab and Switch', 'newtabswitch', linkurl)
menu.AddItem('Open Link in New Tab', self._menu_actions, 'newtab', linkurl, icon=Icon('newtab'))
menu.AddItem('Open Link in New Tab and Switch', self._menu_actions, 'newtabswitch', linkurl, icon=Icon('newtab'))
menu.AddSeparator()
addItem('Download Link', 'download', linkurl)
menu.AddSeparator()
if acctcount and linkispost and linkismasto:
addMastoItem('Reply to Linked Post', 'reply', linkurl)
addMastoItem('Favorite Linked Post', 'favourite', linkurl)
addMastoItem('Boost Linked Post', 'reblog', linkurl)
addMastoItem('Bookmark Linked Post', 'bookmark', linkurl)
menu.AddSeparator()
addItem('Copy Link URL', 'copyurl')
addItem('Copy Link Text', 'copy', linktext)
menu.AddItem('Copy Link Location', self._menu_actions, 'copyurl')
menu.AddItem('Copy Link Text', self._menu_actions, 'copy', linktext)
if selected:
if data.CanCut:
addItem('Cut', 'cut', selected)
menu.AddItem('Cut', self._menu_actions, 'cut', selected)
if data.CanCopy:
addItem('Copy', 'copy', selected)
menu.AddItem('Copy', self._menu_actions, 'copy', selected)
if data.isContentEditable():
if data.CanPaste:
addItem('Paste', 'paste', clipboard.text('plain'))
menu.AddItem('Paste', self._menu_actions, 'paste', clipboard.text('plain'))
if data.CanUndo:
menu.AddSeparator()
addItem('Undo', 'undo')
menu.AddItem('Undo', self._menu_actions, 'undo')
if data.CanRedo:
addItem('Redo', 'Redo')
menu.AddItem('Redo', self._menu_actions, 'Redo')
if linkurl != '':
menu.AddItem('Download Link', self._menu_actions, 'download', linkurl)
if acctcount and linkispost and linkismasto:
menu.AddSeparator()
menu.AddItem('Reply to Linked Post', self._mastodon_action, 'reply', linkurl)
menu.AddItem('Favorite Linked Post', self._mastodon_action, 'favourite', linkurl)
menu.AddItem('Boost Linked Post', self._mastodon_action, 'reblog', linkurl)
menu.AddItem('Bookmark Linked Post', self._mastodon_action, 'bookmark', linkurl)
#menu.AddItem('Add Domain Block', self._mastodon_action, 'domainblock', data.linkUrl().host())
if menu.MoreThan(0):
menu.AddSeparator()
addItem('Inspect Page', 'inspect_page')
addItem('Inspect Element', 'inspect')
if selected and search:
menu.AddSeparator()
menu.AddItem(f'Search with {search.name}', self._menu_actions, 'newtabswitch', search.url.format(q=selected))
searchmenu = Menu(self, 'Search with...')
for row in db.fetch('search', single=False):
if row.keyword != search.keyword:
searchmenu.AddItem(row.name, self._menu_actions, 'newtabswitch', row.url.format(q=selected))
menu.addMenu(searchmenu)
menu.AddSeparator()
if mediaurl != '':
menu.AddItem('Open Media in New Tab', self._menu_actions, 'newtab', mediaurl, icon=Icon('newtab'))
menu.AddItem('Open Media in New Tab and Switch', self._menu_actions, 'newtabswitch', mediaurl, icon=Icon('newtab'))
menu.AddSeparator()
menu.AddItem('Copy Media Location', self._menu_actions, 'copy', mediaurl)
menu.AddItem('Download Media', self._menu_actions, 'download', mediaurl)
menu.AddSeparator()
menu.AddItem('Inspect Page', self._menu_actions, 'inspect_page')
menu.AddItem('Inspect Element', self._menu_actions, 'inspect')
menu.AddSeparator()
addItem('View Page Source', 'source')
menu.AddItem('View Page Source', self._menu_actions, 'source')
menu.aboutToHide.connect(menu.deleteLater) #not sure if needed
menu.popup(cursor)
@ -285,6 +346,9 @@ class WebEngineView(QWebEngineView):
def _mastodon_action(self, action, url):
if action == 'domainblock':
account = get.mastoaccount(default=True)
self.window.NewWebTab(f'https://{account.domain}/admin/domain_blocks/new?_domain={url}')
if action == 'reply':
dialogs.Toot(self.window, self.window.mastodon, reply=url).show()
@ -306,6 +370,11 @@ class WebEngineView(QWebEngineView):
logging.debug('js:', message)
## Does nothing right now, but will be used later
def CloseTab(self):
pass
def Fullscreen(self, gofull):
fullscreen = self.window.FullscreenState()
@ -390,10 +459,12 @@ class WebEngineView(QWebEngineView):
twitter_paths = any(map(parsed.path.startswith, ['/settings', '/home', '/login', '/logout', '/explore', '/sessions', '/account']))
twitter_files = any(map(parsed.path.endswith, ['.js', '.jpeg', '.jpg', '.png', '.css']))
if parsed.netloc == 'twitter.com' and get.config('twiredir', True) and parsed.path != '/' and not twitter_paths and not twitter_files:
newurl = f'https://nitter.net{parsed.path}'
if parsed.netloc == 'twitter.com' and get.config('nitter', False) and parsed.path != '/' and not twitter_paths and not twitter_files:
nitterurl = get.config('nitterurl', default.nitter)
newurl = nitterurl + parsed.path
self.webpage.setUrl(QUrl(newurl))
self.setFocus()
class SearchBar(QFrame):
@ -403,7 +474,7 @@ class SearchBar(QFrame):
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
layout = QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 2)
layout.setContentsMargins(0, 6, 0, 0)
layout.setSpacing(4)
#layout.setSizeConstraint(QLayout.SetFixedSize)
self.setLayout(layout)
@ -418,10 +489,6 @@ class SearchBar(QFrame):
close.setIcon(Icon('close'))
for widget in [previous, next, self.search, self.sensitive, reset, close]:
#if widget == self.search:
#widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum)
#else:
#widget.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
self.layout().addWidget(widget)
self.search.installEventFilter(FindEsc(self))
@ -434,17 +501,18 @@ class SearchBar(QFrame):
close.clicked.connect(lambda *args: self.Search('toggle'))
def Search(self, action=None):
def Search(self, action=None, text=None):
if text:
self.search.setText(text)
text = self.search.text()
webview = self.webview
selected = webview.selectedText()
selected = self.webview.selectedText()
sensitive = self.sensitive.isChecked()
if action == 'previous':
webview.Search(text, reverse=True, sensitive=sensitive)
self.webview.Search(text, reverse=True, sensitive=sensitive)
elif action == 'reset':
webview.Search()
self.webview.Search()
self.sensitive.setChecked(False)
self.search.setText('')
@ -461,12 +529,12 @@ class SearchBar(QFrame):
self.Search()
else:
## Reset the search and focus the webview
## Reset the search and focus the window
self.Search('reset')
webview.setFocus()
self.webview.setFocus()
else:
webview.Search(text, sensitive=sensitive)
self.webview.Search(text, sensitive=sensitive)
class FindEsc(QObject):
@ -474,7 +542,6 @@ class FindEsc(QObject):
super().__init__(parent)
self.webview = parent
def eventFilter(self, obj, event):
if event.type() == QEvent.KeyPress and event.key() == 16777216:
self.webview.Search('toggle')
@ -512,12 +579,14 @@ class WebEnginePage(QWebEnginePage):
def createWindow(self, wintype):
options = {'nexttab': True}
#wintypes = [
#QWebEnginePage.WebBrowserTab,
#QWebEnginePage.WebBrowserWindow,
#QWebEnginePage.WebBrowserBackgroundTab,
#QWebEnginePage.WebDialog
#]
wintypes = {
QWebEnginePage.WebBrowserTab: 'new tab',
QWebEnginePage.WebBrowserWindow: 'new window',
QWebEnginePage.WebBrowserBackgroundTab: 'new background tab',
QWebEnginePage.WebDialog: 'new dialog'
}
typename = wintypes.get(wintype, f'Invalid window type: {type(wintype)}')
if wintype != QWebEnginePage.WebDialog:
options['switch'] = True
@ -525,9 +594,31 @@ class WebEnginePage(QWebEnginePage):
return self.webview.window.NewWebTab('newtab' ,**options).webpage
def triggerAction(self, action, checked=False):
if action == QWebEnginePage.OpenLinkInNewWindow:
self.createWindow(QWebEnginePage.WebBrowserWindow)
return
return super(WebEnginePage, self).triggerAction(action, checked)
def acceptNavigationRequest(self, url, Type, isMainFrame):
parsed = urlparse(url.toString())
types = {
QWebEnginePage.NavigationTypeLinkClicked: 'link clicked',
QWebEnginePage.NavigationTypeTyped: 'loaded url',
QWebEnginePage.NavigationTypeFormSubmitted: 'form submission',
QWebEnginePage.NavigationTypeBackForward: 'back/forward action',
QWebEnginePage.NavigationTypeReload: 'reloaded',
QWebEnginePage.NavigationTypeOther: 'other type'
}
if not debstable:
types[QWebEnginePage.NavigationTypeRedirect] = 'redirected'
typename = types.get(Type, 'Invalid nav type')
return QWebEnginePage.acceptNavigationRequest(self, url, Type, isMainFrame)
@ -537,36 +628,36 @@ class DevPage(QMainWindow):
self.profile = profile
self.window = parent
self.view = None
self.page = None
#self.setGeometry(100, 100, 800, 600)
self.resize(800, 600)
def Setup(self, url):
self.view = QWebEngineView()
self.view = QWebEngineView(self.window)
self.page = DevWebEnginePage(self.profile, self.window)
self.view.setPage(self.page)
self.setCentralWidget(self.view)
self.setWindowTitle(f'QtWeb Inspector: {url}')
self.show()
self.activateWindow()
return self.page
def closeEvent(self, event):
def closeEvent(self, event=None):
logging.debug('closing dev tools')
#self.view.deleteLater()
#self.page.deleteLater()
#del self.view
#del self.page
event.accept()
self.view.deleteLater()
self.page.deleteLater()
self.view = None
self.page = None
def heck(self):
print('HECK!')
if event:
event.accept()
class DevWebEnginePage(QWebEnginePage):
def __init__(self, profile, parent):
super().__init__(profile)
super().__init__(profile, parent)
self.window = parent

View file

@ -4,9 +4,6 @@ from .config import var
from .Lib.IzzyLib import logging
from PyQt5.QtWebChannel import QWebChannel
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
class JsFuncs(object):
def __init__(self, webview):

View file

@ -4,3 +4,5 @@ publicsuffixlist
asyncqt
urllib3
dbutils
jinja2
hamlpy3