diff --git a/.gitignore b/.gitignore index 739c2f4..af574bb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ __pycache__/ *.py[cod] *$py.class + +# meson build files +builddir diff --git a/README.md b/README.md index d7d6e5b..91ba85b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # Webapp Manager ![build](https://github.com/linuxmint/webapp-manager/actions/workflows/build.yml/badge.svg) +![License](https://img.shields.io/github/license/linuxmint/webapp-manager?label=License) +![GitHub repo size](https://img.shields.io/github/repo-size/linuxmint/webapp-manager?label=Repo%20size) +![GitHub release (latest by date)](https://img.shields.io/github/v/release/linuxmint/webapp-manager?label=Latest%20Stable%20Release) +![Downloads](https://img.shields.io/github/downloads/linuxmint/webapp-manager/total?label=Downloads&style=plastic) Run websites as if they were apps. diff --git a/usr/share/icons/hicolor/scalable/categories/applications-webapps.svg b/data/icons/categories/applications-webapps.svg similarity index 100% rename from usr/share/icons/hicolor/scalable/categories/applications-webapps.svg rename to data/icons/categories/applications-webapps.svg diff --git a/usr/share/icons/hicolor/scalable/apps/webapp-manager.svg b/data/icons/webapp-manager.svg similarity index 100% rename from usr/share/icons/hicolor/scalable/apps/webapp-manager.svg rename to data/icons/webapp-manager.svg diff --git a/data/meson.build b/data/meson.build new file mode 100644 index 0000000..6a55b9c --- /dev/null +++ b/data/meson.build @@ -0,0 +1,45 @@ +app_icon_dir = join_paths(datadir, 'icons', 'hicolor', 'scalable', 'apps') +category_icon_dir = join_paths(datadir, 'icons', 'hicolor', 'scalable', 'categories') +schema_dir = join_paths(datadir, 'glib-2.0', 'schemas') +# message(f'Icon dir: @app_icon_dir@') +# message(f'Schema dir: @schema_dir@') + +# install icons +install_emptydir(app_icon_dir) +install_data( + join_paths(meson.current_source_dir(), 'icons', f'@application_id@.svg'), + install_dir: app_icon_dir, +) + +install_data( + join_paths(meson.current_source_dir(), 'icons/categories/applications-webapps.svg'), + install_dir: category_icon_dir, +) + + +# Install desktop file +# desktop_file = i18n.merge_file( +# input: 'webapp-manager.desktop.in', +# output: 'webapp-manager.desktop', +# type: 'desktop', +# po_dir: '../po', +# install: true, +# install_dir: desktop_dir +# ) + +# Install schema file +schema_file = i18n.merge_file( + input: 'org.x.webapp-manager.gschema.xml.in', + output: 'org.x.webapp-manager.gschema.xml', + type: 'xml', + po_dir: '../po', + install: true, + install_dir: schema_dir +) + +compile_schemas = find_program('glib-compile-schemas', required: false) +if compile_schemas.found() + test('Validate schema file', + compile_schemas, + args: ['--strict', '--dry-run', meson.current_source_dir()]) +endif diff --git a/usr/share/glib-2.0/schemas/org.x.webapp-manager.gschema.xml b/data/org.x.webapp-manager.gschema.xml.in similarity index 100% rename from usr/share/glib-2.0/schemas/org.x.webapp-manager.gschema.xml rename to data/org.x.webapp-manager.gschema.xml.in diff --git a/debian/compat b/debian/compat deleted file mode 100644 index ec63514..0000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -9 diff --git a/debian/control b/debian/control index 150fa3f..5db00a8 100644 --- a/debian/control +++ b/debian/control @@ -2,8 +2,15 @@ Source: webapp-manager Section: admin Priority: optional Maintainer: Linux Mint -Build-Depends: debhelper (>= 9) -Standards-Version: 3.9.5 +Build-Depends: debhelper-compat (= 13), + desktop-file-utils, + dh-python, + libglib2.0-bin, + libgtk-4-bin, + meson (>= 1.3.0), + pkgconf, + python3-all, +Standards-Version: 4.6.1 Package: webapp-manager Architecture: all @@ -15,7 +22,8 @@ Depends: gir1.2-xapp-1.0 (>= 1.4), python3-pil, python3-setproctitle, python3-tldextract, - xapps-common, - ${misc:Depends}, + xapps-common,, + ${python3:Depends}, + ${misc:Depends} Description: Web Application Manager Launch websites as if they were apps. diff --git a/debian/postinst b/debian/postinst index af3b796..19b4d46 100644 --- a/debian/postinst +++ b/debian/postinst @@ -1,16 +1,24 @@ #!/bin/sh +# postinst script for webapp-manager +# +# see: dh_installdeb(1) + set -e + case "$1" in configure) - if which glib-compile-schemas >/dev/null 2>&1 - then - glib-compile-schemas /usr/share/glib-2.0/schemas - fi + if which glib-compile-schemas >/dev/null 2>&1 + then + glib-compile-schemas /usr/share/glib-2.0/schemas + fi + if which gtk-update-icon-cache >/dev/null 2>&1 + then + gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor + fi ;; abort-upgrade|abort-remove|abort-deconfigure) - ;; *) @@ -18,3 +26,10 @@ case "$1" in exit 1 ;; esac + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +exit 0 diff --git a/debian/postrm b/debian/postrm new file mode 100644 index 0000000..1113b1a --- /dev/null +++ b/debian/postrm @@ -0,0 +1,32 @@ +#!/bin/sh +# postrm script for webapp-manager +# +# see: dh_installdeb(1) + +set -e + + +case "$1" in + purge|remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) + if which glib-compile-schemas >/dev/null 2>&1 + then + glib-compile-schemas /usr/share/glib-2.0/schemas + fi + if which gtk-update-icon-cache >/dev/null 2>&1 + then + gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor + fi + ;; + + *) + echo "postrm called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +exit 0 diff --git a/debian/rules b/debian/rules index 097372c..38509f4 100755 --- a/debian/rules +++ b/debian/rules @@ -1,13 +1,23 @@ #!/usr/bin/make -f +# See debhelper(7) (uncomment to enable) +# output every command that modifies files on the build system. +# export DH_VERBOSE = 1 -DEB_VERSION := $(shell dpkg-parsechangelog | egrep '^Version:' | cut -f 2 -d ' ') +export PYBUILD_NAME=webapp-manager +export PYBUILD_SYSTEM=pyproject + +# DEB_VERSION := $(shell dpkg-parsechangelog | egrep '^Version:' | cut -f 2 -d ' ') %: - dh ${@} - -# Inject version number in the code -override_dh_installdeb: - dh_installdeb - for pkg in $$(dh_listpackages -i); do \ - find debian/$$pkg -type f -exec sed -i -e s/__DEB_VERSION__/$(DEB_VERSION)/g {} +; \ - done + dh ${@} --with=python3 --buildsystem=meson + +# # Inject version number in the code +# override_dh_installdeb: +# dh_installdeb +# for pkg in $$(dh_listpackages -i); do \ +# find debian/$$pkg -type f -exec sed -i -e s/__DEB_VERSION__/$(DEB_VERSION)/g {} +; \ +# done + +override_dh_auto_build: + dh_auto_build -O--buildsystem=meson + make -j8 diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..b06fc26 --- /dev/null +++ b/meson.build @@ -0,0 +1,23 @@ +project('webapp-manager', + version: run_command('head', '-1', 'debian/changelog', check: true).stdout().split(' ')[1].strip('(').strip(')'), + license: ['GPL3'], + meson_version: '>= 1.3.0', + default_options: ['warning_level=3', + 'prefix=/usr', + ] +) + +application_id = meson.project_name() +i18n = import('i18n') +pymod = import('python') +python = pymod.find_installation('python3') + +prefix = get_option('prefix') +bindir = get_option('bindir') +datadir = get_option('datadir') + +subdir('src') +subdir('data') +# subdir('po') + +meson.add_install_script('post_install.py') diff --git a/post_install.py b/post_install.py new file mode 100755 index 0000000..23bc2e1 --- /dev/null +++ b/post_install.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 + +from os import environ, path +from subprocess import call + +if not environ.get('DESTDIR', ''): + PREFIX = environ.get('MESON_INSTALL_PREFIX', '/usr') + + schemadir = path.join(PREFIX, 'share', 'glib-2.0', 'schemas') + print('Compiling gsettings schemas...') + call(['glib-compile-schemas', schemadir]) + + themedir = path.join(PREFIX, 'share', 'icons', 'hicolor') + print('Updating icon cache...') + call(['gtk-update-icon-cache', '-qtf', themedir]) + + mimedir = path.join(PREFIX, 'share', 'mime') + print('Updating mime database...') + call(['update-mime-database', mimedir]) diff --git a/src/WebappManager/VERSION.in b/src/WebappManager/VERSION.in new file mode 100644 index 0000000..14d2ff6 --- /dev/null +++ b/src/WebappManager/VERSION.in @@ -0,0 +1 @@ +@version@ diff --git a/src/WebappManager/common.py b/src/WebappManager/common.py new file mode 100644 index 0000000..f8d5ca9 --- /dev/null +++ b/src/WebappManager/common.py @@ -0,0 +1,568 @@ +#!/usr/bin/python3 + +# 1. Standard library imports. +import configparser +import gettext +from io import BytesIO +import json +import locale +import os +from random import choice +import shutil +import string +import sys +import tempfile +import urllib.error +import urllib.parse +import urllib.request +import threading +import traceback +from typing import Optional + +# 2. Related third party imports. +from gi.repository import GObject +import PIL.Image +import requests +# Note: BeautifulSoup is an optional import supporting another way of getting a website's favicons. + + +# Used as a decorator to run things in the background +def _async(func): + def wrapper(*args, **kwargs): + thread = threading.Thread(target=func, args=args, kwargs=kwargs) + thread.daemon = True + thread.start() + return thread + return wrapper + +# Used as a decorator to run things in the main loop, from another thread +def idle(func): + def wrapper(*args): + GObject.idle_add(func, *args) + return wrapper + +# i18n +APP = 'webapp-manager' +LOCALE_DIR = "/usr/share/locale" +locale.bindtextdomain(APP, LOCALE_DIR) +gettext.bindtextdomain(APP, LOCALE_DIR) +gettext.textdomain(APP) +_ = gettext.gettext + +# Constants +ICE_DIR = os.path.expanduser("~/.local/share/ice") +APPS_DIR = os.path.expanduser("~/.local/share/applications") +PROFILES_DIR = os.path.join(ICE_DIR, "profiles") +FIREFOX_PROFILES_DIR = os.path.join(ICE_DIR, "firefox") +FIREFOX_FLATPAK_PROFILES_DIR = os.path.expanduser("~/.var/app/org.mozilla.firefox/data/ice/firefox") +FIREFOX_SNAP_PROFILES_DIR = os.path.expanduser("~/snap/firefox/common/.mozilla/firefox") +LIBREWOLF_FLATPAK_PROFILES_DIR = os.path.expanduser("~/.var/app/io.gitlab.librewolf-community/data/ice/librewolf") +WATERFOX_FLATPAK_PROFILES_DIR = os.path.expanduser("~/.var/app/net.waterfox.waterfox/data") +FLOORP_FLATPAK_PROFILES_DIR = os.path.expanduser("~/.var/app/one.ablaze.floorp/data") +EPIPHANY_PROFILES_DIR = os.path.join(ICE_DIR, "epiphany") +FALKON_PROFILES_DIR = os.path.join(ICE_DIR, "falkon") +ICONS_DIR = os.path.join(ICE_DIR, "icons") +BROWSER_TYPE_FIREFOX, BROWSER_TYPE_FIREFOX_FLATPAK, BROWSER_TYPE_FIREFOX_SNAP, BROWSER_TYPE_LIBREWOLF_FLATPAK, BROWSER_TYPE_WATERFOX_FLATPAK, BROWSER_TYPE_FLOORP_FLATPAK, BROWSER_TYPE_CHROMIUM, BROWSER_TYPE_EPIPHANY, BROWSER_TYPE_FALKON = range(9) + +class Browser: + + def __init__(self, browser_type, name, exec_path, test_path): + self.browser_type = browser_type + self.name = name + self.exec_path = exec_path + self.test_path = test_path + +# This is a data structure representing +# the app menu item (path, name, icon..etc.) +class WebAppLauncher: + + def __init__(self, path, codename): + self.path = path + self.codename = codename + self.web_browser = None + self.name = None + self.icon = None + self.is_valid = False + self.exec = None + self.category = None + self.url = "" + self.custom_parameters = "" + self.isolate_profile = False + self.navbar = False + self.privatewindow = False + + is_webapp = False + with open(path) as desktop_file: + for line in desktop_file: + line = line.strip() + + # Identify if the app is a webapp + if "StartupWMClass=WebApp" in line or "StartupWMClass=Chromium" in line or "StartupWMClass=ICE-SSB" in line: + is_webapp = True + continue + + if "Name=" in line: + self.name = line.replace("Name=", "") + continue + + if "Icon=" in line: + self.icon = line.replace("Icon=", "") + continue + + if "Exec=" in line: + self.exec = line.replace("Exec=", "") + continue + + if "Categories=" in line: + self.category = line.replace("Categories=", "").replace("GTK;", "").replace(";", "") + continue + + if "X-WebApp-Browser=" in line: + self.web_browser = line.replace("X-WebApp-Browser=", "") + continue + + if "X-WebApp-URL=" in line: + self.url = line.replace("X-WebApp-URL=", "") + continue + + if "X-WebApp-CustomParameters" in line: + self.custom_parameters = line.replace("X-WebApp-CustomParameters=", "") + continue + + if "X-WebApp-Isolated" in line: + self.isolate_profile = line.replace("X-WebApp-Isolated=", "").lower() == "true" + continue + + if "X-WebApp-Navbar" in line: + self.navbar = line.replace("X-WebApp-Navbar=", "").lower() == "true" + continue + + if "X-WebApp-PrivateWindow" in line: + self.privatewindow = line.replace("X-WebApp-PrivateWindow=", "").lower() == "true" + continue + + if is_webapp and self.name is not None and self.icon is not None: + self.is_valid = True + +# This is the backend. +# It contains utility functions to load, +# save and delete webapps. +class WebAppManager: + + def __init__(self): + for directory in [ICE_DIR, APPS_DIR, PROFILES_DIR, FIREFOX_PROFILES_DIR, FIREFOX_FLATPAK_PROFILES_DIR, ICONS_DIR, EPIPHANY_PROFILES_DIR, FALKON_PROFILES_DIR]: + if not os.path.exists(directory): + os.makedirs(directory) + + def get_webapps(self): + webapps = [] + for filename in os.listdir(APPS_DIR): + if filename.lower().startswith("webapp-") and filename.endswith(".desktop"): + path = os.path.join(APPS_DIR, filename) + codename = filename.replace("webapp-", "").replace("WebApp-", "").replace(".desktop", "") + if not os.path.isdir(path): + try: + webapp = WebAppLauncher(path, codename) + if webapp.is_valid: + webapps.append(webapp) + except Exception: + print("Could not create webapp for path", path) + traceback.print_exc() + + return webapps + + @staticmethod + def get_supported_browsers(): + # type, name, exec, test + return [Browser(BROWSER_TYPE_FIREFOX, "Firefox", "firefox", "/usr/bin/firefox"), + Browser(BROWSER_TYPE_FIREFOX, "Firefox Developer Edition", "firefox-developer-edition", "/usr/bin/firefox-developer-edition"), + Browser(BROWSER_TYPE_FIREFOX, "Firefox Nightly", "firefox-nightly", "/usr/bin/firefox-nightly"), + Browser(BROWSER_TYPE_FIREFOX, "Firefox Extended Support Release", "firefox-esr", "/usr/bin/firefox-esr"), + Browser(BROWSER_TYPE_FIREFOX_FLATPAK, "Firefox (Flatpak)", "/var/lib/flatpak/exports/bin/org.mozilla.firefox", "/var/lib/flatpak/exports/bin/org.mozilla.firefox"), + Browser(BROWSER_TYPE_FIREFOX_FLATPAK, "Firefox (Flatpak)", ".local/share/flatpak/exports/bin/org.mozilla.firefox", ".local/share/flatpak/exports/bin/org.mozilla.firefox"), + Browser(BROWSER_TYPE_FIREFOX_SNAP, "Firefox (Snap)", "/snap/bin/firefox", "/snap/bin/firefox"), + Browser(BROWSER_TYPE_CHROMIUM, "Brave", "brave", "/usr/bin/brave"), + Browser(BROWSER_TYPE_CHROMIUM, "Brave Browser", "brave-browser", "/usr/bin/brave-browser"), + Browser(BROWSER_TYPE_CHROMIUM, "Brave (Bin)", "brave-bin", "/usr/bin/brave-bin"), + Browser(BROWSER_TYPE_CHROMIUM, "Chrome", "google-chrome-stable", "/usr/bin/google-chrome-stable"), + Browser(BROWSER_TYPE_CHROMIUM, "Chrome (Beta)", "google-chrome-beta", "/usr/bin/google-chrome-beta"), + Browser(BROWSER_TYPE_CHROMIUM, "Chrome (Flatpak)", "/var/lib/flatpak/exports/bin/com.google.Chrome", "/var/lib/flatpak/exports/bin/com.google.Chrome"), + Browser(BROWSER_TYPE_CHROMIUM, "Chrome (Flatpak)", ".local/share/flatpak/exports/bin/com.google.Chrome", ".local/share/flatpak/exports/bin/com.google.Chrome"), + Browser(BROWSER_TYPE_CHROMIUM, "Chromium", "chromium", "/usr/bin/chromium"), + Browser(BROWSER_TYPE_CHROMIUM, "Chromium (chromium-browser)", "chromium-browser", "/usr/bin/chromium-browser"), + Browser(BROWSER_TYPE_CHROMIUM, "Chromium (Snap)", "chromium", "/snap/bin/chromium"), + Browser(BROWSER_TYPE_CHROMIUM, "Chromium (Bin)", "chromium-bin", "/usr/bin/chromium-bin-browser"), + Browser(BROWSER_TYPE_CHROMIUM, "Ungoogled Chromium", "ungoogled-chromium", "/usr/bin/ungoogled-chromium"), + Browser(BROWSER_TYPE_EPIPHANY, "Epiphany", "epiphany", "/usr/bin/epiphany"), + Browser(BROWSER_TYPE_FIREFOX, "LibreWolf", "librewolf", "/usr/bin/librewolf"), + Browser(BROWSER_TYPE_LIBREWOLF_FLATPAK, "LibreWolf (Flatpak)", "/var/lib/flatpak/exports/bin/io.gitlab.librewolf-community", "/var/lib/flatpak/exports/bin/io.gitlab.librewolf-community"), + Browser(BROWSER_TYPE_LIBREWOLF_FLATPAK, "LibreWolf (Flatpak)", ".local/share/flatpak/exports/bin/io.gitlab.librewolf-community", ".local/share/flatpak/exports/bin/io.gitlab.librewolf-community"), + Browser(BROWSER_TYPE_FIREFOX, "Waterfox", "waterfox", "/usr/bin/waterfox"), + Browser(BROWSER_TYPE_FIREFOX, "Waterfox Current", "waterfox-current", "/usr/bin/waterfox-current"), + Browser(BROWSER_TYPE_FIREFOX, "Waterfox Classic", "waterfox-classic", "/usr/bin/waterfox-classic"), + Browser(BROWSER_TYPE_FIREFOX, "Waterfox 3rd Generation", "waterfox-g3", "/usr/bin/waterfox-g3"), + Browser(BROWSER_TYPE_FIREFOX, "Waterfox 4th Generation", "waterfox-g4", "/usr/bin/waterfox-g4"), + Browser(BROWSER_TYPE_FIREFOX, "Floorp", "floorp", "/usr/bin/floorp"), + Browser(BROWSER_TYPE_WATERFOX_FLATPAK, "Waterfox (Flatpak)", "/var/lib/flatpak/exports/bin/net.waterfox.waterfox", "/var/lib/flatpak/exports/bin/net.waterfox.waterfox"), + Browser(BROWSER_TYPE_WATERFOX_FLATPAK, "Waterfox (Flatpak)", ".local/share/flatpak/exports/bin/net.waterfox.waterfox", ".local/share/flatpak/exports/bin/net.waterfox.waterfox"), + Browser(BROWSER_TYPE_CHROMIUM, "Vivaldi", "vivaldi-stable", "/usr/bin/vivaldi-stable"), + Browser(BROWSER_TYPE_CHROMIUM, "Vivaldi Snapshot", "vivaldi-snapshot", "/usr/bin/vivaldi-snapshot"), + Browser(BROWSER_TYPE_CHROMIUM, "Vivaldi (Flatpak)", "/var/lib/flatpak/exports/bin/com.vivaldi.Vivaldi", "/var/lib/flatpak/exports/bin/com.vivaldi.Vivaldi"), + Browser(BROWSER_TYPE_CHROMIUM, "Vivaldi (Flatpak)", ".local/share/flatpak/exports/bin/com.vivaldi.Vivaldi", ".local/share/flatpak/exports/bin/com.vivaldi.Vivaldi"), + Browser(BROWSER_TYPE_CHROMIUM, "Microsoft Edge", "microsoft-edge-stable", "/usr/bin/microsoft-edge-stable"), + Browser(BROWSER_TYPE_CHROMIUM, "Microsoft Edge Beta", "microsoft-edge-beta", "/usr/bin/microsoft-edge-beta"), + Browser(BROWSER_TYPE_CHROMIUM, "Microsoft Edge Dev", "microsoft-edge-dev", "/usr/bin/microsoft-edge-dev"), + Browser(BROWSER_TYPE_CHROMIUM, "FlashPeak Slimjet", "flashpeak-slimjet", "/usr/bin/flashpeak-slimjet"), + Browser(BROWSER_TYPE_CHROMIUM, "Ungoogled Chromium (Flatpak)", "/var/lib/flatpak/exports/bin/io.github.ungoogled_software.ungoogled_chromium", "/var/lib/flatpak/exports/bin/io.github.ungoogled_software.ungoogled_chromium"), + Browser(BROWSER_TYPE_CHROMIUM, "Ungoogled Chromium (Flatpak)", ".local/share/flatpak/exports/bin/io.github.ungoogled_software.ungoogled_chromium", ".local/share/flatpak/exports/bin/io.github.ungoogled_software.ungoogled_chromium"), + Browser(BROWSER_TYPE_CHROMIUM, "Chromium (Flatpak)", "/var/lib/flatpak/exports/bin/org.chromium.Chromium", "/var/lib/flatpak/exports/bin/org.chromium.Chromium"), + Browser(BROWSER_TYPE_CHROMIUM, "Chromium (Flatpak)", ".local/share/flatpak/exports/bin/org.chromium.Chromium", ".local/share/flatpak/exports/bin/org.chromium.Chromium"), + Browser(BROWSER_TYPE_FALKON, "Falkon", "falkon", "/usr/bin/falkon"), + Browser(BROWSER_TYPE_CHROMIUM, "Edge (Flatpak)", "/var/lib/flatpak/exports/bin/com.microsoft.Edge", "/var/lib/flatpak/exports/bin/com.microsoft.Edge"), + Browser(BROWSER_TYPE_CHROMIUM, "Edge (Flatpak)", ".local/share/flatpak/exports/bin/com.microsoft.Edge", ".local/share/flatpak/exports/bin/com.microsoft.Edge"), + Browser(BROWSER_TYPE_CHROMIUM, "Brave (Flatpak)", "/var/lib/flatpak/exports/bin/com.brave.Browser", "/var/lib/flatpak/exports/bin/com.brave.Browser"), + Browser(BROWSER_TYPE_CHROMIUM, "Brave (Flatpak)", ".local/share/flatpak/exports/bin/com.brave.Browser", ".local/share/flatpak/exports/bin/com.brave.Browser"), + Browser(BROWSER_TYPE_CHROMIUM, "Yandex", "yandex-browser", "/usr/bin/yandex-browser"), + Browser(BROWSER_TYPE_FALKON, "Falkon (Flatpak)", "/var/lib/flatpak/exports/bin/org.kde.falkon", "/var/lib/flatpak/exports/bin/org.kde.falkon"), + Browser(BROWSER_TYPE_FALKON, "Falkon (Flatpak)", ".local/share/flatpak/exports/bin/org.kde.falkon", ".local/share/flatpak/exports/bin/org.kde.falkon"), + Browser(BROWSER_TYPE_CHROMIUM, "Naver Whale", "naver-whale-stable", "/usr/bin/naver-whale-stable"), + Browser(BROWSER_TYPE_CHROMIUM, "Yandex (Flatpak)", "/var/lib/flatpak/exports/bin/ru.yandex.Browser", "/var/lib/flatpak/exports/bin/ru.yandex.Browser"), + Browser(BROWSER_TYPE_CHROMIUM, "Yandex (Flatpak)", ".local/share/flatpak/exports/bin/ru.yandex.Browser", ".local/share/flatpak/exports/bin/ru.yandex.Browser"), + Browser(BROWSER_TYPE_CHROMIUM, "Thorium", "thorium-browser", "/usr/bin/thorium-browser"), + Browser(BROWSER_TYPE_FIREFOX, "Floorp", "floorp", "/usr/bin/floorp"), + Browser(BROWSER_TYPE_FLOORP_FLATPAK, "Floorp (Flatpak)", "/var/lib/flatpak/exports/bin/one.ablaze.floorp", "/var/lib/flatpak/exports/bin/one.ablaze.floorp"), + Browser(BROWSER_TYPE_FLOORP_FLATPAK, "Floorp (Flatpak)", ".local/share/flatpak/exports/bin/one.ablaze.floorp", ".local/share/flatpak/exports/bin/one.ablaze.floorp") + ] + + def delete_webbapp(self, webapp): + shutil.rmtree(os.path.join(FIREFOX_PROFILES_DIR, webapp.codename), ignore_errors=True) + shutil.rmtree(os.path.join(FIREFOX_FLATPAK_PROFILES_DIR, webapp.codename), ignore_errors=True) + shutil.rmtree(os.path.join(FIREFOX_SNAP_PROFILES_DIR, webapp.codename), ignore_errors=True) + shutil.rmtree(os.path.join(PROFILES_DIR, webapp.codename), ignore_errors=True) + # first remove symlinks then others + if os.path.exists(webapp.path): + os.remove(webapp.path) + epiphany_orig_prof_dir=os.path.join(os.path.expanduser("~/.local/share"), "org.gnome.Epiphany.WebApp-" + webapp.codename) + if os.path.exists(epiphany_orig_prof_dir): + os.remove(epiphany_orig_prof_dir) + shutil.rmtree(os.path.join(EPIPHANY_PROFILES_DIR, "org.gnome.Epiphany.WebApp-%s" % webapp.codename), ignore_errors=True) + falkon_orig_prof_dir = os.path.join(os.path.expanduser("~/.config/falkon/profiles"), webapp.codename) + if os.path.exists(falkon_orig_prof_dir): + os.remove(falkon_orig_prof_dir) + shutil.rmtree(os.path.join(FALKON_PROFILES_DIR, webapp.codename), ignore_errors=True) + + def create_webapp(self, name, url, icon, category, browser, custom_parameters, isolate_profile=True, navbar=False, privatewindow=False): + # Generate a 4 digit random code (to prevent name collisions, so we can define multiple launchers with the same name) + random_code = ''.join(choice(string.digits) for _ in range(4)) + codename = "".join(filter(str.isalpha, name)) + random_code + path = os.path.join(APPS_DIR, "WebApp-%s.desktop" % codename) + + with open(path, 'w') as desktop_file: + desktop_file.write("[Desktop Entry]\n") + desktop_file.write("Version=1.0\n") + desktop_file.write("Name=%s\n" % name) + desktop_file.write("Comment=%s\n" % _("Web App")) + + exec_string = self.get_exec_string(browser, codename, custom_parameters, icon, isolate_profile, navbar, + privatewindow, url) + + desktop_file.write("Exec=%s\n" % exec_string) + desktop_file.write("Terminal=false\n") + desktop_file.write("X-MultipleArgs=false\n") + desktop_file.write("Type=Application\n") + desktop_file.write("Icon=%s\n" % icon) + desktop_file.write("Categories=GTK;%s;\n" % category) + desktop_file.write("MimeType=text/html;text/xml;application/xhtml_xml;\n") + desktop_file.write("StartupWMClass=WebApp-%s\n" % codename) + desktop_file.write("StartupNotify=true\n") + desktop_file.write("X-WebApp-Browser=%s\n" % browser.name) + desktop_file.write("X-WebApp-URL=%s\n" % url) + desktop_file.write("X-WebApp-CustomParameters=%s\n" % custom_parameters) + desktop_file.write("X-WebApp-Navbar=%s\n" % bool_to_string(navbar)) + desktop_file.write("X-WebApp-PrivateWindow=%s\n" % bool_to_string(privatewindow)) + desktop_file.write("X-WebApp-Isolated=%s\n" % bool_to_string(isolate_profile)) + + if browser.browser_type == BROWSER_TYPE_EPIPHANY: + # Move the desktop file and create a symlink + epiphany_profile_path = os.path.join(EPIPHANY_PROFILES_DIR, "org.gnome.Epiphany.WebApp-" + codename) + new_path = os.path.join(epiphany_profile_path, "org.gnome.Epiphany.WebApp-%s.desktop" % codename) + os.makedirs(epiphany_profile_path) + os.replace(path, new_path) + os.symlink(new_path, path) + # copy the icon to profile directory + new_icon=os.path.join(epiphany_profile_path, "app-icon.png") + shutil.copy(icon, new_icon) + # required for app mode. create an empty file .app + app_mode_file=os.path.join(epiphany_profile_path, ".app") + with open(app_mode_file, 'w') as fp: + pass + + if browser.browser_type == BROWSER_TYPE_FALKON: + falkon_profile_path = os.path.join(FALKON_PROFILES_DIR, codename) + os.makedirs(falkon_profile_path) + # Create symlink of profile dir at ~/.config/falkon/profiles + falkon_orig_prof_dir = os.path.join(os.path.expanduser("~/.config/falkon/profiles"), codename) + os.symlink(falkon_profile_path, falkon_orig_prof_dir) + + + def get_exec_string(self, browser, codename, custom_parameters, icon, isolate_profile, navbar, privatewindow, url): + if browser.browser_type in [BROWSER_TYPE_FIREFOX, BROWSER_TYPE_FIREFOX_FLATPAK, BROWSER_TYPE_FIREFOX_SNAP]: + # Firefox based + if browser.browser_type == BROWSER_TYPE_FIREFOX: + firefox_profiles_dir = FIREFOX_PROFILES_DIR + elif browser.browser_type == BROWSER_TYPE_FIREFOX_FLATPAK: + firefox_profiles_dir = FIREFOX_FLATPAK_PROFILES_DIR + else: + firefox_profiles_dir = FIREFOX_SNAP_PROFILES_DIR + firefox_profile_path = os.path.join(firefox_profiles_dir, codename) + exec_string = ("sh -c 'XAPP_FORCE_GTKWINDOW_ICON=\"" + icon + "\" " + browser.exec_path + + " --class WebApp-" + codename + + " --name WebApp-" + codename + + " --profile " + firefox_profile_path + + " --no-remote") + if privatewindow: + exec_string += " --private-window" + if custom_parameters: + exec_string += " {}".format(custom_parameters) + exec_string += " \"" + url + "\"" + "'" + # Create a Firefox profile + shutil.copytree('/usr/share/webapp-manager/firefox/profile', firefox_profile_path, dirs_exist_ok = True) + if navbar: + shutil.copy('/usr/share/webapp-manager/firefox/userChrome-with-navbar.css', + os.path.join(firefox_profile_path, "chrome", "userChrome.css")) + elif browser.browser_type == BROWSER_TYPE_LIBREWOLF_FLATPAK: + # LibreWolf flatpak + firefox_profiles_dir = LIBREWOLF_FLATPAK_PROFILES_DIR + firefox_profile_path = os.path.join(firefox_profiles_dir, codename) + exec_string = ("sh -c 'XAPP_FORCE_GTKWINDOW_ICON=\"" + icon + "\" " + browser.exec_path + + " --class WebApp-" + codename + + " --name WebApp-" + codename + + " --profile " + firefox_profile_path + + " --no-remote") + if privatewindow: + exec_string += " --private-window" + if custom_parameters: + exec_string += " {}".format(custom_parameters) + exec_string += " \"" + url + "\"" + "'" + # Create a Firefox profile + shutil.copytree('/usr/share/webapp-manager/firefox/profile', firefox_profile_path, dirs_exist_ok = True) + if navbar: + shutil.copy('/usr/share/webapp-manager/firefox/userChrome-with-navbar.css', + os.path.join(firefox_profile_path, "chrome", "userChrome.css")) + elif browser.browser_type == BROWSER_TYPE_FLOORP_FLATPAK: + # Floorp flatpak + firefox_profiles_dir = FLOORP_FLATPAK_PROFILES_DIR + firefox_profile_path = os.path.join(firefox_profiles_dir, codename) + exec_string = ("sh -c 'XAPP_FORCE_GTKWINDOW_ICON=\"" + icon + "\" " + browser.exec_path + + " --class WebApp-" + codename + + " --name WebApp-" + codename + + " --profile " + firefox_profile_path + + " --no-remote") + if privatewindow: + exec_string += " --private-window" + if custom_parameters: + exec_string += " {}".format(custom_parameters) + exec_string += " \"" + url + "\"" + "'" + # Create a Firefox profile + shutil.copytree('/usr/share/webapp-manager/firefox/profile', firefox_profile_path, dirs_exist_ok = True) + if navbar: + shutil.copy('/usr/share/webapp-manager/firefox/userChrome-with-navbar.css', + os.path.join(firefox_profile_path, "chrome", "userChrome.css")) + elif browser.browser_type == BROWSER_TYPE_EPIPHANY: + # Epiphany based + epiphany_profile_path = os.path.join(EPIPHANY_PROFILES_DIR, "org.gnome.Epiphany.WebApp-" + codename) + # Create symlink of profile dir at ~/.local/share + epiphany_orig_prof_dir = os.path.join(os.path.expanduser("~/.local/share"), + "org.gnome.Epiphany.WebApp-" + codename) + os.symlink(epiphany_profile_path, epiphany_orig_prof_dir) + exec_string = browser.exec_path + exec_string += " --application-mode " + exec_string += " --profile=\"" + epiphany_orig_prof_dir + "\"" + exec_string += " \"" + url + "\"" + if custom_parameters: + exec_string += " {}".format(custom_parameters) + elif browser.browser_type == BROWSER_TYPE_FALKON: + # KDE Falkon + exec_string = browser.exec_path + exec_string += " --wmclass=WebApp-" + codename + if isolate_profile: + exec_string += " --profile=" + codename + if privatewindow: + exec_string += " --private-browsing" + if custom_parameters: + exec_string += " {}".format(custom_parameters) + exec_string += " --no-remote " + url + else: + # Chromium based + if isolate_profile: + profile_path = os.path.join(PROFILES_DIR, codename) + exec_string = (browser.exec_path + + " --app=" + "\"" + url + "\"" + + " --class=WebApp-" + codename + + " --name=WebApp-" + codename + + " --user-data-dir=" + profile_path) + else: + exec_string = (browser.exec_path + + " --app=" + "\"" + url + "\"" + + " --class=WebApp-" + codename + + " --name=WebApp-" + codename) + + if privatewindow: + if browser.name == "Microsoft Edge": + exec_string += " --inprivate" + elif browser.name == "Microsoft Edge Beta": + exec_string += " --inprivate" + elif browser.name == "Microsoft Edge Dev": + exec_string += " --inprivate" + else: + exec_string += " --incognito" + + if custom_parameters: + exec_string += " {}".format(custom_parameters) + + return exec_string + + def edit_webapp(self, path, name, browser, url, icon, category, custom_parameters, codename, isolate_profile, navbar, privatewindow): + config = configparser.RawConfigParser() + config.optionxform = str + config.read(path) + config.set("Desktop Entry", "Name", name) + config.set("Desktop Entry", "Icon", icon) + config.set("Desktop Entry", "Comment", _("Web App")) + config.set("Desktop Entry", "Categories", "GTK;%s;" % category) + + try: + # This will raise an exception on legacy apps which + # have no X-WebApp-URL and X-WebApp-Browser + + exec_line = self.get_exec_string(browser, codename, custom_parameters, icon, isolate_profile, navbar, privatewindow, url) + + config.set("Desktop Entry", "Exec", exec_line) + config.set("Desktop Entry", "X-WebApp-Browser", browser.name) + config.set("Desktop Entry", "X-WebApp-URL", url) + config.set("Desktop Entry", "X-WebApp-CustomParameters", custom_parameters) + config.set("Desktop Entry", "X-WebApp-Isolated", bool_to_string(isolate_profile)) + config.set("Desktop Entry", "X-WebApp-Navbar", bool_to_string(navbar)) + config.set("Desktop Entry", "X-WebApp-PrivateWindow", bool_to_string(privatewindow)) + + except: + print("This WebApp was created with an old version of WebApp Manager. Its URL cannot be edited.") + + with open(path, 'w') as configfile: + config.write(configfile, space_around_delimiters=False) + +def bool_to_string(boolean): + if boolean: + return "true" + else: + return "false" + +def normalize_url(url): + (scheme, netloc, path, _, _, _) = urllib.parse.urlparse(url, "http") + if not netloc and path: + return urllib.parse.urlunparse((scheme, path, "", "", "", "")) + return urllib.parse.urlunparse((scheme, netloc, path, "", "", "")) + +def download_image(root_url: str, link: str) -> Optional[PIL.Image.Image]: + if "://" not in link: + if link.startswith("/"): + link = root_url + link + else: + link = root_url + "/" + link + try: + response = requests.get(link, timeout=3) + image = PIL.Image.open(BytesIO(response.content)) + if image.height > 256: + return image.resize((256, 256), PIL.Image.BICUBIC) + return image + except Exception as e: + print(e) + print(link) + return None + +def _find_link_favicon(soup, iconformat): + items = soup.find_all("link", {"rel": iconformat}) + for item in items: + link = item.get("href") + if link: + yield link + +def _find_meta_content(soup, iconformat): + item = soup.find("meta", {"name": iconformat}) + if not item: + return + link = item.get("content") + if link: + yield link + +def _find_property(soup, iconformat): + items = soup.find_all("meta", {"property": iconformat}) + for item in items: + link = item.get("content") + if link: + yield link + +def _find_url(_soup, iconformat): + yield iconformat + + +def download_favicon(url): + images = [] + url = normalize_url(url) + (scheme, netloc, path, _, _, _) = urllib.parse.urlparse(url) + root_url = "%s://%s" % (scheme, netloc) + + # try favicon grabber first + try: + response = requests.get("https://favicongrabber.com/api/grab/%s?pretty=true" % netloc, timeout=3) + if response.status_code == 200: + source = response.content.decode("UTF-8") + array = json.loads(source) + for icon in array['icons']: + image = download_image(root_url, icon['src']) + if image is not None: + t = tempfile.NamedTemporaryFile(suffix=".png", delete=False) + images.append(["Favicon Grabber", image, t.name]) + image.save(t.name) + images = sorted(images, key = lambda x: x[1].height, reverse=True) + if images: + return images + except Exception as e: + print(e) + + # Fallback: Check HTML and /favicon.ico + try: + response = requests.get(url, timeout=3) + if response.ok: + import bs4 + soup = bs4.BeautifulSoup(response.content, "html.parser") + + iconformats = [ + ("apple-touch-icon", _find_link_favicon), + ("shortcut icon", _find_link_favicon), + ("icon", _find_link_favicon), + ("msapplication-TileImage", _find_meta_content), + ("msapplication-square310x310logo", _find_meta_content), + ("msapplication-square150x150logo", _find_meta_content), + ("msapplication-square70x70logo", _find_meta_content), + ("og:image", _find_property), + ("favicon.ico", _find_url), + ] + + # icons defined in the HTML + for (iconformat, getter) in iconformats: + for link in getter(soup, iconformat): + image = download_image(root_url, link) + if image is not None: + t = tempfile.NamedTemporaryFile(suffix=".png", delete=False) + images.append([iconformat, image, t.name]) + image.save(t.name) + + except Exception as e: + print(e) + + images = sorted(images, key = lambda x: x[1].height, reverse=True) + return images + +if __name__ == "__main__": + download_favicon(sys.argv[1]) diff --git a/usr/lib/webapp-manager/webapp-manager.py b/src/WebappManager/main.py old mode 100755 new mode 100644 similarity index 98% rename from usr/lib/webapp-manager/webapp-manager.py rename to src/WebappManager/main.py index fa403bf..c6f5900 --- a/usr/lib/webapp-manager/webapp-manager.py +++ b/src/WebappManager/main.py @@ -6,6 +6,7 @@ import os import shutil import subprocess +import sys import warnings # 2. Related third party imports. @@ -21,7 +22,7 @@ from gi.repository import Gtk, Gdk, Gio, XApp, GdkPixbuf # 3. Local application/library specific imports. -from common import _async, idle, WebAppManager, download_favicon, ICONS_DIR, BROWSER_TYPE_FIREFOX, BROWSER_TYPE_FIREFOX_FLATPAK, BROWSER_TYPE_FIREFOX_SNAP +from WebappManager.common import _async, idle, WebAppManager, download_favicon, ICONS_DIR, BROWSER_TYPE_FIREFOX, BROWSER_TYPE_FIREFOX_FLATPAK, BROWSER_TYPE_FIREFOX_SNAP setproctitle.setproctitle("webapp-manager") @@ -33,6 +34,10 @@ gettext.textdomain(APP) _ = gettext.gettext +# get version +version_file = os.path.dirname(os.path.abspath(__file__))+'/VERSION' +__version__ = open(version_file, 'r').readlines()[0] + COL_ICON, COL_NAME, COL_BROWSER, COL_WEBAPP = range(4) CATEGORY_ID, CATEGORY_NAME = range(2) BROWSER_OBJ, BROWSER_NAME = range(2) @@ -248,7 +253,7 @@ def open_about(self, widget): except Exception as e: print(e) - dlg.set_version("__DEB_VERSION__") + dlg.set_version(__version__) dlg.set_icon_name("webapp-manager") dlg.set_logo_icon_name("webapp-manager") dlg.set_website("https://www.github.com/linuxmint/webapp-manager") @@ -542,7 +547,7 @@ def load_webapps(self): self.headerbar.set_subtitle(_("Run websites as if they were apps")) -if __name__ == "__main__": +def main(): + """The application's entry point.""" application = MyApplication("org.x.webapp-manager", Gio.ApplicationFlags.FLAGS_NONE) - application.run() - + return application.run(sys.argv) diff --git a/src/WebappManager/meson.build b/src/WebappManager/meson.build new file mode 100644 index 0000000..839a3ab --- /dev/null +++ b/src/WebappManager/meson.build @@ -0,0 +1,17 @@ +python_sources = files('common.py', 'main.py') + +verconf= configuration_data() +verconf.set('version', meson.project_version()) +version_file = configure_file( + input: 'VERSION.in', + output: 'VERSION', + configuration: verconf, + install: true, + install_dir: pkgdatadir/'WebappManager', +) + +install_data( + python_sources, + preserve_path: true, + install_dir: pkgdatadir/'WebappManager', +) diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..dfa88c8 --- /dev/null +++ b/src/meson.build @@ -0,0 +1,17 @@ +pkgdatadir = join_paths(prefix, 'lib', application_id) +message(f'pkgdata dir: @pkgdatadir@') + +conf = configuration_data() +conf.set('PYTHON', python.full_path()) +conf.set('pkgdatadir', pkgdatadir) + +configure_file( + input: 'webapp-manager.in', + output: 'webapp-manager', + configuration: conf, + install: true, + install_dir: get_option('bindir'), + install_mode: 'rwxr-xr-x' +) + +subdir('WebappManager') diff --git a/src/webapp-manager.in b/src/webapp-manager.in new file mode 100644 index 0000000..ca80a6f --- /dev/null +++ b/src/webapp-manager.in @@ -0,0 +1,30 @@ +#!@PYTHON@ + +# webapp-manager.in +# +# Copyright 2024 Linux Mint +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# SPDX-License-Identifier: GPL-3.0-or-later + +import re +import sys + +sys.path.insert(1, '@pkgdatadir@') + +from WebappManager.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/test b/test index b14a952..1d4e794 100755 --- a/test +++ b/test @@ -1,5 +1,9 @@ #!/bin/bash -sudo rm -rf /usr/lib/webapp-manager -sudo rm -rf /usr/share/webapp-manager -sudo cp -R usr / +rm -rf builddir +meson setup -Dprefix=$HOME/.local builddir +meson compile -C builddir --verbose +meson install -C builddir # --dry-run # with dry-run files are not acutally installed webapp-manager + +# To remove installed files uncomment below line +# ninja uninstall -C builddir diff --git a/usr/bin/webapp-manager b/usr/bin/webapp-manager deleted file mode 100755 index 144e87c..0000000 --- a/usr/bin/webapp-manager +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -/usr/lib/webapp-manager/webapp-manager.py &