From 040a3b54a49a93ce23917fa631a957dee1ae0804 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sun, 10 Sep 2023 09:19:56 +0800 Subject: [PATCH 001/315] fix: make download button back to work in code editor (#537) * fix: download button in code editor * refactor: open link from new tab --- src/page/Components/Editor/Editor.svelte | 12 +----- src/page/Components/Settings.svelte | 11 ++++- src/shared/utils.js | 52 ++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/src/page/Components/Editor/Editor.svelte b/src/page/Components/Editor/Editor.svelte index 81bb18b0..e8f1ed49 100644 --- a/src/page/Components/Editor/Editor.svelte +++ b/src/page/Components/Editor/Editor.svelte @@ -3,7 +3,7 @@ import Loader from "../../../shared/Components/Loader.svelte"; import IconButton from "../../../shared/Components/IconButton.svelte"; import Tag from "../../../shared/Components/Tag.svelte"; - import {formatDate} from "../../../shared/utils.js"; + import {formatDate, downloadToFile} from "../../../shared/utils.js"; import CodeMirror, {cmChanged, cmGetInstance, cmSetSavedCode} from "./CodeMirror.svelte"; import iconDownload from "../../../shared/img/icon-download.svg?raw"; import iconTrash from "../../../shared/img/icon-trash.svg?raw"; @@ -85,15 +85,7 @@ const discard = () => codemirror.discardChanges(); function download() { - const link = document.createElement("a"); - const content = codemirror.getValue(); - const filename = activeItem.filename; - link.setAttribute("href", `data:text/plain;charset=utf-8,${encodeURIComponent(content)}`); - link.setAttribute("download", filename); - link.style.display = "none"; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + downloadToFile(activeItem.filename, codemirror.getValue()); } async function update() { diff --git a/src/page/Components/Settings.svelte b/src/page/Components/Settings.svelte index 5b89ea28..40f7f7b3 100644 --- a/src/page/Components/Settings.svelte +++ b/src/page/Components/Settings.svelte @@ -1,6 +1,7 @@ +
Userscripts App Icon

You can turn on the Userscripts iOS Safari extension in Settings. Read the docs.

CURRENT DIRECTORY:
init
From 9bbd3b91d54bca1c4166a0fe5090156d1cc14ff0 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Mon, 2 Oct 2023 03:52:52 +0800 Subject: [PATCH 020/315] refactor: adjust xcconfig files --- xcode/xcconfig/Userscripts-Debug.xcconfig | 2 +- xcode/xcconfig/Userscripts-Release.xcconfig | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/xcode/xcconfig/Userscripts-Debug.xcconfig b/xcode/xcconfig/Userscripts-Debug.xcconfig index f7b4cc7f..a0060f16 100644 --- a/xcode/xcconfig/Userscripts-Debug.xcconfig +++ b/xcode/xcconfig/Userscripts-Debug.xcconfig @@ -9,7 +9,7 @@ MAC_APP_VERSION = 1.0.0 // Sign to Run Locally CODE_SIGN_IDENTITY = - // Organization Identifier -ORG_IDENTIFIER = debug +ORG_IDENTIFIER = dev.debug // Application Identifier APP_IDENTIFIER = $(ORG_IDENTIFIER).userscripts // Safari Extension Identifier diff --git a/xcode/xcconfig/Userscripts-Release.xcconfig b/xcode/xcconfig/Userscripts-Release.xcconfig index 516d42eb..3869c42d 100644 --- a/xcode/xcconfig/Userscripts-Release.xcconfig +++ b/xcode/xcconfig/Userscripts-Release.xcconfig @@ -11,12 +11,13 @@ DEVELOPMENT_TEAM = J74Q8V8V8N // Organization Identifier ORG_IDENTIFIER = com.userscripts // Application Identifier -APP_IDENTIFIER = com.userscripts.macos +APP_IDENTIFIER = $(ORG_IDENTIFIER).macos // Safari Extension Identifier EXT_IDENTIFIER = $(APP_IDENTIFIER).Userscripts-Extension // App Groups and Group Containers -IOS_SHARED_GID = group.com.userscripts.ios -MAC_SHARED_GID = $(DEVELOPMENT_TEAM).com.userscripts.macos +// https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_application-groups +IOS_SHARED_GID = group.$(ORG_IDENTIFIER).ios +MAC_SHARED_GID = $(DEVELOPMENT_TEAM).$(ORG_IDENTIFIER).macos // App URL Scheme APP_URL_SCHEME = userscriptsurlscheme From 9c5c95e7a0c3aeaa759ecf15199c2f85aee9c505 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Wed, 4 Oct 2023 09:07:01 +0800 Subject: [PATCH 021/315] build: v4.5.0(81), v1.5.0(54) --- xcode/Safari-Extension/Resources/manifest.json | 2 +- xcode/xcconfig/Userscripts-Release.xcconfig | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/xcode/Safari-Extension/Resources/manifest.json b/xcode/Safari-Extension/Resources/manifest.json index 85e80919..6cddb10c 100644 --- a/xcode/Safari-Extension/Resources/manifest.json +++ b/xcode/Safari-Extension/Resources/manifest.json @@ -4,7 +4,7 @@ "default_locale": "en", "name": "__MSG_extension_name__", "description": "__MSG_extension_description__", - "version": "4.4.4", + "version": "4.5.0", "icons": { "48": "images/icon-48.png", "96": "images/icon-96.png", diff --git a/xcode/xcconfig/Userscripts-Release.xcconfig b/xcode/xcconfig/Userscripts-Release.xcconfig index 3869c42d..19dd52a0 100644 --- a/xcode/xcconfig/Userscripts-Release.xcconfig +++ b/xcode/xcconfig/Userscripts-Release.xcconfig @@ -1,10 +1,10 @@ #include "Userscripts-Base.xcconfig" INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © 2018–2023 Justin Wasack. All rights reserved. -IOS_APP_BUILD = 53 -IOS_APP_VERSION = 1.4.4 -MAC_APP_BUILD = 80 -MAC_APP_VERSION = 4.4.4 +IOS_APP_BUILD = 54 +IOS_APP_VERSION = 1.5.0 +MAC_APP_BUILD = 81 +MAC_APP_VERSION = 4.5.0 // Developer Team ID DEVELOPMENT_TEAM = J74Q8V8V8N From 2e641f43f893afb2ebdc926ffd56f4da559d0f46 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sat, 7 Oct 2023 07:39:35 +0800 Subject: [PATCH 022/315] fix(ios): improve initialization process make sure display document folder (#548) --- xcode/Userscripts.xcodeproj/project.pbxproj | 4 ++ xcode/iOS-App/AppDelegate.swift | 7 +-- xcode/iOS-App/Initialization.swift | 47 +++++++++++++++++++++ xcode/iOS-App/ViewController.swift | 2 +- 4 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 xcode/iOS-App/Initialization.swift diff --git a/xcode/Userscripts.xcodeproj/project.pbxproj b/xcode/Userscripts.xcodeproj/project.pbxproj index 1f7f2371..3a703519 100644 --- a/xcode/Userscripts.xcodeproj/project.pbxproj +++ b/xcode/Userscripts.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 030C64F62AC62CC900548FBD /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 030C64F52AC62CC900548FBD /* Settings.bundle */; }; + 030C64FC2AD0B24C00548FBD /* Initialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030C64FB2AD0B24C00548FBD /* Initialization.swift */; }; 0336619F294DF7C900CFE179 /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0336619E294DF7C900CFE179 /* Functions.swift */; }; 033661A529510B7900CFE179 /* View.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033661A329510B7900CFE179 /* View.storyboard */; }; 0376905B29808EEA00F3474D /* background.js in Resources */ = {isa = PBXBuildFile; fileRef = 0376905A29808EEA00F3474D /* background.js */; }; @@ -106,6 +107,7 @@ /* Begin PBXFileReference section */ 030C64F52AC62CC900548FBD /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; + 030C64FB2AD0B24C00548FBD /* Initialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Initialization.swift; sourceTree = ""; }; 0336619E294DF7C900CFE179 /* Functions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = ""; }; 033661A429510B7900CFE179 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/View.storyboard; sourceTree = ""; }; 0376905A29808EEA00F3474D /* background.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = background.js; sourceTree = ""; }; @@ -210,6 +212,7 @@ 4A4CF6DD270A38BD00111584 /* AppDelegate.swift */, 4A4CF6E7270A38BD00111584 /* SceneDelegate.swift */, 4A4CF6E9270A38BD00111584 /* ViewController.swift */, + 030C64FB2AD0B24C00548FBD /* Initialization.swift */, 4A4CF6EB270A38BD00111584 /* LaunchScreen.storyboard */, 4A4CF6EE270A38BD00111584 /* Main.storyboard */, ); @@ -566,6 +569,7 @@ 4A4CF6EA270A38BD00111584 /* ViewController.swift in Sources */, 4A4CF6DE270A38BD00111584 /* AppDelegate.swift in Sources */, 03C24F992ABD2CBB00F130F9 /* Preferences.swift in Sources */, + 030C64FC2AD0B24C00548FBD /* Initialization.swift in Sources */, 78B25585299D4C27000B2E9B /* Shared.swift in Sources */, 4A4CF6E8270A38BD00111584 /* SceneDelegate.swift in Sources */, ); diff --git a/xcode/iOS-App/AppDelegate.swift b/xcode/iOS-App/AppDelegate.swift index 8bb92da4..1857846c 100644 --- a/xcode/iOS-App/AppDelegate.swift +++ b/xcode/iOS-App/AppDelegate.swift @@ -16,12 +16,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. - let logger = USLogger(#fileID) - // set the scripts directory to the app document on first use - if Preferences.scriptsDirectoryUrl == getDefaultScriptsDirectoryUrl() { - logger?.info("\(#function, privacy: .public) - Initialize default directory") - Preferences.scriptsDirectoryUrl = getDocumentsDirectory() - } + initializeFirstStart() return true } diff --git a/xcode/iOS-App/Initialization.swift b/xcode/iOS-App/Initialization.swift new file mode 100644 index 00000000..394e054e --- /dev/null +++ b/xcode/iOS-App/Initialization.swift @@ -0,0 +1,47 @@ +import Foundation + +private let logger = USLogger(#fileID) + +private func createDemoScript() { + let demoScript = """ +// ==UserScript== +// @name Demo user script +// @description I am a demo user script that you can safely delete (add any files to this folder and I will no longer automatically generate) +// @author Userscripts +// @version 0.0.1 +// @match *://*/* +// @grant none +// @inject-into content +// ==/UserScript== + +(function () { + 'use strict'; + // here is your code +})(); +""" + let url = getDocumentsDirectory() + // get a list of non-hidden files + guard let contents = try? FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: [], options: .skipsHiddenFiles) else { + logger?.error("\(#function, privacy: .public) - failed get contentsOfDirectory") + return + } + if contents.isEmpty { + let fileURL = url.appendingPathComponent("Demo_user_script.user.js") + do { + logger?.info("\(#function, privacy: .public) - try to write demo user script") + try demoScript.write(to: fileURL, atomically: true, encoding: .utf8) + } catch { + logger?.error("\(#function, privacy: .public) - failed to write demo user script") + } + } +} + +func initializeFirstStart() { + // set the scripts directory to the app document on first use + if Preferences.scriptsDirectoryUrl == getDefaultScriptsDirectoryUrl() { + logger?.info("\(#function, privacy: .public) - Initialize default directory") + Preferences.scriptsDirectoryUrl = getDocumentsDirectory() + } + // put a visible file to display the documents directory in files app + createDemoScript() +} diff --git a/xcode/iOS-App/ViewController.swift b/xcode/iOS-App/ViewController.swift index d7734b76..e21c5cbe 100644 --- a/xcode/iOS-App/ViewController.swift +++ b/xcode/iOS-App/ViewController.swift @@ -62,7 +62,7 @@ class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHan logger?.info("\(#function, privacy: .public) - Userscripts iOS has requested to set the readLocation") let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.folder]) documentPicker.delegate = self - documentPicker.directoryURL = Preferences.scriptsDirectoryUrl + documentPicker.directoryURL = getDocumentsDirectory() present(documentPicker, animated: true, completion: nil) } } From 58356ff21d7decd945e62d35801beae9d57aa822 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sat, 7 Oct 2023 07:46:30 +0800 Subject: [PATCH 023/315] build: v4.5.0(81), v1.5.0(55) --- xcode/xcconfig/Userscripts-Release.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xcode/xcconfig/Userscripts-Release.xcconfig b/xcode/xcconfig/Userscripts-Release.xcconfig index 19dd52a0..ce27a26a 100644 --- a/xcode/xcconfig/Userscripts-Release.xcconfig +++ b/xcode/xcconfig/Userscripts-Release.xcconfig @@ -1,7 +1,7 @@ #include "Userscripts-Base.xcconfig" INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © 2018–2023 Justin Wasack. All rights reserved. -IOS_APP_BUILD = 54 +IOS_APP_BUILD = 55 IOS_APP_VERSION = 1.5.0 MAC_APP_BUILD = 81 MAC_APP_VERSION = 4.5.0 From bdfdf085839607d9882ab0c5dfbd48d9279da0b6 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Thu, 12 Oct 2023 17:44:15 +0800 Subject: [PATCH 024/315] refactor(dev): adjust xcode structure --- xcode/{Mac-App => App-Mac}/App.entitlements | 0 xcode/{Mac-App => App-Mac}/AppDelegate.swift | 0 .../AppIcon.appiconset/1024.png | Bin .../AppIcon.appiconset/128.png | Bin .../Assets.xcassets/AppIcon.appiconset/16.png | Bin .../AppIcon.appiconset/256-1.png | Bin .../AppIcon.appiconset/256.png | Bin .../AppIcon.appiconset/32-1.png | Bin .../Assets.xcassets/AppIcon.appiconset/32.png | Bin .../AppIcon.appiconset/512-1.png | Bin .../AppIcon.appiconset/512.png | Bin .../Assets.xcassets/AppIcon.appiconset/64.png | Bin .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../Base.lproj/Main.storyboard | 0 .../Base.lproj/View.storyboard | 0 xcode/{Mac-App => App-Mac}/Functions.swift | 0 xcode/{Mac-App => App-Mac}/Info.plist | 0 .../{Mac-App => App-Mac}/ViewController.swift | 0 xcode/{iOS-App => App-iOS}/App.entitlements | 0 xcode/{iOS-App => App-iOS}/AppDelegate.swift | 0 .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/1024.png | Bin .../AppIcon.appiconset/120-1.png | Bin .../AppIcon.appiconset/120.png | Bin .../AppIcon.appiconset/152.png | Bin .../AppIcon.appiconset/167.png | Bin .../AppIcon.appiconset/180.png | Bin .../Assets.xcassets/AppIcon.appiconset/20.png | Bin .../Assets.xcassets/AppIcon.appiconset/29.png | Bin .../AppIcon.appiconset/40-1.png | Bin .../AppIcon.appiconset/40-2.png | Bin .../Assets.xcassets/AppIcon.appiconset/40.png | Bin .../AppIcon.appiconset/58-1.png | Bin .../Assets.xcassets/AppIcon.appiconset/58.png | Bin .../Assets.xcassets/AppIcon.appiconset/60.png | Bin .../Assets.xcassets/AppIcon.appiconset/76.png | Bin .../AppIcon.appiconset/80-1.png | Bin .../Assets.xcassets/AppIcon.appiconset/80.png | Bin .../Assets.xcassets/AppIcon.appiconset/87.png | Bin .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../LargeIcon.imageset/Contents.json | 0 .../LaunchScreen.imageset/Contents.json | 0 .../LaunchScreen.imageset/LaunchScreen.pdf | Bin .../Base.lproj/LaunchScreen.storyboard | 0 .../{iOS-App => App-iOS}/Base.lproj/Main.html | 0 .../Base.lproj/Main.storyboard | 0 xcode/{iOS-App => App-iOS}/Info.plist | 0 .../{iOS-App => App-iOS}/Initialization.swift | 0 xcode/{iOS-App => App-iOS}/Resources/Icon.png | Bin .../{iOS-App => App-iOS}/SceneDelegate.swift | 0 .../Settings.bundle/Root.plist | 0 .../Settings.bundle/en.lproj/Root.strings | Bin .../{iOS-App => App-iOS}/ViewController.swift | 0 .../Functions.swift | 0 .../Info.plist | 0 .../Mac.entitlements | 0 .../Resources/_locales/en/messages.json | 0 .../Resources/images/icon-128.png | Bin .../Resources/images/icon-256.png | Bin .../Resources/images/icon-48.png | Bin .../Resources/images/icon-512.png | Bin .../Resources/images/icon-96.png | Bin .../Resources/images/toolbar-icon-16.png | Bin .../Resources/images/toolbar-icon-32.png | Bin .../Resources/manifest.json | 6 +- .../SafariWebExtensionHandler.swift | 0 .../iOS.entitlements | 0 .../Safari-Extension/Resources/background.js | 509 ------------------ xcode/Safari-Extension/Resources/content.js | 469 ---------------- .../Shared/{Shared.swift => Utilities.swift} | 0 .../UserscriptsTests.swift | 0 xcode/Userscripts.xcodeproj/project.pbxproj | 68 ++- xcode/xcconfig/Mac-App.xcconfig | 6 +- xcode/xcconfig/Mac-Safari-Extension.xcconfig | 6 +- xcode/xcconfig/Mac-Tests.xcconfig | 2 +- xcode/xcconfig/Userscripts-Base.xcconfig | 2 +- xcode/xcconfig/Userscripts-Debug.xcconfig | 2 +- xcode/xcconfig/iOS-App.xcconfig | 6 +- xcode/xcconfig/iOS-Safari-Extension.xcconfig | 6 +- 81 files changed, 50 insertions(+), 1032 deletions(-) rename xcode/{Mac-App => App-Mac}/App.entitlements (100%) rename xcode/{Mac-App => App-Mac}/AppDelegate.swift (100%) rename xcode/{Mac-App => App-Mac}/Assets.xcassets/AppIcon.appiconset/1024.png (100%) rename xcode/{Mac-App => App-Mac}/Assets.xcassets/AppIcon.appiconset/128.png (100%) rename xcode/{Mac-App => App-Mac}/Assets.xcassets/AppIcon.appiconset/16.png (100%) rename xcode/{Mac-App => App-Mac}/Assets.xcassets/AppIcon.appiconset/256-1.png (100%) rename xcode/{Mac-App => App-Mac}/Assets.xcassets/AppIcon.appiconset/256.png (100%) rename xcode/{Mac-App => App-Mac}/Assets.xcassets/AppIcon.appiconset/32-1.png (100%) rename xcode/{Mac-App => App-Mac}/Assets.xcassets/AppIcon.appiconset/32.png (100%) rename xcode/{Mac-App => App-Mac}/Assets.xcassets/AppIcon.appiconset/512-1.png (100%) rename xcode/{Mac-App => App-Mac}/Assets.xcassets/AppIcon.appiconset/512.png (100%) rename xcode/{Mac-App => App-Mac}/Assets.xcassets/AppIcon.appiconset/64.png (100%) rename xcode/{Mac-App => App-Mac}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename xcode/{Mac-App => App-Mac}/Assets.xcassets/Contents.json (100%) rename xcode/{Mac-App => App-Mac}/Base.lproj/Main.storyboard (100%) rename xcode/{Mac-App => App-Mac}/Base.lproj/View.storyboard (100%) rename xcode/{Mac-App => App-Mac}/Functions.swift (100%) rename xcode/{Mac-App => App-Mac}/Info.plist (100%) rename xcode/{Mac-App => App-Mac}/ViewController.swift (100%) rename xcode/{iOS-App => App-iOS}/App.entitlements (100%) rename xcode/{iOS-App => App-iOS}/AppDelegate.swift (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/AppIcon.appiconset/1024.png (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/AppIcon.appiconset/120-1.png (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/AppIcon.appiconset/120.png (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/AppIcon.appiconset/152.png (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/AppIcon.appiconset/167.png (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/AppIcon.appiconset/180.png (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/AppIcon.appiconset/20.png (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/AppIcon.appiconset/29.png (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/AppIcon.appiconset/40-1.png (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/AppIcon.appiconset/40-2.png (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/AppIcon.appiconset/40.png (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/AppIcon.appiconset/58-1.png (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/AppIcon.appiconset/58.png (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/AppIcon.appiconset/60.png (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/AppIcon.appiconset/76.png (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/AppIcon.appiconset/80-1.png (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/AppIcon.appiconset/80.png (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/AppIcon.appiconset/87.png (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/Contents.json (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/LargeIcon.imageset/Contents.json (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/LaunchScreen.imageset/Contents.json (100%) rename xcode/{iOS-App => App-iOS}/Assets.xcassets/LaunchScreen.imageset/LaunchScreen.pdf (100%) rename xcode/{iOS-App => App-iOS}/Base.lproj/LaunchScreen.storyboard (100%) rename xcode/{iOS-App => App-iOS}/Base.lproj/Main.html (100%) rename xcode/{iOS-App => App-iOS}/Base.lproj/Main.storyboard (100%) rename xcode/{iOS-App => App-iOS}/Info.plist (100%) rename xcode/{iOS-App => App-iOS}/Initialization.swift (100%) rename xcode/{iOS-App => App-iOS}/Resources/Icon.png (100%) rename xcode/{iOS-App => App-iOS}/SceneDelegate.swift (100%) rename xcode/{iOS-App => App-iOS}/Settings.bundle/Root.plist (100%) rename xcode/{iOS-App => App-iOS}/Settings.bundle/en.lproj/Root.strings (100%) rename xcode/{iOS-App => App-iOS}/ViewController.swift (100%) rename xcode/{Safari-Extension => Ext-Safari}/Functions.swift (100%) rename xcode/{Safari-Extension => Ext-Safari}/Info.plist (100%) rename xcode/{Safari-Extension => Ext-Safari}/Mac.entitlements (100%) rename xcode/{Safari-Extension => Ext-Safari}/Resources/_locales/en/messages.json (100%) rename xcode/{Safari-Extension => Ext-Safari}/Resources/images/icon-128.png (100%) rename xcode/{Safari-Extension => Ext-Safari}/Resources/images/icon-256.png (100%) rename xcode/{Safari-Extension => Ext-Safari}/Resources/images/icon-48.png (100%) rename xcode/{Safari-Extension => Ext-Safari}/Resources/images/icon-512.png (100%) rename xcode/{Safari-Extension => Ext-Safari}/Resources/images/icon-96.png (100%) rename xcode/{Safari-Extension => Ext-Safari}/Resources/images/toolbar-icon-16.png (100%) rename xcode/{Safari-Extension => Ext-Safari}/Resources/images/toolbar-icon-32.png (100%) rename xcode/{Safari-Extension => Ext-Safari}/Resources/manifest.json (88%) rename xcode/{Safari-Extension => Ext-Safari}/SafariWebExtensionHandler.swift (100%) rename xcode/{Safari-Extension => Ext-Safari}/iOS.entitlements (100%) delete mode 100644 xcode/Safari-Extension/Resources/background.js delete mode 100644 xcode/Safari-Extension/Resources/content.js rename xcode/Shared/{Shared.swift => Utilities.swift} (100%) rename xcode/{Mac-Tests => Tests-Mac}/UserscriptsTests.swift (100%) diff --git a/xcode/Mac-App/App.entitlements b/xcode/App-Mac/App.entitlements similarity index 100% rename from xcode/Mac-App/App.entitlements rename to xcode/App-Mac/App.entitlements diff --git a/xcode/Mac-App/AppDelegate.swift b/xcode/App-Mac/AppDelegate.swift similarity index 100% rename from xcode/Mac-App/AppDelegate.swift rename to xcode/App-Mac/AppDelegate.swift diff --git a/xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/1024.png b/xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/1024.png similarity index 100% rename from xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/1024.png rename to xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/1024.png diff --git a/xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/128.png b/xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/128.png similarity index 100% rename from xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/128.png rename to xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/128.png diff --git a/xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/16.png b/xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/16.png similarity index 100% rename from xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/16.png rename to xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/16.png diff --git a/xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/256-1.png b/xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/256-1.png similarity index 100% rename from xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/256-1.png rename to xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/256-1.png diff --git a/xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/256.png b/xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/256.png similarity index 100% rename from xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/256.png rename to xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/256.png diff --git a/xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/32-1.png b/xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/32-1.png similarity index 100% rename from xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/32-1.png rename to xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/32-1.png diff --git a/xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/32.png b/xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/32.png similarity index 100% rename from xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/32.png rename to xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/32.png diff --git a/xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/512-1.png b/xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/512-1.png similarity index 100% rename from xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/512-1.png rename to xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/512-1.png diff --git a/xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/512.png b/xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/512.png similarity index 100% rename from xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/512.png rename to xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/512.png diff --git a/xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/64.png b/xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/64.png similarity index 100% rename from xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/64.png rename to xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/64.png diff --git a/xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/Contents.json b/xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/Contents.json rename to xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/xcode/Mac-App/Assets.xcassets/Contents.json b/xcode/App-Mac/Assets.xcassets/Contents.json similarity index 100% rename from xcode/Mac-App/Assets.xcassets/Contents.json rename to xcode/App-Mac/Assets.xcassets/Contents.json diff --git a/xcode/Mac-App/Base.lproj/Main.storyboard b/xcode/App-Mac/Base.lproj/Main.storyboard similarity index 100% rename from xcode/Mac-App/Base.lproj/Main.storyboard rename to xcode/App-Mac/Base.lproj/Main.storyboard diff --git a/xcode/Mac-App/Base.lproj/View.storyboard b/xcode/App-Mac/Base.lproj/View.storyboard similarity index 100% rename from xcode/Mac-App/Base.lproj/View.storyboard rename to xcode/App-Mac/Base.lproj/View.storyboard diff --git a/xcode/Mac-App/Functions.swift b/xcode/App-Mac/Functions.swift similarity index 100% rename from xcode/Mac-App/Functions.swift rename to xcode/App-Mac/Functions.swift diff --git a/xcode/Mac-App/Info.plist b/xcode/App-Mac/Info.plist similarity index 100% rename from xcode/Mac-App/Info.plist rename to xcode/App-Mac/Info.plist diff --git a/xcode/Mac-App/ViewController.swift b/xcode/App-Mac/ViewController.swift similarity index 100% rename from xcode/Mac-App/ViewController.swift rename to xcode/App-Mac/ViewController.swift diff --git a/xcode/iOS-App/App.entitlements b/xcode/App-iOS/App.entitlements similarity index 100% rename from xcode/iOS-App/App.entitlements rename to xcode/App-iOS/App.entitlements diff --git a/xcode/iOS-App/AppDelegate.swift b/xcode/App-iOS/AppDelegate.swift similarity index 100% rename from xcode/iOS-App/AppDelegate.swift rename to xcode/App-iOS/AppDelegate.swift diff --git a/xcode/iOS-App/Assets.xcassets/AccentColor.colorset/Contents.json b/xcode/App-iOS/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AccentColor.colorset/Contents.json rename to xcode/App-iOS/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/1024.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/1024.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/1024.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/1024.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/120-1.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/120-1.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/120-1.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/120-1.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/120.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/120.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/120.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/120.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/152.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/152.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/152.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/152.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/167.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/167.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/167.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/167.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/180.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/180.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/180.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/180.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/20.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/20.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/20.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/20.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/29.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/29.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/29.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/29.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/40-1.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/40-1.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/40-1.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/40-1.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/40-2.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/40-2.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/40-2.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/40-2.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/40.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/40.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/40.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/40.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/58-1.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/58-1.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/58-1.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/58-1.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/58.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/58.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/58.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/58.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/60.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/60.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/60.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/60.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/76.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/76.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/76.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/76.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/80-1.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/80-1.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/80-1.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/80-1.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/80.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/80.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/80.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/80.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/87.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/87.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/87.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/87.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/Contents.json b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/Contents.json rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/xcode/iOS-App/Assets.xcassets/Contents.json b/xcode/App-iOS/Assets.xcassets/Contents.json similarity index 100% rename from xcode/iOS-App/Assets.xcassets/Contents.json rename to xcode/App-iOS/Assets.xcassets/Contents.json diff --git a/xcode/iOS-App/Assets.xcassets/LargeIcon.imageset/Contents.json b/xcode/App-iOS/Assets.xcassets/LargeIcon.imageset/Contents.json similarity index 100% rename from xcode/iOS-App/Assets.xcassets/LargeIcon.imageset/Contents.json rename to xcode/App-iOS/Assets.xcassets/LargeIcon.imageset/Contents.json diff --git a/xcode/iOS-App/Assets.xcassets/LaunchScreen.imageset/Contents.json b/xcode/App-iOS/Assets.xcassets/LaunchScreen.imageset/Contents.json similarity index 100% rename from xcode/iOS-App/Assets.xcassets/LaunchScreen.imageset/Contents.json rename to xcode/App-iOS/Assets.xcassets/LaunchScreen.imageset/Contents.json diff --git a/xcode/iOS-App/Assets.xcassets/LaunchScreen.imageset/LaunchScreen.pdf b/xcode/App-iOS/Assets.xcassets/LaunchScreen.imageset/LaunchScreen.pdf similarity index 100% rename from xcode/iOS-App/Assets.xcassets/LaunchScreen.imageset/LaunchScreen.pdf rename to xcode/App-iOS/Assets.xcassets/LaunchScreen.imageset/LaunchScreen.pdf diff --git a/xcode/iOS-App/Base.lproj/LaunchScreen.storyboard b/xcode/App-iOS/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from xcode/iOS-App/Base.lproj/LaunchScreen.storyboard rename to xcode/App-iOS/Base.lproj/LaunchScreen.storyboard diff --git a/xcode/iOS-App/Base.lproj/Main.html b/xcode/App-iOS/Base.lproj/Main.html similarity index 100% rename from xcode/iOS-App/Base.lproj/Main.html rename to xcode/App-iOS/Base.lproj/Main.html diff --git a/xcode/iOS-App/Base.lproj/Main.storyboard b/xcode/App-iOS/Base.lproj/Main.storyboard similarity index 100% rename from xcode/iOS-App/Base.lproj/Main.storyboard rename to xcode/App-iOS/Base.lproj/Main.storyboard diff --git a/xcode/iOS-App/Info.plist b/xcode/App-iOS/Info.plist similarity index 100% rename from xcode/iOS-App/Info.plist rename to xcode/App-iOS/Info.plist diff --git a/xcode/iOS-App/Initialization.swift b/xcode/App-iOS/Initialization.swift similarity index 100% rename from xcode/iOS-App/Initialization.swift rename to xcode/App-iOS/Initialization.swift diff --git a/xcode/iOS-App/Resources/Icon.png b/xcode/App-iOS/Resources/Icon.png similarity index 100% rename from xcode/iOS-App/Resources/Icon.png rename to xcode/App-iOS/Resources/Icon.png diff --git a/xcode/iOS-App/SceneDelegate.swift b/xcode/App-iOS/SceneDelegate.swift similarity index 100% rename from xcode/iOS-App/SceneDelegate.swift rename to xcode/App-iOS/SceneDelegate.swift diff --git a/xcode/iOS-App/Settings.bundle/Root.plist b/xcode/App-iOS/Settings.bundle/Root.plist similarity index 100% rename from xcode/iOS-App/Settings.bundle/Root.plist rename to xcode/App-iOS/Settings.bundle/Root.plist diff --git a/xcode/iOS-App/Settings.bundle/en.lproj/Root.strings b/xcode/App-iOS/Settings.bundle/en.lproj/Root.strings similarity index 100% rename from xcode/iOS-App/Settings.bundle/en.lproj/Root.strings rename to xcode/App-iOS/Settings.bundle/en.lproj/Root.strings diff --git a/xcode/iOS-App/ViewController.swift b/xcode/App-iOS/ViewController.swift similarity index 100% rename from xcode/iOS-App/ViewController.swift rename to xcode/App-iOS/ViewController.swift diff --git a/xcode/Safari-Extension/Functions.swift b/xcode/Ext-Safari/Functions.swift similarity index 100% rename from xcode/Safari-Extension/Functions.swift rename to xcode/Ext-Safari/Functions.swift diff --git a/xcode/Safari-Extension/Info.plist b/xcode/Ext-Safari/Info.plist similarity index 100% rename from xcode/Safari-Extension/Info.plist rename to xcode/Ext-Safari/Info.plist diff --git a/xcode/Safari-Extension/Mac.entitlements b/xcode/Ext-Safari/Mac.entitlements similarity index 100% rename from xcode/Safari-Extension/Mac.entitlements rename to xcode/Ext-Safari/Mac.entitlements diff --git a/xcode/Safari-Extension/Resources/_locales/en/messages.json b/xcode/Ext-Safari/Resources/_locales/en/messages.json similarity index 100% rename from xcode/Safari-Extension/Resources/_locales/en/messages.json rename to xcode/Ext-Safari/Resources/_locales/en/messages.json diff --git a/xcode/Safari-Extension/Resources/images/icon-128.png b/xcode/Ext-Safari/Resources/images/icon-128.png similarity index 100% rename from xcode/Safari-Extension/Resources/images/icon-128.png rename to xcode/Ext-Safari/Resources/images/icon-128.png diff --git a/xcode/Safari-Extension/Resources/images/icon-256.png b/xcode/Ext-Safari/Resources/images/icon-256.png similarity index 100% rename from xcode/Safari-Extension/Resources/images/icon-256.png rename to xcode/Ext-Safari/Resources/images/icon-256.png diff --git a/xcode/Safari-Extension/Resources/images/icon-48.png b/xcode/Ext-Safari/Resources/images/icon-48.png similarity index 100% rename from xcode/Safari-Extension/Resources/images/icon-48.png rename to xcode/Ext-Safari/Resources/images/icon-48.png diff --git a/xcode/Safari-Extension/Resources/images/icon-512.png b/xcode/Ext-Safari/Resources/images/icon-512.png similarity index 100% rename from xcode/Safari-Extension/Resources/images/icon-512.png rename to xcode/Ext-Safari/Resources/images/icon-512.png diff --git a/xcode/Safari-Extension/Resources/images/icon-96.png b/xcode/Ext-Safari/Resources/images/icon-96.png similarity index 100% rename from xcode/Safari-Extension/Resources/images/icon-96.png rename to xcode/Ext-Safari/Resources/images/icon-96.png diff --git a/xcode/Safari-Extension/Resources/images/toolbar-icon-16.png b/xcode/Ext-Safari/Resources/images/toolbar-icon-16.png similarity index 100% rename from xcode/Safari-Extension/Resources/images/toolbar-icon-16.png rename to xcode/Ext-Safari/Resources/images/toolbar-icon-16.png diff --git a/xcode/Safari-Extension/Resources/images/toolbar-icon-32.png b/xcode/Ext-Safari/Resources/images/toolbar-icon-32.png similarity index 100% rename from xcode/Safari-Extension/Resources/images/toolbar-icon-32.png rename to xcode/Ext-Safari/Resources/images/toolbar-icon-32.png diff --git a/xcode/Safari-Extension/Resources/manifest.json b/xcode/Ext-Safari/Resources/manifest.json similarity index 88% rename from xcode/Safari-Extension/Resources/manifest.json rename to xcode/Ext-Safari/Resources/manifest.json index 6cddb10c..63a425d1 100644 --- a/xcode/Safari-Extension/Resources/manifest.json +++ b/xcode/Ext-Safari/Resources/manifest.json @@ -13,11 +13,11 @@ "512": "images/icon-512.png" }, "background": { - "scripts": ["background.js"], + "scripts": ["dist/background.js"], "persistent": false }, "browser_action": { - "default_popup": "dist/entry-popup.html", + "default_popup": "dist/entry-ext-action-popup.html", "default_icon": { "16": "images/toolbar-icon-16.png", "32": "images/toolbar-icon-32.png" @@ -25,7 +25,7 @@ }, "content_scripts": [ { - "js": ["content.js"], + "js": ["dist/content.js"], "matches": [""], "run_at": "document_start", "all_frames": true diff --git a/xcode/Safari-Extension/SafariWebExtensionHandler.swift b/xcode/Ext-Safari/SafariWebExtensionHandler.swift similarity index 100% rename from xcode/Safari-Extension/SafariWebExtensionHandler.swift rename to xcode/Ext-Safari/SafariWebExtensionHandler.swift diff --git a/xcode/Safari-Extension/iOS.entitlements b/xcode/Ext-Safari/iOS.entitlements similarity index 100% rename from xcode/Safari-Extension/iOS.entitlements rename to xcode/Ext-Safari/iOS.entitlements diff --git a/xcode/Safari-Extension/Resources/background.js b/xcode/Safari-Extension/Resources/background.js deleted file mode 100644 index 5e0a8feb..00000000 --- a/xcode/Safari-Extension/Resources/background.js +++ /dev/null @@ -1,509 +0,0 @@ -// functions from "src/shared/utils.js"; -async function openExtensionPage() { - const url = browser.runtime.getURL("dist/entry-page.html"); - const tabs = await browser.tabs.query({url}); - const tab = tabs.find(e => e.url.startsWith(url)); - if (!tab) return browser.tabs.create({url}); - browser.tabs.update(tab.id, {active: true}); - browser.windows.update(tab.windowId, {focused: true}); -} - -// functions from "src/shared/settings.js"; - -// first sorts files by run-at value, then by weight value -function userscriptSort(a, b) { - // map the run-at values to numeric values - const runAtValues = { - "document-start": 1, - "document-end": 2, - "document-idle": 3 - }; - const runAtA = a.scriptObject["run-at"]; - const runAtB = b.scriptObject["run-at"]; - if (runAtA !== runAtB && runAtValues[runAtA] && runAtValues[runAtB]) { - return runAtValues[runAtA] > runAtValues[runAtB]; - } - return Number(a.scriptObject.weight) < Number(b.scriptObject.weight); -} - -async function readAsDataURL(blob) { - return new Promise(resolve => { - const reader = new FileReader(); - reader.readAsDataURL(blob); - reader.onloadend = () => resolve(reader.result); // base64data - }); -} - -async function getPlatform() { - let platform = localStorage.getItem("platform"); - if (!platform) { - const message = {name: "REQ_PLATFORM"}; - const response = await browser.runtime.sendNativeMessage(message); - if (!response.platform) { - console.error("Failed to get platform"); - return ""; - } - platform = response.platform; - localStorage.setItem("platform", platform); - } - return platform; -} - -function setClipboard(data, type = "text/plain") { - // future enhancement? - // https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/write - // https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText - const onCopy = e => { - e.stopImmediatePropagation(); - e.preventDefault(); - e.clipboardData.setData(type, data); - document.removeEventListener("copy", onCopy, true); - }; - - const textarea = document.createElement("textarea"); - textarea.textContent = ""; - document.body.appendChild(textarea); - textarea.select(); - document.addEventListener("copy", onCopy, true); - try { - return document.execCommand("copy"); - } catch (error) { - console.warn("setClipboard failed", error); - document.removeEventListener("copy", onCopy, true); - return false; - } finally { - document.body.removeChild(textarea); - } -} - -async function setBadgeCount() { - const clearBadge = () => browser.browserAction.setBadgeText({text: ""}); - // @todo until better introduce in ios, only set badge on macOS - const platform = await getPlatform(); - if (platform !== "macos") return clearBadge(); - // @todo after the background script is modularized, import and use: - // settingsStorage.get(["global_active","toolbar_badge_count","global_exclude_match"]) - const results = await browser.storage.local.get(["US_GLOBAL_ACTIVE", "US_TOOLBAR_BADGE_COUNT"]); - if (results?.US_GLOBAL_ACTIVE === false) return clearBadge(); - if (results?.US_TOOLBAR_BADGE_COUNT === false) return clearBadge(); - - const currentTab = await browser.tabs.getCurrent(); - // no active tabs exist (user closed all windows) - if (!currentTab) return clearBadge(); - const url = currentTab.url; - // if url doesn't exist, stop - if (!url) return clearBadge(); - // only check for http/s pages - if (!url.startsWith("http://") && !url.startsWith("https://")) return clearBadge(); - // @todo if url match in global exclude list, clear badge - const frameUrls = new Set(); - const frames = await browser.webNavigation.getAllFrames({tabId: currentTab.id}); - for (let i = 0; i < frames.length; i++) { - const frameUrl = frames[i].url; - if (frameUrl !== url && frameUrl.startsWith("http")) { - frameUrls.add(frameUrl); - } - } - const message = { - name: "POPUP_BADGE_COUNT", - url, - frameUrls: Array.from(frameUrls) - }; - browser.runtime.sendNativeMessage(message, response => { - if (response.error) return console.error(response.error); - const count = response.count; - if (count > 0) { - browser.browserAction.setBadgeText({text: count.toString()}); - } else { - const _url = new URL(url); - if (_url.pathname.endsWith(".user.js")) { - browser.browserAction.setBadgeText({text: "JS"}); - } else { - clearBadge(); - } - } - }); -} - -// on startup get declarativeNetRequests -// and set the requests for the session -// should also check and refresh when: -// 1. dnr item save event in the page occurs -// 2. dnr item toggle event in the page occurs -// 3. external editor changes script file content -async function setSessionRules() { - // not supported below safari 15.4 - if (!browser.declarativeNetRequest.updateSessionRules) return; - await clearAllSessionRules(); - const message = {name: "REQ_REQUESTS"}; - const response = await browser.runtime.sendNativeMessage(message); - if (response.error) { - console.error(response.error); - return; - } - // there are no rules to apply - if (!response.length) return; - // loop through response, parse the rules, push to array and log - const rules = []; - for (let i = 0; i < response.length; i++) { - const rule = response[i]; - const code = JSON.parse(rule.code); - // check if an array or single rule - if (Array.isArray(code)) { - code.forEach(r => rules.push(r)); - console.info(`Setting session rule: ${rule.name} (${code.length})`); - } else { - rules.push(code); - console.info(`Setting session rule: ${rule.name}`); - } - } - // generate unique ids for all rules to ensure no repeats - const ids = randomNumberSet(1000, rules.length); - rules.map((rule, index) => rule.id = ids[index]); - try { - await browser.declarativeNetRequest.updateSessionRules({addRules: rules}); - } catch (error) { - console.error(`Error setting session rules: ${error}`); - return; - } - console.info(`Finished setting ${rules.length} session rules`); -} - -async function clearAllSessionRules() { - const rules = await browser.declarativeNetRequest.getSessionRules(); - if (!rules.length) return; - console.info(`Clearing ${rules.length} session rules`); - const ruleIds = rules.map(a => a.id); - await browser.declarativeNetRequest.updateSessionRules({ - removeRuleIds: ruleIds - }); -} - -function randomNumberSet(max, count) { - // generates a set of random unique numbers - // returns an array - const numbers = new Set(); - while (numbers.size < count) { - numbers.add((Math.floor(Math.random() * (max - 1 + 1)) + 1)); - } - return [...numbers]; -} - -// the current update logic is similar to setSessionRules() -// this feature needs a more detailed redesign in the future -// https://github.com/quoid/userscripts/issues/453 -async function getContextMenuItems() { - // macos exclusive feature - const platform = await getPlatform(); - if (platform !== "macos") return; - // since it's not possible to get a list of currently active menu items - // on update, all context-menu items are cleared, then re-added - // this is done to ensure fresh code changes appear - await browser.menus.removeAll(); - // get the context-menu scripts - const message = {name: "REQ_CONTEXT_MENU_SCRIPTS"}; - const response = await browser.runtime.sendNativeMessage(message); - if (response.error) { - console.error(response.error); - return; - } - // add menus items - const items = response.files?.menu || []; - if (items.length) { - console.info(`Setting ${items.length} context-menu userscripts`); - } - for (let i = 0; i < items.length; i++) { - const item = items[i]; - // context-menu scripts require @match value - // @include values are ignored - if (!item.scriptObject.matches.length) continue; - addContextMenuItem(item); - } -} - -async function addContextMenuItem(userscript) { - // context-menu items persist for a session - // to avoid duplication, when created, save the filename to session storage - const savedItems = sessionStorage.getItem("menu"); - // if the session storage key doesn't exist use empty array - const activeItems = savedItems ? JSON.parse(savedItems) : []; - if (activeItems.indexOf(userscript.scriptObject.filename) !== -1) { - // if already saved, remove it, to get fresh code changes - await browser.menus.remove(userscript.scriptObject.filename); - } - // potential bug? https://developer.apple.com/forums/thread/685273 - // https://stackoverflow.com/q/68431201 - // parse through match values and change pathnames to deal with bug - const patterns = userscript.scriptObject.matches; - patterns.forEach((pattern, index) => { - try { - const url = new URL(pattern); - let pathname = url.pathname; - if (pathname.length > 1 && pathname.endsWith("/")) { - pathname = pathname.slice(0, -1); - } - patterns[index] = `${url.protocol}//${url.hostname}${pathname}`; - } catch (error) { - // prevent breaking when non-url pattern present - } - }); - - browser.menus.create({ - contexts: ["all"], - documentUrlPatterns: patterns, - id: userscript.scriptObject.filename, - title: userscript.scriptObject.name - }, () => { - // add event listener if needed - if (!browser.menus.onClicked.hasListener(contextClick)) { - browser.menus.onClicked.addListener(contextClick); - } - // save the context-menu item reference to sessionStorage - const value = JSON.stringify([userscript.scriptObject.filename]); - sessionStorage.setItem("menu", value); - }); -} - -function contextClick(info, tab) { - // when any created context-menu item is clicked, send message to tab - // the content script for that tag will have the context-menu code - // which will get send back in the response if/when found - const message = {name: "CONTEXT_RUN", menuItemId: info.menuItemId}; - browser.tabs.sendMessage(tab.id, message, response => { - // if code is returned, execute on that tab - if (!response.code) return; - browser.tabs.executeScript(tab.id, { - code: response.code - }); - }); -} - -async function nativeChecks() { - const response = await browser.runtime.sendNativeMessage({name: "NATIVE_CHECKS"}); - // note: use settings.js once background page modularization - if (response.error) { - browser.storage.local.set({US_ERROR_NATIVE: response}); - return false; - } - browser.storage.local.remove("US_ERROR_NATIVE"); - return true; -} - -// handles messages sent with browser.runtime.sendMessage -function handleMessage(request, sender, sendResponse) { - switch (request.name) { - case "REQ_USERSCRIPTS": { - // get the page url from the content script that sent request - const url = sender.url; - // use frameId to determine if request came from top level window - // if @noframes true, and isTop false, swift layer won't return code - const isTop = sender.frameId === 0; - // send request to swift layer to provide code for page url - const message = {name: "REQ_USERSCRIPTS", url, isTop}; - browser.runtime.sendNativeMessage(message, response => { - // if request failed, send error to content script for logging - if (response.error) return sendResponse(response); - // sort files - response.files.js.sort(userscriptSort); - response.files.css.sort((a, b) => { - return Number(a.weight) < Number(b.weight); - }); - // return sorted files for injection - sendResponse(response); - }); - return true; - } - case "API_CLOSE_TAB": { - const tabId = request.tabId || sender.tab.id; - browser.tabs.remove(tabId, () => sendResponse({success: 1})); - return true; - } - case "API_OPEN_TAB": { - const props = { - active: request.active, - index: sender.tab.index + 1, - url: request.url - }; - browser.tabs.create(props, response => sendResponse(response)); - return true; - } - case "API_ADD_STYLE": { - const tabId = sender.tab.id; - const details = {code: request.css, cssOrigin: "user"}; - browser.tabs.insertCSS(tabId, details, () => { - sendResponse(request.css); - }); - return true; - } - case "API_GET_TAB": { - let tab = null; - if (typeof sender.tab !== "undefined") { - const tabData = sessionStorage.getItem(`tab-${sender.tab.id}`); - try { - // if tabData is null, can still parse it and return that - tab = JSON.parse(tabData); - } catch (error) { - console.error("failed to parse tab data for getTab"); - } - } else { - console.error("unable to deliver tab due to empty tab id"); - } - sendResponse(tab == null ? {} : tab); - break; - } - case "API_SAVE_TAB": { - if (sender.tab != null && sender.tab.id) { - const key = `tab-${sender.tab.id}`; - sessionStorage.setItem(key, JSON.stringify(request.tab)); - sendResponse({success: true}); - } else { - console.error("unable to save tab, empty tab id"); - sendResponse({success: false}); - } - break; - } - case "API_SET_CLIPBOARD": { - const result = setClipboard(request.clipboardData, request.type); - sendResponse(result); - break; - } - case "API_XHR": { - // parse details and set up for XMLHttpRequest - const details = request.details; - const method = details.method ? details.method : "GET"; - const user = details.user || null; - const password = details.password || null; - let body = details.data || null; - if (body != null && details.binary != null) { - const len = body.length; - const arr = new Uint8Array(len); - for (let i = 0; i < len; i++) { - arr[i] = body.charCodeAt(i); - } - body = new Blob([arr], {type: "text/plain"}); - } - // establish a long-lived port connection to content script - const port = browser.tabs.connect(sender.tab.id, { - name: request.xhrPortName - }); - // set up XMLHttpRequest - const xhr = new XMLHttpRequest(); - xhr.withCredentials = (details.user && details.password); - xhr.timeout = details.timeout || 0; - if (details.overrideMimeType) { - xhr.overrideMimeType(details.overrideMimeType); - } - // add required listeners and send result back to the content script - for (const e of request.events) { - if (!details[e]) continue; - xhr[e] = async event => { - // can not send xhr through postMessage - // construct new object to be sent as "response" - const x = { - readyState: xhr.readyState, - response: xhr.response, - responseHeaders: xhr.getAllResponseHeaders(), - responseType: xhr.responseType, - responseURL: xhr.responseURL, - status: xhr.status, - statusText: xhr.statusText, - timeout: xhr.timeout, - withCredentials: xhr.withCredentials - }; - // only include responseText when needed - if (["", "text"].indexOf(xhr.responseType) !== -1) { - x.responseText = xhr.responseText; - } - // need to convert arraybuffer data to postMessage - if (xhr.responseType === "arraybuffer") { - const arr = Array.from(new Uint8Array(xhr.response)); - x.response = arr; - } - // need to blob arraybuffer data to postMessage - if (xhr.responseType === "blob") { - const base64data = await readAsDataURL(xhr.response); - x.response = { - data: base64data, - type: xhr.responseType - }; - } - port.postMessage({name: e, event, response: x}); - }; - } - xhr.open(method, details.url, true, user, password); - xhr.responseType = details.responseType || ""; - if (details.headers) { - for (const key in details.headers) { - if (!key.startsWith("Proxy-") && !key.startsWith("Sec-")) { - const val = details.headers[key]; - xhr.setRequestHeader(key, val); - } - } - } - // receive messages from content script and process them - port.onMessage.addListener(msg => { - if (msg.name === "ABORT") xhr.abort(); - if (msg.name === "DISCONNECT") port.disconnect(); - }); - // handle port disconnect and clean tasks - port.onDisconnect.addListener(p => { - if (p?.error) { - console.error(`port disconnected due to an error: ${p.error.message}`); - } - }); - xhr.send(body); - // if onloadend not set in xhr details - // onloadend event won't be passed to content script - // if that happens port DISCONNECT message won't be posted - // if details lacks onloadend attach listener - if (!details.onloadend) { - xhr.onloadend = event => { - port.postMessage({name: "onloadend", event}); - }; - } - break; - } - case "REFRESH_SESSION_RULES": { - setSessionRules(); - break; - } - case "REFRESH_CONTEXT_MENU_SCRIPTS": { - getContextMenuItems(); - break; - } - } -} -browser.runtime.onInstalled.addListener(async () => { - nativeChecks(); -}); -browser.runtime.onStartup.addListener(async () => { - setSessionRules(); - getContextMenuItems(); -}); -// listens for messages from content script, popup and page -browser.runtime.onMessage.addListener(handleMessage); -// set the badge count -browser.tabs.onActivated.addListener(setBadgeCount); -browser.windows.onFocusChanged.addListener(async windowId => { - if (windowId < 1) { // lose focus - return; - } - nativeChecks(); - setBadgeCount(); - setSessionRules(); - getContextMenuItems(); -}); -browser.webNavigation.onCompleted.addListener(setBadgeCount); - -// handle native app messages -const port = browser.runtime.connectNative(); -port.onMessage.addListener(message => { - // console.info(message); // DEBUG - if (message.name === "SAVE_LOCATION_CHANGED") { - openExtensionPage(); - if (message?.userInfo?.returnApp === true) browser.runtime.sendNativeMessage({name: "OPEN_APP"}); - } - // if (message.name === "OPEN_EXTENSION_PAGE") { - // openExtensionPage(); - // } -}); diff --git a/xcode/Safari-Extension/Resources/content.js b/xcode/Safari-Extension/Resources/content.js deleted file mode 100644 index 2060a321..00000000 --- a/xcode/Safari-Extension/Resources/content.js +++ /dev/null @@ -1,469 +0,0 @@ -// code received from background page will be stored in this variable -// code referenced again when strict CSPs block initial injection attempt -let data; -// determines whether strict csp injection has already run (JS only) -let cspFallbackAttempted = false; -// save reference to window's browser object -const browser = window.browser; -// GM APIs -const apis = { - closeTab(tabId) { - return new Promise(resolve => { - const message = { - name: "API_CLOSE_TAB", - tabId - }; - browser.runtime.sendMessage(message, response => resolve(response)); - }); - }, - openInTab(url, openInBackground = false) { - if (!url) return console.error("openInTab missing url arg"); - return new Promise(resolve => { - const message = { - name: "API_OPEN_TAB", - url, - active: !openInBackground - }; - browser.runtime.sendMessage(message, response => resolve(response)); - }); - }, - setValue(key, value) { - if (typeof key !== "string" || !key.length) { - return console.error("setValue invalid key arg"); - } - if (value == null) { - return console.error("setValue invalid value arg"); - } - return new Promise(resolve => { - const item = {}; - item[`${this.US_filename}---${key}`] = value; - browser.storage.local.set(item, () => resolve({success: 1})); - }); - }, - getValue(key, defaultValue) { - if (typeof key !== "string" || !key.length) { - return console.error("getValue invalid key arg"); - } - const prefixedKey = `${this.US_filename}---${key}`; - return new Promise(resolve => { - browser.storage.local.get(prefixedKey, item => { - if (Object.keys(item).length === 0) { - if (defaultValue != null) { - resolve(defaultValue); - } else { - resolve(undefined); - } - } else { - resolve(Object.values(item)[0]); - } - }); - }); - }, - deleteValue(key) { - if (typeof key !== "string" || !key.length) { - return console.error("deleteValue missing key arg"); - } - return new Promise(resolve => { - const prefixedKey = `${this.US_filename}---${key}`; - browser.storage.local.remove(prefixedKey, () => { - resolve({success: 1}); - }); - }); - }, - listValues() { - return new Promise(resolve => { - const prefix = `${this.US_filename}---`; - const keys = []; - browser.storage.local.get().then(items => { - for (const key in items) { - if (key.startsWith(prefix)) { - const k = key.replace(prefix, ""); - keys.push(k); - } - } - resolve(keys); - }); - }); - }, - addStyle(css) { - if (typeof css !== "string") { - return console.error("addStyle invalid css arg"); - } - return new Promise(resolve => { - const message = { - name: "API_ADD_STYLE", - css - }; - browser.runtime.sendMessage(message, response => resolve(response)); - }); - }, - getTab() { - return new Promise(resolve => { - const message = {name: "API_GET_TAB"}; - browser.runtime.sendMessage(message, response => { - resolve(response); - }); - }); - }, - saveTab(tab) { - if (tab == null) return console.error("saveTab invalid arg"); - return new Promise(resolve => { - const message = { - name: "API_SAVE_TAB", - tab - }; - browser.runtime.sendMessage(message, response => { - resolve(response); - }); - }); - }, - setClipboard(clipboardData, type) { - return new Promise(resolve => { - const message = { - name: "API_SET_CLIPBOARD", - clipboardData, - type - }; - browser.runtime.sendMessage(message, response => { - resolve(response); - }); - }); - }, - xhr(details) { - if (details == null) return console.error("xhr invalid details arg"); - if (!details.url) return console.error("xhr details missing url key"); - // generate random port name for single xhr - const xhrPortName = Math.random().toString(36).substring(1, 9); - // strip out functions from details - const detailsParsed = JSON.parse(JSON.stringify(details)); - // get all the "on" events from XMLHttpRequest object - const events = []; - for (const k in XMLHttpRequest.prototype) { - if (k.slice(0, 2) === "on") events.push(k); - } - // check which functions are included in the original details object - // add a bool to indicate if event listeners should be attached - for (const e of events) { - if (typeof details[e] === "function") detailsParsed[e] = true; - } - // define return method, will be populated after port is established - const response = { - abort: () => console.error("xhr has not yet been initialized") - }; - // port listener, most of the messaging logic goes here - const listener = port => { - if (port.name !== xhrPortName) return; - port.onMessage.addListener(async msg => { - if ( - events.includes(msg.name) - && typeof details[msg.name] === "function" - ) { - // process xhr response - const r = msg.response; - if (r.responseType === "arraybuffer") { - // arraybuffer responses had their data converted in background - // convert it back to arraybuffer - try { - const buffer = new Uint8Array(r.response).buffer; - r.response = buffer; - } catch (err) { - console.error("error parsing xhr arraybuffer", err); - } - } else if (r.responseType === "blob" && r.response.data) { - // blob responses had their data converted in background - // convert it back to blob - const resp = await fetch(r.response.data); - const b = await resp.blob(); - r.response = b; - } - // call userscript method - details[msg.name](msg.response); - } - // all messages received - // tell background it's safe to close port - if (msg.name === "onloadend") { - port.postMessage({name: "DISCONNECT"}); - } - }); - - // handle port disconnect and clean tasks - port.onDisconnect.addListener(p => { - if (p?.error) { - console.error(`port disconnected due to an error: ${p.error.message}`); - } - browser.runtime.onConnect.removeListener(listener); - }); - // fill the method returned to the user script - response.abort = () => port.postMessage({name: "ABORT"}); - }; - // wait for the background to establish a port connection - browser.runtime.onConnect.addListener(listener); - // pass the basic information to the background through a common message - const message = { - name: "API_XHR", - details: detailsParsed, - xhrPortName, - events - }; - browser.runtime.sendMessage(message); - return response; - }, - // include method names so they don't get skipped when adding to userscript - xmlHttpRequest: true, - GM_xmlhttpRequest: true -}; -// remote window's browser object -delete window.browser; - -// label used to distinguish frames in console -const label = randomLabel(); - -function randomLabel() { - const a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", r = Math.random(); - return a[Math.floor(r * a.length)] + r.toString().slice(5, 6); -} - -function processJS(userscript) { - const runAt = userscript.scriptObject["run-at"]; - if (runAt === "document-start") { - injectJS(userscript); - } else if (runAt === "document-end") { - if (document.readyState !== "loading") { - injectJS(userscript); - } else { - document.addEventListener("DOMContentLoaded", () => { - injectJS(userscript); - }); - } - } else if (runAt === "document-idle") { - if (document.readyState === "complete") { - injectJS(userscript); - } else { - document.addEventListener("readystatechange", () => { - if (document.readyState === "complete") { - injectJS(userscript); - } - }); - } - } -} - -function wrapCode(preCode, code, filename) { - const tag = window.self === window.top ? "" : `(${label})`; - return ` - (function() { - ${preCode} - (function() { - const US_filename = "${filename}"; - const apis = undefined; - const browser = undefined; - // userscript code below - ${code} - //# sourceURL=${filename.replace(/\s/g, "-") + tag} - })(); - })(); - `; -} - -function injectJS(userscript) { - const filename = userscript.scriptObject.filename; - const code = wrapCode(userscript.preCode, userscript.code, filename); - const name = userscript.scriptObject.name; - let injectInto = userscript.scriptObject["inject-into"]; - // change scope to content since strict CSP event detected - if (injectInto === "auto" && (userscript.fallback || cspFallbackAttempted)) { - injectInto = "content"; - console.warn(`Attempting fallback injection for ${name}`); - } else if (window.self === window.top) { - console.info(`Injecting ${name} %c(js)`, "color: #fff600"); - } else { - console.info(`Injecting ${name} %c(js)%c - %cframe(${label})(${window.location})`, "color: #fff600", "color: inherit", "color: #006fff"); - } - if (injectInto !== "content") { - const tag = document.createElement("script"); - tag.textContent = code; - document.head.appendChild(tag); - } else { - try { - // eslint-disable-next-line no-new-func - return Function(code)(); - } catch (error) { - console.error(`${filename} error`, error); - } - } -} - -function injectCSS(name, code) { - if (window.self === window.top) { - console.info(`Injecting ${name} %c(css)`, "color: #60f36c"); - } else { - console.info(`Injecting ${name} %c(css)%c - %cframe(${label})(${window.location})`, "color: #60f36c", "color: inherit", "color: #006fff"); - } - // Safari lacks full support for tabs.insertCSS - // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/insertCSS - // specifically frameId and cssOrigin - // if support for those details keys arrives, the method below can be used - // NOTE: manifest V3 does support frameId, but not origin - // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/insertCSS - - // write the css code to head of the document - const tag = document.createElement("style"); - tag.textContent = code; - document.head.appendChild(tag); -} - -function cspFallback(e) { - // if a security policy violation event has occurred - // and the directive is script-src or script-src-elem - // it's fair to assume that there is a strict CSP for javascript - // and that injection was blocked for all userscripts - // when any script-src violation is detected, re-attempt injection - if ( - e.effectiveDirective === "script-src" - || e.effectiveDirective === "script-src-elem" - ) { - // get all "auto" code - // since other code can trigger a security policy violation event - // make sure data var is not undefined before attempting fallback - if (!data || cspFallbackAttempted) return; - // update global that tracks security policy violations - cspFallbackAttempted = 1; - // for all userscripts with @inject-into: auto, attempt re-injection - for (let i = 0; i < data.files.js.length; i++) { - const userscript = data.files.js[i]; - if (userscript.scriptObject["inject-into"] !== "auto") continue; - userscript.fallback = 1; - processJS(userscript); - } - } -} - -function injection() { - browser.runtime.sendMessage({name: "REQ_USERSCRIPTS"}, response => { - // cancel injection if errors detected - if (!response || response.error) { - console.error(response?.error || "REQ_USERSCRIPTS returned undefined"); - return; - } - // save response locally in case CSP events occur - data = response; - // combine regular and context-menu scripts - const scripts = [...data.files.js, ...data.files.menu]; - // loop through each userscript and prepare for processing - for (let i = 0; i < scripts.length; i++) { - const userscript = scripts[i]; - userscript.preCode = ""; - // pass references to the api methods as needed - const gmMethods = []; - const filename = userscript.scriptObject.filename; - const grants = userscript.scriptObject.grant; - const injectInto = userscript.scriptObject["inject-into"]; - // create GM.info object - const scriptData = { - script: userscript.scriptObject, - scriptHandler: data.scriptHandler, - scriptHandlerVersion: data.scriptHandlerVersion, - scriptMetaStr: userscript.scriptMetaStr - }; - // all userscripts get access to GM.info - gmMethods.push("info: GM_info"); - // if @grant explicitly set to none, empty grants array - if (grants.includes("none")) grants.length = 0; - // @grant values exist for page scoped userscript - if (grants.length && injectInto === "page") { - // remove grants - grants.length = 0; - // log warning - console.warn(`${filename} @grant values removed due to @inject-into value: ${injectInto} - https://github.com/quoid/userscripts/issues/265#issuecomment-1213462394`); - } - // @grant exist for auto scoped userscript - if (grants.length && injectInto === "auto") { - // change scope - userscript.scriptObject["inject-into"] = "content"; - // log warning - console.warn(`${filename} @inject-into value set to 'content' due to @grant values: ${grants} - https://github.com/quoid/userscripts/issues/265#issuecomment-1213462394`); - } - // loop through each userscript @grant value, add methods as needed - for (let j = 0; j < grants.length; j++) { - const grant = grants[j]; - const method = grant.split(".")[1] || grant.split(".")[0]; - // ensure API method exists in apis object - if (!Object.keys(apis).includes(method)) continue; - // create the method string to be pushed to methods array - let methodStr = `${method}: apis.${method}`; - // add require variables to specific methods - switch (method) { - case "getValue": - case "setValue": - case "deleteValue": - case "listValues": - methodStr += `.bind({"US_filename": "${filename}"})`; - break; - case "info": - case "GM_info": - continue; - case "xmlHttpRequest": - gmMethods.push("xmlHttpRequest: apis.xhr"); - continue; - case "GM_xmlhttpRequest": - userscript.preCode += "const GM_xmlhttpRequest = apis.xhr;"; - continue; - } - gmMethods.push(methodStr); - } - // add GM.info - userscript.preCode += `const GM_info = ${JSON.stringify(scriptData)};`; - // add other included GM API methods - userscript.preCode += `const GM = {${gmMethods.join(",")}};`; - // process file for injection - processJS(userscript); - } - for (let i = 0; i < data.files.css.length; i++) { - const userstyle = data.files.css[i]; - injectCSS(userstyle.name, userstyle.code); - } - }); -} - -function listeners() { - // listens for messages from background, popup, etc... - browser.runtime.onMessage.addListener((request, sender, sendResponse) => { - const name = request.name; - if (name === "CONTEXT_RUN") { - // from bg script when context-menu item is clicked - // double check to ensure context-menu scripts only run in top windows - if (window !== window.top) return; - - // loop through context-menu scripts saved to data object and find match - // if no match found, nothing will execute and error will log - const filename = request.menuItemId; - for (let i = 0; i < data.files.menu.length; i++) { - const item = data.files.menu[i]; - if (item.scriptObject.filename === filename) { - console.info(`Injecting ${filename} %c(js)`, "color: #fff600"); - sendResponse({ - code: wrapCode( - item.preCode, - item.code, - filename - ) - }); - return; - } - } - console.error(`Couldn't find ${filename} code!`); - } - }); - // listen for CSP violations - document.addEventListener("securitypolicyviolation", cspFallback); -} - -async function initialize() { - const results = await browser.storage.local.get("US_GLOBAL_ACTIVE"); - if (results?.US_GLOBAL_ACTIVE === false) return console.info("Userscripts off"); - // start the injection process and add the listeners - injection(); - listeners(); -} - -initialize(); diff --git a/xcode/Shared/Shared.swift b/xcode/Shared/Utilities.swift similarity index 100% rename from xcode/Shared/Shared.swift rename to xcode/Shared/Utilities.swift diff --git a/xcode/Mac-Tests/UserscriptsTests.swift b/xcode/Tests-Mac/UserscriptsTests.swift similarity index 100% rename from xcode/Mac-Tests/UserscriptsTests.swift rename to xcode/Tests-Mac/UserscriptsTests.swift diff --git a/xcode/Userscripts.xcodeproj/project.pbxproj b/xcode/Userscripts.xcodeproj/project.pbxproj index 3a703519..4f17eb62 100644 --- a/xcode/Userscripts.xcodeproj/project.pbxproj +++ b/xcode/Userscripts.xcodeproj/project.pbxproj @@ -11,7 +11,6 @@ 030C64FC2AD0B24C00548FBD /* Initialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030C64FB2AD0B24C00548FBD /* Initialization.swift */; }; 0336619F294DF7C900CFE179 /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0336619E294DF7C900CFE179 /* Functions.swift */; }; 033661A529510B7900CFE179 /* View.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033661A329510B7900CFE179 /* View.storyboard */; }; - 0376905B29808EEA00F3474D /* background.js in Resources */ = {isa = PBXBuildFile; fileRef = 0376905A29808EEA00F3474D /* background.js */; }; 03C24F972ABD2CBB00F130F9 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C24F962ABD2CBB00F130F9 /* Preferences.swift */; }; 03C24F982ABD2CBB00F130F9 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C24F962ABD2CBB00F130F9 /* Preferences.swift */; }; 03C24F992ABD2CBB00F130F9 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C24F962ABD2CBB00F130F9 /* Preferences.swift */; }; @@ -24,7 +23,6 @@ 4A301B2A270A474400C7E9E1 /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A57BA07227235CE008A9763 /* SafariWebExtensionHandler.swift */; }; 4A301B2B270A474F00C7E9E1 /* _locales in Resources */ = {isa = PBXBuildFile; fileRef = 4A36A617268266B30018536B /* _locales */; }; 4A301B2C270A475200C7E9E1 /* images in Resources */ = {isa = PBXBuildFile; fileRef = 4A36A616268266B30018536B /* images */; }; - 4A301B2E270A475D00C7E9E1 /* content.js in Resources */ = {isa = PBXBuildFile; fileRef = 4AB22357268B9B9200D551C6 /* content.js */; }; 4A301B2F270A476000C7E9E1 /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = 4AD66D2026826D8100B004E8 /* manifest.json */; }; 4A301B32270A49F200C7E9E1 /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AED6491268CDB58001794BF /* Functions.swift */; }; 4A36A61A268266B30018536B /* images in Resources */ = {isa = PBXBuildFile; fileRef = 4A36A616268266B30018536B /* images */; }; @@ -44,16 +42,14 @@ 4A57B9F9227235CE008A9763 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4A57B9F8227235CE008A9763 /* Assets.xcassets */; }; 4A57BA00227235CE008A9763 /* Userscripts-Mac-Safari-Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 4A57B9FF227235CE008A9763 /* Userscripts-Mac-Safari-Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4A57BA08227235CE008A9763 /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A57BA07227235CE008A9763 /* SafariWebExtensionHandler.swift */; }; - 4AB22358268B9B9200D551C6 /* content.js in Resources */ = {isa = PBXBuildFile; fileRef = 4AB22357268B9B9200D551C6 /* content.js */; }; 4AD66D2326826D8100B004E8 /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = 4AD66D2026826D8100B004E8 /* manifest.json */; }; 4AED6492268CDB58001794BF /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AED6491268CDB58001794BF /* Functions.swift */; }; 7878ED24299BA27B00E36A24 /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A57BA07227235CE008A9763 /* SafariWebExtensionHandler.swift */; }; - 7878ED2B299BA36800E36A24 /* background.js in Resources */ = {isa = PBXBuildFile; fileRef = 0376905A29808EEA00F3474D /* background.js */; }; - 78B25583299D4C27000B2E9B /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B25582299D4C27000B2E9B /* Shared.swift */; }; - 78B25584299D4C27000B2E9B /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B25582299D4C27000B2E9B /* Shared.swift */; }; - 78B25585299D4C27000B2E9B /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B25582299D4C27000B2E9B /* Shared.swift */; }; - 78B25586299D4C27000B2E9B /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B25582299D4C27000B2E9B /* Shared.swift */; }; - 78B25587299D4C27000B2E9B /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B25582299D4C27000B2E9B /* Shared.swift */; }; + 78B25583299D4C27000B2E9B /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B25582299D4C27000B2E9B /* Utilities.swift */; }; + 78B25584299D4C27000B2E9B /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B25582299D4C27000B2E9B /* Utilities.swift */; }; + 78B25585299D4C27000B2E9B /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B25582299D4C27000B2E9B /* Utilities.swift */; }; + 78B25586299D4C27000B2E9B /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B25582299D4C27000B2E9B /* Utilities.swift */; }; + 78B25587299D4C27000B2E9B /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B25582299D4C27000B2E9B /* Utilities.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -110,7 +106,6 @@ 030C64FB2AD0B24C00548FBD /* Initialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Initialization.swift; sourceTree = ""; }; 0336619E294DF7C900CFE179 /* Functions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = ""; }; 033661A429510B7900CFE179 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/View.storyboard; sourceTree = ""; }; - 0376905A29808EEA00F3474D /* background.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = background.js; sourceTree = ""; }; 03C24F962ABD2CBB00F130F9 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; 03E20D05291E170000B4F692 /* dist */ = {isa = PBXFileReference; lastKnownFileType = folder; path = dist; sourceTree = ""; }; 4A143AA9279DE6FF0029BFD0 /* Mac-Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Mac-Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -140,7 +135,6 @@ 4A57BA07227235CE008A9763 /* SafariWebExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariWebExtensionHandler.swift; sourceTree = ""; }; 4A57BA0E227235CE008A9763 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4A57BA1D227235E6008A9763 /* Mac.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Mac.entitlements; sourceTree = ""; }; - 4AB22357268B9B9200D551C6 /* content.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = content.js; sourceTree = ""; }; 4AD66D2026826D8100B004E8 /* manifest.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = manifest.json; sourceTree = ""; }; 4AED6491268CDB58001794BF /* Functions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = ""; }; 78391D4629A4CB3C00979E34 /* Userscripts-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Userscripts-Debug.xcconfig"; sourceTree = ""; }; @@ -151,7 +145,7 @@ 7878ED32299BBEBF00E36A24 /* Mac-Safari-Extension.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Mac-Safari-Extension.xcconfig"; sourceTree = ""; }; 7878ED34299BBEBF00E36A24 /* iOS-App.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "iOS-App.xcconfig"; sourceTree = ""; }; 7878ED38299BBEBF00E36A24 /* Mac-App.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Mac-App.xcconfig"; sourceTree = ""; }; - 78B25582299D4C27000B2E9B /* Shared.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shared.swift; sourceTree = ""; }; + 78B25582299D4C27000B2E9B /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -193,15 +187,22 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 4A143AAA279DE6FF0029BFD0 /* Mac-Tests */ = { + 0316A9992AD7709600AAEC8A /* App-Shared */ = { + isa = PBXGroup; + children = ( + ); + path = "App-Shared"; + sourceTree = ""; + }; + 4A143AAA279DE6FF0029BFD0 /* Tests-Mac */ = { isa = PBXGroup; children = ( 4A143AAB279DE6FF0029BFD0 /* UserscriptsTests.swift */, ); - path = "Mac-Tests"; + path = "Tests-Mac"; sourceTree = ""; }; - 4A4CF6DC270A38BD00111584 /* iOS-App */ = { + 4A4CF6DC270A38BD00111584 /* App-iOS */ = { isa = PBXGroup; children = ( 4A4CF6F2270A38BF00111584 /* Info.plist */, @@ -216,7 +217,7 @@ 4A4CF6EB270A38BD00111584 /* LaunchScreen.storyboard */, 4A4CF6EE270A38BD00111584 /* Main.storyboard */, ); - path = "iOS-App"; + path = "App-iOS"; sourceTree = ""; }; 4A4CF6DF270A38BD00111584 /* Resources */ = { @@ -233,10 +234,11 @@ children = ( 7878ED2E299BBEBF00E36A24 /* xcconfig */, 78B25581299D4C27000B2E9B /* Shared */, - 4A57BA06227235CE008A9763 /* Safari-Extension */, - 4A57B9EE227235CD008A9763 /* Mac-App */, - 4A4CF6DC270A38BD00111584 /* iOS-App */, - 4A143AAA279DE6FF0029BFD0 /* Mac-Tests */, + 4A57BA06227235CE008A9763 /* Ext-Safari */, + 0316A9992AD7709600AAEC8A /* App-Shared */, + 4A57B9EE227235CD008A9763 /* App-Mac */, + 4A4CF6DC270A38BD00111584 /* App-iOS */, + 4A143AAA279DE6FF0029BFD0 /* Tests-Mac */, 4A57B9ED227235CD008A9763 /* Products */, ); sourceTree = ""; @@ -253,7 +255,7 @@ name = Products; sourceTree = ""; }; - 4A57B9EE227235CD008A9763 /* Mac-App */ = { + 4A57B9EE227235CD008A9763 /* App-Mac */ = { isa = PBXGroup; children = ( 4A57B9FA227235CE008A9763 /* Info.plist */, @@ -265,10 +267,10 @@ 4A57B9F3227235CD008A9763 /* Main.storyboard */, 033661A329510B7900CFE179 /* View.storyboard */, ); - path = "Mac-App"; + path = "App-Mac"; sourceTree = ""; }; - 4A57BA06227235CE008A9763 /* Safari-Extension */ = { + 4A57BA06227235CE008A9763 /* Ext-Safari */ = { isa = PBXGroup; children = ( 4A57BA0E227235CE008A9763 /* Info.plist */, @@ -278,7 +280,7 @@ 4A57BA07227235CE008A9763 /* SafariWebExtensionHandler.swift */, 4AED6491268CDB58001794BF /* Functions.swift */, ); - path = "Safari-Extension"; + path = "Ext-Safari"; sourceTree = ""; }; 4AF1DD0D268265DB00086E88 /* Resources */ = { @@ -288,8 +290,6 @@ 4A36A616268266B30018536B /* images */, 03E20D05291E170000B4F692 /* dist */, 4AD66D2026826D8100B004E8 /* manifest.json */, - 0376905A29808EEA00F3474D /* background.js */, - 4AB22357268B9B9200D551C6 /* content.js */, ); path = Resources; sourceTree = ""; @@ -311,7 +311,7 @@ 78B25581299D4C27000B2E9B /* Shared */ = { isa = PBXGroup; children = ( - 78B25582299D4C27000B2E9B /* Shared.swift */, + 78B25582299D4C27000B2E9B /* Utilities.swift */, 03C24F962ABD2CBB00F130F9 /* Preferences.swift */, ); path = Shared; @@ -491,9 +491,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4A301B2E270A475D00C7E9E1 /* content.js in Resources */, 4A301B2C270A475200C7E9E1 /* images in Resources */, - 7878ED2B299BA36800E36A24 /* background.js in Resources */, 03E20D07291E170000B4F692 /* dist in Resources */, 4A301B2F270A476000C7E9E1 /* manifest.json in Resources */, 4A301B2B270A474F00C7E9E1 /* _locales in Resources */, @@ -515,10 +513,8 @@ buildActionMask = 2147483647; files = ( 4AD66D2326826D8100B004E8 /* manifest.json in Resources */, - 4AB22358268B9B9200D551C6 /* content.js in Resources */, 03E20D06291E170000B4F692 /* dist in Resources */, 4A36A61A268266B30018536B /* images in Resources */, - 0376905B29808EEA00F3474D /* background.js in Resources */, 4A36A61B268266B30018536B /* _locales in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -557,7 +553,7 @@ 7878ED24299BA27B00E36A24 /* SafariWebExtensionHandler.swift in Sources */, 4A143AB2279DEA170029BFD0 /* Functions.swift in Sources */, 03C24F9B2ABD2CBB00F130F9 /* Preferences.swift in Sources */, - 78B25587299D4C27000B2E9B /* Shared.swift in Sources */, + 78B25587299D4C27000B2E9B /* Utilities.swift in Sources */, 4A143AAC279DE6FF0029BFD0 /* UserscriptsTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -570,7 +566,7 @@ 4A4CF6DE270A38BD00111584 /* AppDelegate.swift in Sources */, 03C24F992ABD2CBB00F130F9 /* Preferences.swift in Sources */, 030C64FC2AD0B24C00548FBD /* Initialization.swift in Sources */, - 78B25585299D4C27000B2E9B /* Shared.swift in Sources */, + 78B25585299D4C27000B2E9B /* Utilities.swift in Sources */, 4A4CF6E8270A38BD00111584 /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -579,7 +575,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 78B25586299D4C27000B2E9B /* Shared.swift in Sources */, + 78B25586299D4C27000B2E9B /* Utilities.swift in Sources */, 4A301B2A270A474400C7E9E1 /* SafariWebExtensionHandler.swift in Sources */, 03C24F9A2ABD2CBB00F130F9 /* Preferences.swift in Sources */, 4A301B32270A49F200C7E9E1 /* Functions.swift in Sources */, @@ -593,7 +589,7 @@ 4A57B9F7227235CD008A9763 /* ViewController.swift in Sources */, 4A57B9F2227235CD008A9763 /* AppDelegate.swift in Sources */, 03C24F972ABD2CBB00F130F9 /* Preferences.swift in Sources */, - 78B25583299D4C27000B2E9B /* Shared.swift in Sources */, + 78B25583299D4C27000B2E9B /* Utilities.swift in Sources */, 0336619F294DF7C900CFE179 /* Functions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -602,7 +598,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 78B25584299D4C27000B2E9B /* Shared.swift in Sources */, + 78B25584299D4C27000B2E9B /* Utilities.swift in Sources */, 4A57BA08227235CE008A9763 /* SafariWebExtensionHandler.swift in Sources */, 03C24F982ABD2CBB00F130F9 /* Preferences.swift in Sources */, 4AED6492268CDB58001794BF /* Functions.swift in Sources */, diff --git a/xcode/xcconfig/Mac-App.xcconfig b/xcode/xcconfig/Mac-App.xcconfig index 76526d70..37b672da 100644 --- a/xcode/xcconfig/Mac-App.xcconfig +++ b/xcode/xcconfig/Mac-App.xcconfig @@ -1,11 +1,11 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon -CODE_SIGN_ENTITLEMENTS = Mac-App/App.entitlements +CODE_SIGN_ENTITLEMENTS = App-Mac/App.entitlements COMBINE_HIDPI_IMAGES = YES CURRENT_PROJECT_VERSION = $(MAC_APP_BUILD) ENABLE_HARDENED_RUNTIME = YES GENERATE_INFOPLIST_FILE = YES -INFOPLIST_FILE = Mac-App/Info.plist +INFOPLIST_FILE = App-Mac/Info.plist INFOPLIST_KEY_CFBundleDisplayName = $(PROJECT_NAME)-$(TARGET_NAME) INFOPLIST_KEY_CFBundleDisplayName[config=Debug] = $(PROJECT_NAME)-$(TARGET_NAME)-Debug LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks @@ -15,4 +15,4 @@ PRODUCT_NAME = $(PROJECT_NAME) PRODUCT_NAME[config=Debug] = $(PROJECT_NAME)-Debug // Override this file -#include? "Mac-App.dev.xcconfig" \ No newline at end of file +#include? "Mac-App.dev.xcconfig" diff --git a/xcode/xcconfig/Mac-Safari-Extension.xcconfig b/xcode/xcconfig/Mac-Safari-Extension.xcconfig index 13f53620..a0d123ef 100644 --- a/xcode/xcconfig/Mac-Safari-Extension.xcconfig +++ b/xcode/xcconfig/Mac-Safari-Extension.xcconfig @@ -1,8 +1,8 @@ -CODE_SIGN_ENTITLEMENTS = Safari-Extension/Mac.entitlements +CODE_SIGN_ENTITLEMENTS = Ext-Safari/Mac.entitlements CURRENT_PROJECT_VERSION = $(MAC_APP_BUILD) ENABLE_HARDENED_RUNTIME = YES GENERATE_INFOPLIST_FILE = YES -INFOPLIST_FILE = Safari-Extension/Info.plist +INFOPLIST_FILE = Ext-Safari/Info.plist INFOPLIST_KEY_CFBundleDisplayName = $(PRODUCT_NAME) INFOPLIST_KEY_CFBundleDisplayName[config=Debug] = $(PRODUCT_NAME)-Debug LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks @@ -12,4 +12,4 @@ PRODUCT_NAME = $(PROJECT_NAME)-$(TARGET_NAME) SKIP_INSTALL = YES // Override this file -#include? "Mac-Safari-Extension.dev.xcconfig" \ No newline at end of file +#include? "Mac-Safari-Extension.dev.xcconfig" diff --git a/xcode/xcconfig/Mac-Tests.xcconfig b/xcode/xcconfig/Mac-Tests.xcconfig index 998554bc..01a14d09 100644 --- a/xcode/xcconfig/Mac-Tests.xcconfig +++ b/xcode/xcconfig/Mac-Tests.xcconfig @@ -8,4 +8,4 @@ SWIFT_EMIT_LOC_STRINGS = NO TEST_HOST = $(BUILT_PRODUCTS_DIR)/Userscripts-Debug.app/Contents/MacOS/Userscripts-Debug // Override this file -#include? "Mac-Tests.dev.xcconfig" \ No newline at end of file +#include? "Mac-Tests.dev.xcconfig" diff --git a/xcode/xcconfig/Userscripts-Base.xcconfig b/xcode/xcconfig/Userscripts-Base.xcconfig index 7f35556a..4113b7fa 100644 --- a/xcode/xcconfig/Userscripts-Base.xcconfig +++ b/xcode/xcconfig/Userscripts-Base.xcconfig @@ -61,4 +61,4 @@ SWIFT_OPTIMIZATION_LEVEL[config=Release] = -O SWIFT_VERSION = 5.0 // Override this file -#include? "Userscripts-Base.dev.xcconfig" \ No newline at end of file +#include? "Userscripts-Base.dev.xcconfig" diff --git a/xcode/xcconfig/Userscripts-Debug.xcconfig b/xcode/xcconfig/Userscripts-Debug.xcconfig index a0060f16..c596ca04 100644 --- a/xcode/xcconfig/Userscripts-Debug.xcconfig +++ b/xcode/xcconfig/Userscripts-Debug.xcconfig @@ -21,4 +21,4 @@ MAC_SHARED_GID = 00000.$(ORG_IDENTIFIER).userscripts APP_URL_SCHEME = userscriptsdebug // Override this file -#include? "Userscripts-Debug.dev.xcconfig" \ No newline at end of file +#include? "Userscripts-Debug.dev.xcconfig" diff --git a/xcode/xcconfig/iOS-App.xcconfig b/xcode/xcconfig/iOS-App.xcconfig index f52d56d6..9b41bde9 100644 --- a/xcode/xcconfig/iOS-App.xcconfig +++ b/xcode/xcconfig/iOS-App.xcconfig @@ -1,10 +1,10 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor -CODE_SIGN_ENTITLEMENTS = iOS-App/App.entitlements +CODE_SIGN_ENTITLEMENTS = App-iOS/App.entitlements CURRENT_PROJECT_VERSION = $(IOS_APP_BUILD) GENERATE_INFOPLIST_FILE = YES -INFOPLIST_FILE = iOS-App/Info.plist +INFOPLIST_FILE = App-iOS/Info.plist INFOPLIST_KEY_CFBundleDisplayName = $(PROJECT_NAME) INFOPLIST_KEY_CFBundleDisplayName[config=Debug] = $(PROJECT_NAME)-D INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES @@ -27,4 +27,4 @@ TARGETED_DEVICE_FAMILY = 1,2 VALIDATE_PRODUCT[config=Release] = YES // Override this file -#include? "iOS-App.dev.xcconfig" \ No newline at end of file +#include? "iOS-App.dev.xcconfig" diff --git a/xcode/xcconfig/iOS-Safari-Extension.xcconfig b/xcode/xcconfig/iOS-Safari-Extension.xcconfig index 0bdd357f..7b7e69ac 100644 --- a/xcode/xcconfig/iOS-Safari-Extension.xcconfig +++ b/xcode/xcconfig/iOS-Safari-Extension.xcconfig @@ -1,7 +1,7 @@ -CODE_SIGN_ENTITLEMENTS = Safari-Extension/iOS.entitlements +CODE_SIGN_ENTITLEMENTS = Ext-Safari/iOS.entitlements CURRENT_PROJECT_VERSION = $(IOS_APP_BUILD) GENERATE_INFOPLIST_FILE = YES -INFOPLIST_FILE = Safari-Extension/Info.plist +INFOPLIST_FILE = Ext-Safari/Info.plist INFOPLIST_KEY_CFBundleDisplayName = $(PRODUCT_NAME) INFOPLIST_KEY_CFBundleDisplayName[config=Debug] = $(PROJECT_NAME)-D LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @executable_path/../../Frameworks @@ -16,4 +16,4 @@ TARGETED_DEVICE_FAMILY = 1,2 VALIDATE_PRODUCT[config=Release] = YES // Override this file -#include? "iOS-Safari-Extension.dev.xcconfig" \ No newline at end of file +#include? "iOS-Safari-Extension.dev.xcconfig" From fd7b575999f4dcadd27803c57b3cfd29f904761d Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Thu, 12 Oct 2023 17:50:21 +0800 Subject: [PATCH 025/315] refactor(dev): adjust src structure --- ...-popup.html => entry-ext-action-popup.html | 2 +- ...page.html => entry-ext-extension-page.html | 2 +- index.html | 2 +- src/{ => dev}/App.svelte | 4 +- src/{ => dev}/app.css | 0 src/{ => dev}/assets/logo.png | Bin src/{ => dev}/main.js | 0 src/{ => dev}/vite-env.d.ts | 0 src/{popup => ext/action-popup}/App.svelte | 0 .../action-popup}/Components/PopupItem.svelte | 0 .../action-popup}/Components/View.svelte | 0 .../Components/Views/AllItemsView.svelte | 0 .../Components/Views/InstallView.svelte | 0 .../Components/Views/UpdateView.svelte | 0 src/{popup => ext/action-popup}/app.css | 0 src/{popup => ext/action-popup}/main.js | 0 src/ext/background/main.js | 509 ++++++++++++++++++ src/ext/content-scripts/main.js | 469 ++++++++++++++++ src/{page => ext/extension-page}/App.svelte | 0 .../Components/Editor/CodeMirror.svelte | 0 .../Components/Editor/Editor.svelte | 0 .../Components/Editor/EditorSearch.svelte | 0 .../Components/Notification.svelte | 0 .../Components/Settings.svelte | 0 .../Components/Sidebar/Sidebar.svelte | 0 .../Components/Sidebar/SidebarFilter.svelte | 0 .../Components/Sidebar/SidebarItem.svelte | 0 src/{page => ext/extension-page}/app.css | 0 src/{page => ext/extension-page}/cm.css | 0 .../extension-page}/codemirror.js | 0 src/{page => ext/extension-page}/main.js | 0 src/{page => ext/extension-page}/store.js | 0 .../shared/Components/Dropdown.svelte | 0 .../shared/Components/IconButton.svelte | 0 src/{ => ext}/shared/Components/Loader.svelte | 0 src/{ => ext}/shared/Components/Tag.svelte | 0 src/{ => ext}/shared/Components/Toggle.svelte | 0 src/{ => ext}/shared/dev.js | 0 .../shared/dev/DEMO.Alert-URL.user.js | 0 src/{ => ext}/shared/img/icon-arrow-down.svg | 0 src/{ => ext}/shared/img/icon-arrow-left.svg | 0 src/{ => ext}/shared/img/icon-arrow-up.svg | 0 src/{ => ext}/shared/img/icon-check.svg | 0 src/{ => ext}/shared/img/icon-clear.svg | 0 src/{ => ext}/shared/img/icon-close.svg | 0 src/{ => ext}/shared/img/icon-download.svg | 0 src/{ => ext}/shared/img/icon-edit.svg | 0 src/{ => ext}/shared/img/icon-error.svg | 0 src/{ => ext}/shared/img/icon-info.svg | 0 src/{ => ext}/shared/img/icon-loader.svg | 0 src/{ => ext}/shared/img/icon-loupe.svg | 0 src/{ => ext}/shared/img/icon-open.svg | 0 src/{ => ext}/shared/img/icon-plus.svg | 0 src/{ => ext}/shared/img/icon-power.svg | 0 src/{ => ext}/shared/img/icon-refresh.svg | 0 src/{ => ext}/shared/img/icon-settings.svg | 0 src/{ => ext}/shared/img/icon-sort.svg | 0 src/{ => ext}/shared/img/icon-sync.svg | 0 src/{ => ext}/shared/img/icon-trash.svg | 0 src/{ => ext}/shared/img/icon-update.svg | 0 src/{ => ext}/shared/img/icon-warn.svg | 0 src/{ => ext}/shared/img/logo.svg | 0 src/{ => ext}/shared/reset.css | 0 src/{ => ext}/shared/settings.js | 0 src/{ => ext}/shared/utils.js | 4 +- src/{ => ext}/shared/variables.css | 0 66 files changed, 985 insertions(+), 7 deletions(-) rename entry-popup.html => entry-ext-action-popup.html (83%) rename entry-page.html => entry-ext-extension-page.html (84%) rename src/{ => dev}/App.svelte (75%) rename src/{ => dev}/app.css (100%) rename src/{ => dev}/assets/logo.png (100%) rename src/{ => dev}/main.js (100%) rename src/{ => dev}/vite-env.d.ts (100%) rename src/{popup => ext/action-popup}/App.svelte (100%) rename src/{popup => ext/action-popup}/Components/PopupItem.svelte (100%) rename src/{popup => ext/action-popup}/Components/View.svelte (100%) rename src/{popup => ext/action-popup}/Components/Views/AllItemsView.svelte (100%) rename src/{popup => ext/action-popup}/Components/Views/InstallView.svelte (100%) rename src/{popup => ext/action-popup}/Components/Views/UpdateView.svelte (100%) rename src/{popup => ext/action-popup}/app.css (100%) rename src/{popup => ext/action-popup}/main.js (100%) create mode 100644 src/ext/background/main.js create mode 100644 src/ext/content-scripts/main.js rename src/{page => ext/extension-page}/App.svelte (100%) rename src/{page => ext/extension-page}/Components/Editor/CodeMirror.svelte (100%) rename src/{page => ext/extension-page}/Components/Editor/Editor.svelte (100%) rename src/{page => ext/extension-page}/Components/Editor/EditorSearch.svelte (100%) rename src/{page => ext/extension-page}/Components/Notification.svelte (100%) rename src/{page => ext/extension-page}/Components/Settings.svelte (100%) rename src/{page => ext/extension-page}/Components/Sidebar/Sidebar.svelte (100%) rename src/{page => ext/extension-page}/Components/Sidebar/SidebarFilter.svelte (100%) rename src/{page => ext/extension-page}/Components/Sidebar/SidebarItem.svelte (100%) rename src/{page => ext/extension-page}/app.css (100%) rename src/{page => ext/extension-page}/cm.css (100%) rename src/{page => ext/extension-page}/codemirror.js (100%) rename src/{page => ext/extension-page}/main.js (100%) rename src/{page => ext/extension-page}/store.js (100%) rename src/{ => ext}/shared/Components/Dropdown.svelte (100%) rename src/{ => ext}/shared/Components/IconButton.svelte (100%) rename src/{ => ext}/shared/Components/Loader.svelte (100%) rename src/{ => ext}/shared/Components/Tag.svelte (100%) rename src/{ => ext}/shared/Components/Toggle.svelte (100%) rename src/{ => ext}/shared/dev.js (100%) rename src/{ => ext}/shared/dev/DEMO.Alert-URL.user.js (100%) rename src/{ => ext}/shared/img/icon-arrow-down.svg (100%) rename src/{ => ext}/shared/img/icon-arrow-left.svg (100%) rename src/{ => ext}/shared/img/icon-arrow-up.svg (100%) rename src/{ => ext}/shared/img/icon-check.svg (100%) rename src/{ => ext}/shared/img/icon-clear.svg (100%) rename src/{ => ext}/shared/img/icon-close.svg (100%) rename src/{ => ext}/shared/img/icon-download.svg (100%) rename src/{ => ext}/shared/img/icon-edit.svg (100%) rename src/{ => ext}/shared/img/icon-error.svg (100%) rename src/{ => ext}/shared/img/icon-info.svg (100%) rename src/{ => ext}/shared/img/icon-loader.svg (100%) rename src/{ => ext}/shared/img/icon-loupe.svg (100%) rename src/{ => ext}/shared/img/icon-open.svg (100%) rename src/{ => ext}/shared/img/icon-plus.svg (100%) rename src/{ => ext}/shared/img/icon-power.svg (100%) rename src/{ => ext}/shared/img/icon-refresh.svg (100%) rename src/{ => ext}/shared/img/icon-settings.svg (100%) rename src/{ => ext}/shared/img/icon-sort.svg (100%) rename src/{ => ext}/shared/img/icon-sync.svg (100%) rename src/{ => ext}/shared/img/icon-trash.svg (100%) rename src/{ => ext}/shared/img/icon-update.svg (100%) rename src/{ => ext}/shared/img/icon-warn.svg (100%) rename src/{ => ext}/shared/img/logo.svg (100%) rename src/{ => ext}/shared/reset.css (100%) rename src/{ => ext}/shared/settings.js (100%) rename src/{ => ext}/shared/utils.js (98%) rename src/{ => ext}/shared/variables.css (100%) diff --git a/entry-popup.html b/entry-ext-action-popup.html similarity index 83% rename from entry-popup.html rename to entry-ext-action-popup.html index 2694df66..e135d374 100644 --- a/entry-popup.html +++ b/entry-ext-action-popup.html @@ -9,6 +9,6 @@
- + diff --git a/entry-page.html b/entry-ext-extension-page.html similarity index 84% rename from entry-page.html rename to entry-ext-extension-page.html index 82a9df6b..3f64fff7 100644 --- a/entry-page.html +++ b/entry-ext-extension-page.html @@ -9,7 +9,7 @@
- + diff --git a/index.html b/index.html index b97bad98..a0b12e43 100644 --- a/index.html +++ b/index.html @@ -13,7 +13,7 @@
- + - + diff --git a/public/page/jshint.min.js b/public/extension-page/jshint.min.js similarity index 100% rename from public/page/jshint.min.js rename to public/extension-page/jshint.min.js diff --git a/src/ext/background/main.js b/src/ext/background/main.js index 22e68208..75ce63d6 100644 --- a/src/ext/background/main.js +++ b/src/ext/background/main.js @@ -1,14 +1,6 @@ -// functions from "src/shared/utils.js"; -async function openExtensionPage() { - const url = browser.runtime.getURL("dist/entry-ext-extension-page.html"); - const tabs = await browser.tabs.query({url}); - const tab = tabs.find(e => e.url.startsWith(url)); - if (!tab) return browser.tabs.create({url}); - browser.tabs.update(tab.id, {active: true}); - browser.windows.update(tab.windowId, {focused: true}); -} - -// functions from "src/shared/settings.js"; +import {openExtensionPage} from "../shared/utils.js"; +// eslint-disable-next-line import/no-namespace +import * as settingsStorage from "../shared/settings.js"; // first sorts files by run-at value, then by weight value function userscriptSort(a, b) { @@ -81,11 +73,10 @@ async function setBadgeCount() { // @todo until better introduce in ios, only set badge on macOS const platform = await getPlatform(); if (platform !== "macos") return clearBadge(); - // @todo after the background script is modularized, import and use: - // settingsStorage.get(["global_active","toolbar_badge_count","global_exclude_match"]) - const results = await browser.storage.local.get(["US_GLOBAL_ACTIVE", "US_TOOLBAR_BADGE_COUNT"]); - if (results?.US_GLOBAL_ACTIVE === false) return clearBadge(); - if (results?.US_TOOLBAR_BADGE_COUNT === false) return clearBadge(); + // @todo settingsStorage.get("global_exclude_match") + const settings = await settingsStorage.get(["global_active", "toolbar_badge_count"]); + if (settings.global_active === false) return clearBadge(); + if (settings.toolbar_badge_count === false) return clearBadge(); const currentTab = await browser.tabs.getCurrent(); // no active tabs exist (user closed all windows) @@ -280,12 +271,11 @@ function contextClick(info, tab) { async function nativeChecks() { const response = await browser.runtime.sendNativeMessage({name: "NATIVE_CHECKS"}); - // note: use settings.js once background page modularization if (response.error) { - browser.storage.local.set({US_ERROR_NATIVE: response}); + settingsStorage.set({error_native: response}); return false; } - browser.storage.local.remove("US_ERROR_NATIVE"); + settingsStorage.reset("error_native"); return true; } diff --git a/src/ext/shared/utils.js b/src/ext/shared/utils.js index 44613ac5..48105b2d 100644 --- a/src/ext/shared/utils.js +++ b/src/ext/shared/utils.js @@ -178,8 +178,8 @@ export const validMetaKeys = new Set([ ]); export const extensionPaths = { - popup: "/dist/entry-ext-action-popup.html", - page: "/dist/entry-ext-extension-page.html" + popup: "/dist/s/entry-ext-action-popup.html", + page: "/dist/s/entry-ext-extension-page.html" }; export async function openExtensionPage() { diff --git a/vite.config.js b/vite.config.js index 044b986d..fb8675c5 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,4 +1,3 @@ -import {resolve} from "path"; import {defineConfig} from "vite"; import {svelte} from "@sveltejs/vite-plugin-svelte"; @@ -31,14 +30,5 @@ import {svelte} from "@sveltejs/vite-plugin-svelte"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [svelte()], - base: "./", - build: { - outDir: "xcode/Safari-Extension/Resources/dist/", - rollupOptions: { - input: { - page: resolve(__dirname, "entry-page.html"), - popup: resolve(__dirname, "entry-popup.html") - } - } - } + base: "./" }); diff --git a/xcode/Ext-Safari/Resources/manifest.json b/xcode/Ext-Safari/Resources/manifest.json index 63a425d1..21e146fc 100644 --- a/xcode/Ext-Safari/Resources/manifest.json +++ b/xcode/Ext-Safari/Resources/manifest.json @@ -17,7 +17,7 @@ "persistent": false }, "browser_action": { - "default_popup": "dist/entry-ext-action-popup.html", + "default_popup": "dist/s/entry-ext-action-popup.html", "default_icon": { "16": "images/toolbar-icon-16.png", "32": "images/toolbar-icon-32.png" From 3ed1bdc7e50a32c0b0212ef1283f8d25300c5d0d Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Fri, 13 Oct 2023 07:23:20 +0800 Subject: [PATCH 027/315] refactor(app): import ios app webview src --- entry-app-webview.html | 14 +++++++ etc/App-iOS-WebView.zip | Bin 144962 -> 0 bytes src/app/App.svelte | 85 ++++++++++++++++++++++++++++++++++++++++ src/app/app.css | 37 +++++++++++++++++ src/app/img/icon.png | Bin 0 -> 24309 bytes src/app/img/logo.svg | 1 + src/app/main.js | 10 +++++ src/app/reset.css | 44 +++++++++++++++++++++ src/app/scripts.js | 15 +++++++ src/app/variables.css | 51 ++++++++++++++++++++++++ src/dev/App.svelte | 1 + 11 files changed, 258 insertions(+) create mode 100644 entry-app-webview.html delete mode 100644 etc/App-iOS-WebView.zip create mode 100644 src/app/App.svelte create mode 100644 src/app/app.css create mode 100644 src/app/img/icon.png create mode 100644 src/app/img/logo.svg create mode 100644 src/app/main.js create mode 100644 src/app/reset.css create mode 100644 src/app/scripts.js create mode 100644 src/app/variables.css diff --git a/entry-app-webview.html b/entry-app-webview.html new file mode 100644 index 00000000..62b414e7 --- /dev/null +++ b/entry-app-webview.html @@ -0,0 +1,14 @@ + + + + + + + Userscripts App + + +
+ + + + diff --git a/etc/App-iOS-WebView.zip b/etc/App-iOS-WebView.zip deleted file mode 100644 index 4986344401d3b7c43db9da5ca2531514dfdb3370..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 144962 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

#yCT%0`^@q!(x!*n7Swc!_=uylT?h1n#s*3t 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�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�}F;J;F?C4qnUXuRn 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 + import icon from "./img/icon.png"; + import logo from "./img/logo.svg?raw"; + + + + + diff --git a/src/app/app.css b/src/app/app.css new file mode 100644 index 00000000..fba748df --- /dev/null +++ b/src/app/app.css @@ -0,0 +1,37 @@ +html { + font-size: 100%; + height: 100vh; + overflow: hidden; +} + +body { + background-color: var(--color-bg-secondary); + color: var(--text-color-primary); + font: var(--text-medium); + height: 100%; + letter-spacing: var(--letter-spacing-medium); + position: relative; + text-rendering: optimizelegibility; + -webkit-font-smoothing: antialiased; +} + +@media screen and (height <= 400px) { + html { + font-size: 80%; + } + + p { + font: var(--text-large); + letter-spacing: var(--letter-spacing-large); + } +} + +@media screen and (height >= 700px) and (width >= 600px) { + html { + font-size: 150%; + } +} + +#app { + height: 100%; +} \ No newline at end of file diff --git a/src/app/img/icon.png b/src/app/img/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9fe96182de0e89765ffbef258c50c5a7d2d8b767 GIT binary patch literal 24309 zcmdSBX7nh{@(T9y<(uh#sBByohKrg3Rgg&3!p21T>SH~ z*kZ#^AH5Mb+J_Acp6)(;!us|pu}cSc?fO~AbpK1!zTbatK793;UmqX+{cTzH{Ilim0(FBD)?@ao<`w)%YCx7PG1EX`#4-M84Gj&kG;drIehWv|*f9STGSk zNwbkVsp$S>KCb|5_;7r^ON%I|v&kM=aY#4K>qs2_^m@SY0e{uI+d}0JKd!&c5JkM( z0O|DHc>ks1A8oEmk0s4MwnI+Ay`y z9*dHmIB$Zhr+=h+;oUP>?@%A`BSO1!(1=CTF|e4=qsAja_kRp(A_*xfpS138?CFLO zJJ^bWVxIG(#f^7(kv9$B}Qvpx+7 zy=~h?kYSEbfx$N4$E0*#$=lt;o+4bIm=2>T>gC6&Aq%q6TPux2Xb48TultEP|NFRI zPai6F5`$E%5FTOkZcfQ~myklFRZO}0Med6amhT(NCTX;$;`)}@M=Gl| zGAnqy(aceLzw}E5lfP(&KFsvzN(wTfcv-iQ1ha~i$nGDG-$Ytf1**6)#;RpO_7kRS z#z-pn^OXfv`pTiYxFg^5JK3{Sje;31(X9}`^;E2*e>cOZavRUC>|VJmJM5K z^`o%B-vhr=sx&zB3#Jxkyr$XPDrW|9s3v<#YGXQgYbAJ~{0n7h@z;cjx+KiNJ5JDx zC%zACeotKy-ep@e*N;x&EJJ)n!#;@6A_>>g z`@v!3>#sINVYbV~WBgfS?zM;YfXSYqAIJ)q+xG-T+0 zLBr&09Nm52#?+qi3nr^&g9^gFz%^*4^0y4+$}P(COywNRJvH>ZrRfJUeGwh;RehWe zONjZf;hhrq#YVYvM&DT9)ab3mY+(&nj`!KM@ftpC;C@MW=r4+1d|_ehiYv$)t1En zsN9iOCp?y>=RkSvp9k@_49G9VUH0nk)AevGlpc=HEM$QboY}9j*Awl0f@~>A- z6Y{ziPz6DN4wUm|yvU8{2IrzQ=VC|Kaffh4+d>k1dy(_hPLD9FwJY?APg$Heersgb z)(=1a#1*-8NDZ186me5IL;0qJ)6&;yUK)Xz?kBBeMn+!IX5`29Tc+5st*^~aF#&?S^f4Jxx z*trE-j}{IrokPQ;?XH-Gmn%{Q!{i!$`AE3yk5r}_>2~pkX&SoNCUkOudOn{Y=`UAg zW=AzP=ad*t+eyJ?6ni%%aTy}VVb^eJLJzu>Eo(FDG50m(-P=p0hv-FdpgIji$bjhY zz%}CA#r37>0xgwmR_COCww(0rj273eo+NEvLA`0cgPqi)8^YgA>NQgIEw5NJ)I8WR zx)$mlQ(NAs*UU7nWCA~lbh=F!aoIqkFq`L@RtCEtZEuPSfAr68>hzUGbrwD=WQhQ! zKR~y$uXX1{F1JZupcS)wOqLO4kMcAca`iS_z0Y-jIbu%DdV1E^dm;5KK_5m8v?82Kh3M%QO6(T>UyIPtSV!u&W$CIKYzxD_@iBJx)Tx?LUi$`07X*RLdpk z5i}RyOb>vkil5(^qkpU(>pc1D?5Q|(l{8N=beski@hBpxUn2Co7JeeoG(ql(E-l#i zX0tdio;~b;qmk;!p%Evm5$|DWZ5&KvNVi1_bv*%&ZiND zT~5@uXA5IrH9Bg7Mmt2`~;qBP1uT#J^lH>GhMm+eoI_q#n5p>H2M zG@vURCXM5^&_0iGO50sA2Ft8)6KDFjo~4%Vi;SK_WmRns)Ko}z@y=D;vh_eFB}Z#7 zj;h#OVM}_s**|ucSXJbvkE1RZWntGFQ%8%Ttj&H~=#voRhrYnA#&u-!h^U{OgFQ4L zSth1r$SSUos!qh0xvBWCRiFYSZe-;5KDHp;j><84S(u~MlRE7Ho|OMm^5R5(Y$v`t z^A^+G^w%=ef!4nGP;PHBs$Sr%%1iE+j&mbm%cm`%LxRN+4cddLGWu$4(29nGCC0lU z!&&rb99r6NW9_R3jMqttfCNo`p08CM{LXlJH(S`sn0H3b9X$@yWN9E5k9{WHw-YF8 zF$N{J>}Pbg@NUe`&Y}ia;l{~#vmTpuv~8SAL)TE?^bO~Czjo#D_Jt*inli@L^I26j zTiKJw?A(eD?zgJ=)|rlz77N!7ghquEoHH_W+VDu*EZ&TgW0i2)IxAaALr~oEc4;V} zyKp&hdofisFO8Wq4}I_&wX(81^K1I__s^h@tujgX_Qb#~+uQgfW^shtF_U+jQ z3A!Beq5qG4N0$>V`@g)@8+?$D^LzMQ(ey*x8frQsO8TVMA=svB*wSyCwe4n0xj<9R zFMXarFyj5%pMU?nVhi^}P>+!T#-xi+O)z&AVHq_rOXT+O1Z5Pl9A7^#@|gBp4m0EG zl?5LldNgd%#HkpuTX>bw^&Ieaj*W-(^LRBECU@laoRoC9LKIP2x6BHjgyF3r$XEsi z{ayo@IkphXTebXivz}OzcY%me76nt8xh#~Sn6j;x4)Rh8t02F@X7~JEzln&auN2kPjc&-qL5%G0a7Unlp3{;ll%Uf1lt=GMdrQ{)LvuhHK)8XS06qH*fuwbzK4 z*0y5TJZx@V)wGzSWqvk?+`fG9hb=f2DAk&dGr#tgAf*lyRgpKr)&G5l5*G7XAhDCy>B zMxIaXVHiU#J6IkOt;0szR&`(Za$j#nmYM>62N{a@k+L^qx9K<%d*$*TGxpCjj`r)q z&agQeG1p%qdS3hfv)i8Om@loedaS2a*KzxruphkkI%1@(t(24LrY7%L{oLU5!~rn* znU6w(#U0&mZ8802arrLd?$9U7@)4AD=5%_Tom8IOs^@>3(5562;~&%M2J)O+T11Z6C<^p+hr52YtXBpEP=G9^2%Oos=?$m4n zWAcY(Ur3ED1=iGCMmY~hJ!RE_9=57@<1!y6n@G=lIlT5fueR7@_$WuK{DYz@7<RfEI-_;-J%=tE zu#?8q{9>{HDNrry(&D_eUW>pskHzG0d6eVB-)R0~w+^i+newI+ml-4XUfb+4uQ6gy z%3E41&(j>IK6%01w=Z)VYh7q^!Bt2%>iNCox8TAtSx1%vTzOF;MK4ASl}ntwPphMT ziVckE;*-LhO}0Jd5!=8kwukgIS&HJP9QkcJJ^ZRtZGKfu#Z78^#A341;%HdC{;=eQ zWcSF%SV@o!^jo$FR%q5v$cd`Hn8p5fCCkQ2Y4oDCvU3qX!4C5hv5%78+IOAzBU#28 z^Ri$vNg^ZFI6)dV6yTczMV_F4;!VVB(6o|A=C)$!MinZXm)r}Gd;EweLfNyBO#;m) z7-?icyVz^C>+u*nJE*?;TT!`F#zmCe;K4`btvfB8A)^UnZoQxzskX=7Ed|>?smk&{ z&z-rG&Fmaqz%gT-cW`WpGiq9l*yX+yuz92=*-{KRaY~=R$M`-9l8t%P{wI0wF?qg0 zaa)c4;WWbB&{y=T8iHw=fw%RFTZpX93hWt*I-w^|)d#0NA^kno<#sOINb#nqB=~Xv z=5IuHKxwHKjox^y%i(U1O*)}LDV`Cz1U&wrXrJ+RO8~Gd_9-}FKJEWdmOQt{*YPgv`?Fhh=}Ml!6LoOn zX?(x9PXYwyW*Te2MHegt+r0y`Js+uOUbz31%U_T zu=nceq&TsPhOhZ-Z>7=oz$F~N2z;Z-f?0sUHu2p`4S3(gt|neZZ+WsM$e?jG#*T91 z@ww@rJ`kmUIOdgG2@2t}V$Zt=C;|cmeG++fwCZN439AOzck2!YeHTv`L=SceMvqUC zc#<>5eMwzTMAIFH9g~6eHr;hEif|~V^14+yme zlTTip+pL|EYUwX_DyY}LN}&G4>%}u$-|bYuA8zHXM*0kYj^s&)_1A!3#*y00HjR1Wcl9iiysh; zP7Ic#Le4Lg%wwbf?9S^evj&?^nrB8LN`YtZY|;eu5Ud_=Y)sIHLR7yNoywOVUKk5%Z;Av9mJ@N#sG! zNrpo2Kh;FbtaNHyLFox8_xomfk7d8168&q-+g|r2Uo|;t6vd5{R|uGI@A3InP{L4; zp`b1^^SW+$fKt6{!ZxZ&4AI~HUUlz|ebxw=8`ma$=b6dfE@Q86CQ*PhPEF z>&*MW7=yCTtThLTo~#uU#|PGemUeu@Tf6-s4Y$u0hZam8k12k7b}&Dl+4tQ7(N}k?b^2@Q>Z|@be%s9V>eyQSgj6MJdPIL+S|aDc$S2;6 z@lAk}SxPS=@v!J^MCMk2w}_RM&+AGqY6Lwh6RT`}a+= zzHebka0vFojpG1fkkvqz?14Y->Z_pzQtM8S;nmR`YN!xFk|(0%y{%{kj;db;=;)3cfLxbnTdCu6>=F{F5P8cxGMg;w0}d#F}FRq4Y$m~BFm+t;mlY3$i_ zmjb5Fp9(X8vO0p#W&pGpCtUCoZTQtxF76zuc>8(xLZn^jWbt+#yO+C|hqNCWcAI_O zn88`TBn*r0#Rb2}oD{XRhL`HfMfpwi%R`3!$&OQkbt)*WxWt<i|)vHPX#*PA}T#Q!cUiIb2Tatc#A?+mUpA5|9fEq1cnRxgoE zHm+_-9|*|TjF79|sjQmV%j(_)3wkc9TasP@ZKJpDmbtjf)HIG$1T*_(Y{D;Oc-RZS zy@04$KPHnI;%WW_3@xWVjPV@sRIg|xL>`zANg{R|;CCrOw8M{lG7nXj1?4mPz1g?A zcy%oUZ}$i3)R~;tei&gydaCA?X^+n%8jd{fnsaZ`;`}L9~ID)UoA4kJf#wDFNN!g zHtwyOv^R7@H+DTt%oxc`%7C(pG#s0k1%=&nQ&tPAVBS}Y@yy{O z56*`Q7C@<8AIN7{HC1=vd2PZ6a;~x3w%Q(8%auBnZ946&>aWeZSqLulJsaHAA76+t zW3QUDR{oI42Nz~@+aVj;4iYonepq%70vqr$EfaEX`!d?!Q--Dm`Lk#;K(&aH-ccbX zoKCW{9!9HtkzW3D*Uj@g7-uL$78LX{AgXU&32&5tr}Hkl#*{yLIDtyG@tX4d+JJ@J?dkd z4$OMc;X&ayMkJx{gKL!X>Xi$!`z|&!^=^>IwT-t z(e{TU=(n?t`23aNR0z}MYV4cV4l!S#AY>7R8b9`;wLsKk-&`?a>-+N<)QQ6-Zzj1rqW+y9pO8n=(alM&xMC`z3b^|5zdLl%q{XiWnBhg$xX z-mc;0TMEU5L=w9>D_15@I39Hj78}U)MJ}wKr|;D;5Km8VA}LJuwDdS_oA=*OO90wTKVNxmnukhVONE`rd#pQ^qely?yz|&k8xk4 zt>l=dWfk&s@QtN6c;kmlf6)o{@OnXtd*I-L44%8Y5GA1nGOkgmRTLIiF*(DEMyw|r zhlwb+BsWe}SVl^DV?JVvquTFWS$*K^bL;3-r8F&%au-Kpt?3Z;5Y6InVv7s=ql*FR zlShoJKGAK52P1%)o)*yAP`3=q{h{DFfX@E4B~VpsdoDIwtO(t#<1uWAxvZUw-V%6H zt%eo2O7jve`nE97;fWOZjevgHFyApvm3gn{`>q-Ew7o*AMi%Cf^0Gqz?@F1Bb;Ft* zzSe2xcz&!x#*UDdBtajL24SWQu<$(kCyz{%3AM5w$zmx_=4D7|f3;?Bw_I+WKSpo* zM7|Wet;olnP!&nnBr0_!APJy>gR)U%3orB5_{cc-hm1s31QX8dz8I^#m4sW648zER zXx4FkNvydYMiS|~A%vZTvfXE#+5xz#a3RtAmtOH4&of8x=ERNT_e)I%L+|Wx5VWD? zBStbx0&X~a0J-WzeUGe8*5S2(-ZKDL4S_L$9rE(Z*GTixp@4pZT!xUG^an8zf1Z-w z*%`U`S!b_1-Y~(xl4symUN;O(qSfb9@}_OSSqypKrN^Gsj3~c#Lk(8yq5YH@8ardKI+Zs_vbU|)9`TB~8b_qrBH^U0Q9fuKiicF9fl z3hHJ3@ii?b<+#X26SQ%YI>RMz_cUCV1tvofMJix z>4AC7$O!?-Hb-}CkJEX+Ca7zi0@0P2ZM+8|rQ2l_I<;8_FbJw@_KIgWz*Ptjguy7q zDc7z&VW(>QuiRz#CFyIU)=zq6LUN5EKIPv2vKp$tZ2jQS5{5F z(#8#?LM`p7^<({`Ws!G%Ns*hk`wL>+e#He8Tfw^aG@bhXeyF&z-@Vx6_hI9&>xCI{ zcVrCgOxfMXZYo1n*#@swg91ruT6T;m`5Y}DG$H!Cg+d|(jroU>lyjcOU?jpUL9GZZ zlRq8*TuvLlo>yXP+LC$zIYmufFJzg2icD1Zif*jzpV77W-;N-vBN;>X^_Ar5iyu6* z+9#{mv={M%jGxUZH3CLogdkXn9SY}B0P%c~2+USJJ&>)1K~8Br#*IW8+hG* z+1pFY!lebktE6YDbz+u7=!15GRy*<#^wMU}{i_e<$a<{Jd5ql2CR7Qtb6sNQ2Vzh~o2n+39e zJAwTk0yLjUn-Qngw=TafPu1MCLQj65Ipp}tQDwUWQSz#!(*qbQ9rd>q-~M9Yn||JG|)c3XW|ZFys_>2zSW2(b+j z$~LaNKxv&?#%|Ul0M%F50KqWLq~*h7J#E3bEPOR5$@#gQ|N7hOsN`niOXOFgb?8}H zkV58x$_c9@xkNvI`3SJk8)%Mv3_w>**O0SoY0XgIlB>wEaV=>H2Rh=v#S?X2v@w7jCy$U~ ziGnU-X2Zf=t>MGjs6H`<(0($}(RntxdX-8x_SCZoH4sqWR{NEPQUqnrB`!M!B`^5SE%@Wn<`ZTQ!q zuSE+*HVZnu8IL44V61r!W!9yxPgOL_J8CbGe`qbm{-{o_)z@L&qZzU?99j8LR=*)@ z?Ew~ZV`RuYA!Jg*bX+WJxGvyW$ytlP{Q=Q0WMNNO4X6S_pe9N3nj|0(11mB{_ixTM zyN3IPPAS&3syR}FmM%XuNZ1N!EqUs5;E^T2ng8HqwJmnG_BTSZD$aQA#IG!vtptkD-vm(}vr zfsVa)W4P|J0i4&07U5UtVkjO=<)U)Dm;#r&n% z!c>!P+j*MDiT1TFIOYzcGY6az$AL`zWM??(5J1}KaIiB&Z}b&Fsh&O1Zbcm3|4diH4QWTR#oKCn}Cm8{?ey}+GD8`d?%^X~ZIcV9V zC|~C5LwFtVQr_GLGe`ZmQkt1kv!6bs2{M=bly)YcIsNf2DpJNA;WQkY_n7qPCx7^v zgLV1|p#(;MZwjGL#!>_*<%U}?VB8mj!2Gj^e0>TC$1K_A@(tXaQ+^*x!l`tmhTzt= zvPE48%;Gdr$SI?nDUbWLlBCA0LMLVL1Kaw|I(%q2%}6Gyv_Y2JPR7+}BkMr}1ID+d zR4^)+Ti@5Kwtj50XWB*5HP-=ICfCfrXTl+0JeTyeU|w-b>k{={c6RI z+3G6wgqGCdStr_Yh{Y z>pyW!aJi_UvxJ+WLwsFFvI3WX4z+$?wY$H7oO)v(Nd)z^eL5#w3@Dnm(W%<}-kWOK zx5?pE{zz+nALrO$YsW9T7O)}antSMK+i1}?9PFD&6J814;I>1tf%U_qzBED4ciz#g zsZ`0P`2zeHCWK-d&|MP4ZhC;;yETnX+vcRll%NCc_V!DOE zG7w%tjqI#ACrNf7e5u}96g-<{z^vK_1Knv1P!0Nmqt%b0H&WY8y~cXuR#x?9cCMSH zG3~*##J0i~xe8JDMHL@mW|s~pClGKdR9ZV*-R>-XY;DrDgMss<)3E5f9Qg7Z(I5r8 zu>jy0iIWy_mZp=3DndqC9vQ1-WP#v_HY_6@zO)^=x#zajyCqzNG%^-@( z#>WWGivyma%`Yj|E1FN?1p+|h=}*F7XYtys$)JX!H^BKAxRw{ylHW1troB4y)Bl+P zH*sx6fKh$w?e(>0Y3#}Z(2(i9G&~A^ob0#Ya2PD+jo3z)C*k#kE7GEe2QrXs6YVO-0z^0C(m25v+*C*p3<$p#Jz_v$a zzqSHe=^5%CWWk?mDTdzRhet!#jdi5J)pGV&f1@|kPEc*+9C#}WJE?4XhVhn&&a$)e zY+rG$=*(1W)Fz5O{*f{Eh@`Q}(3~vGyzgqhe`^AunhV*Rp9{X7=2h1;lx#6_Id7s8 zkV=8R#Xf4=u+}16fq+qFUpljEycw?QZ75mPcqyvNB1ZAcmSH$I>uO2X#z6{Q&`pmP z+7daBZ!eU`VZ0IcK?VYIc7o&;@+=TXk@u@Os{j#{`NJPSh}9=}Z_s5;IdSnO+h@fd zIAUktmTeIIN-zVd!CmnzSSz%8$#;?(AolRtnSs5>QALfFJ|5KTuoE+?n#fZIPdIrt zz9r&}fu$gR$5crf6K6g0InhQ*^g6QbM3;O3C=)T7G-4wkp#))TFl{lhwEx@U( zz^wP#?D6pM70i$=#uMdVW2Qnd_GzeN8Z82;Jj^+_JICR)_gA~Knp5VDt+l)Q{HK^GQ zG43=dAod5F@NA)!l3>@aGEV1<%4#`4sQ`=>k6{=0rnhn*wU$c+DhThZP5$9o_-!ZM9A(qR`V{+L9Ow^_SBN-XxlGM%R|7O|}J9`mp#JRzv+v znc2gVai$e5I!_?r zuGY;ks^Y1>1rUmxm#N@GF*P+@gd3p*x!<0wxBoUnt}N!fBiMeF!*)v(iMgFWVkWBb zrOLP9QJroqXL0;M8Y0_hi-#BS_QT$@&mSeY;XIxHD1``H$=V)@_cI|tBD02kbyVS2 z8eY5T8}K1+O`iF7VE9tqLvOTTKU}{!#`Ql&r)~Z}uposZx40;HzehGqa5D4mO1;#b z9-C2K*GXFeK-fgl{A0bBD59CKLrgxO+ZJ7DG{8^Q5??R}^S^~_uO;KICtsDaZ=3!G z=-hvj?zu{&LD1}Y?@2}^qR~h`3g-^@0p^}LDsA5p)BK-EeS0&?!1qx<)JiFj;jhi7 zGKjp42sg{|9eJ&8wj8n$-tObhqFjl=tWW+(BHN5&W9D()!%KW##-m1sYuu=+kR@{4(sNGG z&ygvIng|ZqpSo#1Kz=8(u#Oj5k43830DJ@#h4?6h%gNlyfpb zgh6VoFW!IIkJJYD{%eyS*tP=VplQ0fsDu?Y<$|ohp=)0c8Tu)%C5{Wu8=8B}1akj1 zP&=<`H9YE*42x0`(gy~Cot?c&&gK*&e6s(z6}7j_b>*@K(e<`ZE+>f} z&fMU&*?(|NdGb$>_ldxr4y@qQo!E0+aqB{;B(VjQm@ad$0ZB}oGf#idC&O( z4zad|jRlrhxq$-g%_SATbf9PD;Vng!+UcTyL!p|cpB;PDsNDTq1I>2N*<<=(m%^5z zU+sod7fW&E7Tn`C<*?}+5qKNyNvuCSA{Y4{i`()ezvy%mo_itfTrSP$?gY44XA`D7 zr-tJ9uNKF7k0%9T9T6ozjK{&9_QO=UZF*T-c%rV!7f5ITMKCtUX_?-}$hF6efB z6ME~F=$f1fw-Jay(82FRhn%|zPj6tSbWUkcC2?PNAHegzM(d5G_pA9fZWvDk=LFph zKk~qu;RSCVeUQ_n3q>9*D$}wfr;-<^X6v^FSThQ=T}rr{J#UBOAA$xr6W%*&`{cUL z?4j_sZr+Kxnj!N_>x_{O0Li*yid*C!PU(<_ZV7}?yBNW?C|a>MxD2m#pr2^1i!~82 zj-t-?lzRf{pt}cHcd*o^GT>0&r!7REc0P&ECqOwa?!I*lT|S^!>N${>B79Zg&RR;w z4*?<%eF)24wE_ zsA<(>K1Q;uiH@^-NbR^_*<0Vg27warrYvJwpU_qXn7;shuw}gMnujzG5H=mm_xj+> zUJ^P94#E@V$xc=7vc27@=oOQ)8(*tnpd)9$i2spx;HD+BK*c!qiPdV4(IlzP%IL}s zOQu8$9!9_b+T~B@p9nZG5of_pIBlmtRZ{dO5yUx$^&~sQZvS+SE_TjNy+B~K9dNiH z+x|03rEcQVwWT)Rh;X2}K)~%f-{@MN#o1hS?5R5<@SJv4&BFp>x<;D@Vu<9TahFu` zI~@hI$M9Tj{Q~Cs7P&`)G)dAqT#HL+hTd-lRc|nY&4y)6y>#vwFJqC5p#WTB~EncGLc^$=LD|7K^ejAj7~2MX9pKe(M*jaCF8I^jy2AW_=} zZaq{ijeTVK(z(ktPxz))Q$Y`0PrdA|LUe0ogC7Hl$A%Xc9kXW3!4);c?M3(E%wcEZ z3C3EsG2Pth{Ms1ZZN`_Y;a6J54_^QSS2TVUNQ%vmknekoBA7Qu(#PjYhifmf)M8VW zsx0^)8ebAgTEiagd%}!hORdO?rhj0;0V}f0R|UuNXdBpM*JMpTYSIq6lk59Q>&=Kc zWJ;bg2a!U?EEdCdp@M~W-IrQ|KxFd|%i;9KFBH{GAbie=%Ky|R+R@ZD!X!-p&bZp6 z{yZRi663_VubNd5Y=s%{O-rfMYa-i`oiIa}r`FYVy>9U9G(j0DU+bKXFrMv50+EOQ zkpx=*Pst}U9+w?wGBP3c6QEADW!uTEDLG3W4*UY$5Wm3Nca#v==Bv$*6ieIY!ZRfNJ|%91nC* zro$4{hd6CA^?Ff>w3fK_!Li}N29TTY)5$qO*<{tL2dLJxz)(fDc!R>2^LO`);gyQLl%PQ-OrBEKwpA33DxL;&XRbC@bDK8H+ zOxlh6@(L#e-{sL5^VC{i+N@B`>-$lC)Ydx0`}*iz9stwsC7Mkhy5RFJ=5T?!XW{IZ zRozcdJxO{0AO*L7GFMbR2|&lv`_Zm8z$_Eb&^^J+Rf1l`E)iyZH;%(+tgzbxxu5Xm zu=8u%INE*pyvH+Fa5EDW<7{AA%+1H8n2h0({4L@i(CcZO?-5+&b#~J~@_^F6fx|RZo~H1l{itS7!oB|@ij3!f0uDHtR(2kM@STn-w9FM|A&Vmxmh~KZnkbw~@8AF8JQ@CO=2>OKy~$VzyPrXo!RZM@4hY| zAjDKS^NZndz-6cmPc{8tT?TVecoiqvMM`w+o(Vt@%#6SpGai_ibbegwO!*vN z=|n}~ZCr}p>_m`Iev<3I|J^VXwyW}xdsC)jhO>G2O0+I1e5xz!UAEDU8j_Rg6nO@^ zt7gk}*GLZg7te(TW~wBJ#Zd+^sxvqhwkjnnE|&CVeS-qEPP+o_w@Iv0&a9Q+n zOE1(;jfVa*V-fq9q2`$7O5)68R)=Sni|W01xK#DZ(yj{*-XFTJ-GmLD88TgOWKiY2 z_Xd*?Ho-~#Db?t@XN#mKL!Fs$PoT8KF?6nGyY3PwuF|(h=`oXFl-Rf;n&XiYJY3;s z$1H8v`wh2MgA3`yNh?qsUmNjQqLZC|N&)+Eq|}V6fU$AHOk+o8aZWb6_4Rare3gzC zAx0ZkPjz|G0O;|qT!tzrBkWO;qgX|;Ogp;SH%;*CfxVv`#6tiBrkbN;z`W+6r&LqI|K{2#1;@43xDAijO=Ee zw2`W#8#=hG;ZKVIoEyp=78-`yk&k~Iy)v{^{HnlbzftV^^V_uASGVi?l^$A>!usoO z$(OkU9yGbGreV>i=JT43@N@IomEd5aCq7T~?XxV3G+6I; zO#FPC&*=WDLi6I)f0w-6ZhSlx@T|}=yMAOwGh-R!3ctMen-N&EGd^@2t_3m`snG<+ zWz*Nx@}<_=g;t;F^P4S}U6T|ef{Hfn#UouW;%a^V-44JSH*BiQA$h5?F9b-GpY}gl zJ}l2rpUDm91NpdT=I-jV$A!!M(q*O)6;>Pk9M`yt37DGxVzG02j|Y(C5syxOy`9Y{ z_+>d$++dn**C^DWTyP4Sm?R3h2Smn*;MSx`bG_ym8~5l?WNx2dEW|(0(BHhpNx-xM z)B}5>5x+IbJhSV|4ujP8)s*Xa=YG2P)M?!#81gyVkTVqhK>m57BG#y+FxM*}KU;f_ z%SB{JK)KDS4bMX;5m3Q|CHFTGFzB^@U%PjF_Kxz&KlpJ+4>WNtq5SU^Z$);~&swxj ztzLm56AEH0V*~01tr^J0Vy%c&?X}dX#d9|moy*A%HPz*St)x#t! zuJreKO%(Z)3eHRHcUQd9Xw&0Tm{^cS7>9=&wZ22D7Ix+Pn(;o^C;>A-h)}S3K}yAw zeNi4O0gVW~H8-0TlB{Yq~j%0*QlziZ%VIy^D_+-h*B zv&86IO^Mp_LbYT9ycVf!)D~|tLHv=m)VIw)08B)PR^G}%=!-VhFyl;Uf)9CCR5&3S z#z2fh&TJPdM%|erZ11?pO8`@N{g<>U&mX?FfMi5EJK{G?@1IF1iM_O~hJ~_e9A6qB zEd7mnJhR6C+#fj<8g$|@IC&LqiL9^Cg|b{!?~*=fYz3@#Ae4PDX>`XH(>tUOs`Ika zKUfXw5MFUkj2}RE)x_(beeJ^A2h_@}4l!x$#MR*4gLu^u7R($d0#KJ{(`qMUtEk z>*$XHUr2(@i0LZXKn=dC%m3K;cxt^v+(UaYy`s5RI1B`91N3|v6e)4uRzAC%^-sza z`R?a2Nw=cZi=;D0;7P#yk4JAXiMTrn7^~678~4fO1ERW{KD6kP##fxK8e2lOWSCl} zO>TT7VeZx73WQkxZ=~&3ND>|WTNW{})ZD!T?7i~qM1g!dVE!vT-+ zGUQ?f%M_&y2zK7eE5R&-rF)&gJmsS50**DEh)7R7(7|{H^i0{B31HDpZOV5c2&Ce$ z^Irgc71cbhvggpe_>Z_hWm7EO)c;4Y?J}Cz4*0aJh==4drB=gr`3(Ckr$?SZ9MvD` z>`JWG@kh1Ve@KkbjN1_c*7jdvuVknu+h-X*+PSEy$FXDQ{5;5?LV*ZSF-d#c5o_*ugpXe|Op$T4XmOxoF!Fpj_q;{Y|fHKhR zlDiFd@0G5LmUlU*eV!IBQ&8%6J?oh0U^Xh=yyxzk8s+tpQX<_c6s`#}XqdN&AIKx?HEXf%!i_%!02?^JVOW zZe`o&s;Si;d}MTEaZ+%H&;DLxc@3{_ri=k>*mNps(nundp{h&e77rI>#Ffo}8)*xY z9*3(-C7Y7lrhi8%51xnrmLKz|OJQzjiH$$MGD#j`-Buaz%g|^B78h*)r%)X8NENrB z2Es^O=y{VQ32Xb9mN7D4ldKIy0BByHb*7>VQ>%7f6w|yERbIVn<*B@*z*B9A1kx?V z0>5ms%NivL7VZa3q1^<=@jWcha->_^$IZLVgqj0D*H?%oc4z+Ht6HZ&jbJ=&dLnS7zDeI*g8 z^rS?LbQB1{x%#k)gJ1~~U>=X0N!^)#)UKC3srqTN9j zl&rnlZw7CnbFpxr^jiDR8paRd#Z~s5Kjp_38?6EVZSK1327;lheDWooCfS&~ zY&94d*S2w5G)bEIJ?MQIYuCfUZ-M{>AaM_Bt31S*bfoijfDw9M${$$nv;Ae69&y$M zN#E1NS(@e0wu$ehN%dY+DOI8pgA?s5FyJpGqD0i-m$wvmls6y9b6HUO#Ko7ss5jX! zi#vctH3Tmdu)Mq^5CLq*dp z(u<@E8^K(b-`?q=KAyN#BYwH=|ElK9!;;STJ+3uI9V^B>g@sGyB-7Mn=E{iD#(A@u&ZN6=!4em9xpd@js0XO9K_Ab7SiEf}@?v7rqH;RXOq< z_9#QYLSCwU){TD~db^59H;EH91DS}vMm0uKb%l$M$)(bbbZ_qNgMNKey?f#LIPKTX z4|^dFxBZ=4p5r{yT9PTa^%WU!qzy|u(da7VVqx8=BpW1cZW=7##O*HOxwz6+3LI-L zMNJpF5DmhDUw|#Ye$80+i?Cd~$*{W=POX~RaI=3-Rr_P3yqWHmP#1%~%~vhUOPk(z zV~A_D=Qz`i_vYZdajuO*l}G=oG?b~#{<&zKhX$NFAU_~ilIE6^7zH9xF4S;Gf?>^IcCufTpcY$w zO-&ox>k_W?==zto!%h@n3j(z;QpT|YoclP)Xkey4NDG!E@7h3cqH@+_JP^-9T@DTa zK8j%N6M6dZAI$^vHBMgx(_q>Ret#{@qAKXagd_D8bOFvCl1`aJyDw+&sQy8*gL+>@ zesmG*?zRZBgMDY-#}#{oC-56VX!MNj;H072^W7&>LDsmPy#PZhSjlF z_qGboZshtou;{({&>)w>sb5{A^SCGDT?gq*U1#~;EThH@oUN?UtoA{UiO9% zSl(#XwscXVx~V1LFcFwyzl7)jPEnd@!LxS5pwXvib&SK+OXx5diE2E7f5cohXjmc7|_@#ZRMxix~G6%gRCYv6hPdrdVbcHLUH8=vlygX|}>nuA+Ko>QN@ zzm9<G^ByKm*B3Gp3=p}nBuBCt(?>d36 z?>mG`SLVj=lb$@m85zx2+f%!?h_kBm$BB5XY&_Q2VfdVHVYk^4Y}r3K%&8KI10N#PL;(9RIdf2af* zrY>Sd&9EWY2oowWT>|3M*ah`;no9jeY$)Ar@j>sShJ`ZM6D66R`nqHc>D@8roUsIu zzmZ~ci6%bJ&haj!7h zzlU3wCf&a%j63z0)Y#6U2u-jk-pf4*h2auO{y~3`82OXqrp#(WguxWm#ax*y;oqUv z`-zzW3e+l>D3TQ=+%O9K@BJDjKI!F4=EgV!+zn!f?@Q#?RBPCfZeER5X&es*;LC;j zwz>0XUV%1?Y7f7-L3s6-hQ1pk~D~WvX%H;D-*JNo$jnTFHfBjVJUIXyGGfU1l@e?D>Yq$}1UP1wxhPciS;a z%Tn>5#LNpTY3EDifEOsF0c0t-8(+*!1>vA36{y*hE^?X^-hM!V7T|`xZz_lIOhTY5 zgtq4TNjJ3t#IRG0k#RV1>_yDkwvLaXFB`K9vJZsqx2n+Ty8W};#$8B2$ZT8YCT>HJ zQXcndIAPAf0iAh~n}K)Yyv9GPa04escv|_0?sUglPO7VI^;@St>GGDugwwINZ)U@n zJ;uY5Itntc4^`#FSoDgaj&oOAb_H;DzOFi*`N-Nyb%<-c4l=KOE59@hVe!Mh< z7!A9ey%O3#D%hutr!B{ay&buxZgt6H^+26k7O13wmD5TiZf|%6`Pt$mNcDL|BCXF5!R$W5*z;Q z&f$9>)c4tA6R7P!BlQ`+05%DM2;Gv+{kHzP)C$Rw%(eC2D)@}^@#zpWWAC!)C zzO_8Qa-&E@E9UMQT_TKzQN;CIsmK*t0Ov zE|ZX+fvc_PV< zlE0IepAWu|@;Md^%9&po(f z#Tf^Gk-UK!rAOdV2Hx}=TfPYvdD>?xoygK2o^pn%NT4W#)yK3N-Wy1}AuYg0a-&D> zNY?OJd}(hpO=4s2pwq3_y2I&73o&!-V|!zs8sjZd@qvsTo7~~Zg}EYSe9f%l4Y$P@ zK>F~M$T%{o6|k6&O1=d~VH2;}z|vQAlfh;;Ch%2@uluY@o1}Y4-eYXjvp*f}BCCG?qkmRVSrD`1!X!)O!lE8UUpBK+t#TO;A2Fbrh}SQDVENafH9 zw#TAl!>qizY>3sfrpT*^@3qLtf_uC`-fRHrYa z#igWr=?-Xk+Xr@5RsUsM8FnE3;p2_ICWD& z9A4olRj?uf#As>Ve5(zbq*k|tW!ouR1`|WDY7-Zq*6nILJNKum$?O(cT0oZG6_+$1 z2i-DfZYD$icDIW(Z25a|N-WAhyr@vsPq2^`YwP#?x6vPoK>hVklLi0wh{xi|km&1E zv$C}DmOha)8+8rebfXvab45*Y*p}Yt(V;9^)@rJ#zelvuHaGI^nlQ%DaG0Y8YwiiQ zYoD4j6r_0N9;&*?bru~{-0!;6mvwCLy~AJ_?iRVC310C&jCWhgqP?Wa(#g(z_lsog z-sd$Uu(nU9+mk~HMwvXR z5Ff7MqQ|vJ%eB8t>Xf%$7?ej6r$WsP)US1MwHIf1&es5%W=7>0!-l}1k#7vs(LSXY zS!)<(6oKn`uV<9Li8wvL)|+U2caXWEh-5HSI=ak@D($aUJZm0R?+po%8Nkn3;W8SLHglf&h#!yO$Lzs>l{VmGe*%O4x2ntomY~ zfXe84gm`eH`K^skVxf6qfNpTxWqv|&hPVl&d?_}?q2lXDD=WWHGQ>D3S>#k1+RUoN zjs}CTDj&m&CuS{2L8eXwa5N6i$tx|MA^Xge`r>0sNO!rKw>Xj5iXCh{$dg(GJ2aV>3P23z<)n_jLQ)7GoanmTXb% zU`qTxJ;VDh+V};5S1jLu=^O||6D5=B*ku5GEFVM5yU}uK3!l!cNB@MNjf$4^juU{ml5~n60k7>A6Pj4^bhL zO*EOJ)?gSS-sb9FeP6-kD5o(rX*I%jA&x`@)Lk?2!UUaZM|6k=Z?4BtVUa%j)FPO& zUSC?KH)`RbGoep5-ncM6c9z|J(}~5+Cw^wr8fr>#}+*^fzP$AN9gYkN{Mag9+*}V_=p#1JnKF`Wl57ji2?*sV9s|c zLeDc+QumL4Y*M0Jcyxa zUK_{nxWGBoz)ZwSR4<+G7|H4e#bseN-Ex-Z-G1MJsiY5j-zWJ@+)S4K9=2@Z_abNO zIEct3O`LRBCbvH>xLj9t#-PI6I%{fs*`;ZMNiPj6VcRGq{K^&O3wgH)TQ4dOoNVHL z!}TC*=Wu05M>$OHN-TV26=-n$hJ6A*ovA7G;h1Y0YydAtee6)D&9;XAoQ+wX(LNKQ zOF+(}@r*31>FzAoSj3cizn2Ah1*1Ef`v6iFaaSh;5jFE}w;;$-f)GkDz%&c_iyWI@&->5Kj~zd@9bxipT<^x` z7H#)ORNLY6U$jre5ywZvm9BI90u4UOy;cIKc$Ha0dW$ZS99*fHg|OB!_bgL2(-5{y z5pvMxOKJ7?`D^4V0)D)*I@B8t>UpY!Q#+Gks~jTq)hAjGH)yIm?igsd?#wx$8%6>F z;`4m?z5MdP?Kz%;cykgSjRVA3t{*5UJ<{u zbJd1pZil9XUS0Xc1%dR%f9qVj7>KFt+adiC_Ux+!hd>4U$X-d{m-`!9?7W{I$u3s} zce(d{_pBQpJ*Ya_^bGpCe3T)`eFZj%*dksT%+h1b}c)-PKqu9_zAq}Z+M;wp)iymfG;lfFCaeSLv z*eai;&*RB?TO{ij2{uXPtNTSa-s}|SiF0Gx6Ln@181V=D{5&&B>t7|=B#Ocsv`uzJ z;6}V7ySyX43j0&_hERT`KZSQLiRSMN_Q&gFM3wyLc)`)%F*IuWeS)Lj5B~Pkz;jF; zB0zq3uw8ignU&La+#xzjMWD0?lrVn!+}i%xI)?P9IqmMm`Q8U-VJC*pc(K}m*`7*e zZJapvgOk^E+p8r{f_ak>^8Z^|`f$jY60)kZ>gje*? \ No newline at end of file diff --git a/src/app/main.js b/src/app/main.js new file mode 100644 index 00000000..d7459fa0 --- /dev/null +++ b/src/app/main.js @@ -0,0 +1,10 @@ +import "./reset.css"; +import "./variables.css"; +import "./app.css"; +import App from "./App.svelte"; + +const app = new App({ + target: document.getElementById("app") +}); + +export default app; diff --git a/src/app/reset.css b/src/app/reset.css new file mode 100644 index 00000000..6c181e6b --- /dev/null +++ b/src/app/reset.css @@ -0,0 +1,44 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + +body, +p { + margin: 0; +} + +ul { + list-style: none; + margin: 0; + padding: 0; +} + +button, +input, +select, +textarea { + font-family: inherit; + font-size: 100%; + line-height: inherit; + margin: 0; +} + +button, +input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; +} + +::-webkit-search-decoration, +::-webkit-search-cancel-button, +::-webkit-search-results-button, +::-webkit-search-results-decoration { + -webkit-appearance: none; +} + +img { + border-style: none; +} diff --git a/src/app/scripts.js b/src/app/scripts.js new file mode 100644 index 00000000..1b559fa4 --- /dev/null +++ b/src/app/scripts.js @@ -0,0 +1,15 @@ +/* eslint-disable no-undef */ +/* eslint-disable no-unused-vars */ +const directory = document.querySelector("#directory"); +const button = document.querySelector("#set_directory"); +const version = document.querySelector("#version"); +const build = document.querySelector("#build"); +const setDirectory = () => webkit.messageHandlers.controller.postMessage("SET_READ_LOCATION"); +function printDirectory(location) { + directory.innerText = location; +} +function printVersion(v, b) { + version.innerText = v; + build.innerText = b; +} +button.addEventListener("click", setDirectory); diff --git a/src/app/variables.css b/src/app/variables.css new file mode 100644 index 00000000..acdc671d --- /dev/null +++ b/src/app/variables.css @@ -0,0 +1,51 @@ +:root { + --border-radius: 0.188rem; + --box-shadow: 0 0.5rem 1rem 0 rgb(0 0 0 / 0.25); + --color-bg-primary: #323639; + --color-bg-secondary: #2f3337; + --color-script-highlighted: #364049; /* rgba(116, 178, 235, 0.1); */ + --color-black: #1d2023; + --color-blue: #74b1eb; + --color-green: #60f36c; + --color-grey: rgb(255 255 255 / 0.15); + --color-red: #ff453a; + --color-yellow: #e4f360; + --letter-spacing-large: -0.031rem; + --letter-spacing-default: -0.029rem; + --letter-spacing-medium: -0.018rem; + --letter-spacing-small: -0.008rem; + --opacity-disabled: 0.3; + --text-color-primary: rgb(255 255 255); + --text-color-secondary: rgb(255 255 255 / 0.65); + --text-color-disabled: rgb(255 255 255 / 0.4); + --font-family: system-ui, -apple-system, "Helvetica Neue", "Helvetica", sans-serif; + --text-default: 1rem/1.5rem var(--font-family); + --text-large: 1.25rem/1.5rem var(--font-family); + --text-medium: 0.875rem/1.313rem var(--font-family); + --text-small: 0.719rem/1rem var(--font-family); + + /* editor variables */ + --editor-font: monaco, monospace; + --editor-font-size: 14px; + --editor-line-height: 24px; + --editor-invisible: rgb(255 255 255 / 0.15); + --editor-active-line: var(--color-bg-secondary); + --editor-selected-bg: rgb(116 178 235 / 0.35); + --editor-matched-highlight: rgb(116 178 235 / 0.2); + --editor-search-highlight: rgb(255 166 0 / 0.3); + --editor-number: #77e26a; + --editor-comment: rgb(255 255 255 / 0.35); + --editor-def: #efc371; + --editor-default: #cdcfd1; + --editor-keyword: #96c3ed; + --editor-atom: #59ebf5; + --editor-operator: #8c99a7; + --editor-property: #e86c8a; + --editor-string: #f5eea2; + --editor-string-2: #cdabff; + --editor-error: var(--color-red); + --editor-cursor: #e3e7eb; + --editor-matching-bracket-color: #fff; + --editor-matching-bracket-border: var(--editor-number); + --editor-non-matching-bracket: var(--editor-error); +} diff --git a/src/dev/App.svelte b/src/dev/App.svelte index d5876046..1d9c1db6 100644 --- a/src/dev/App.svelte +++ b/src/dev/App.svelte @@ -11,6 +11,7 @@

Userscripts dev

+ native-app, action-popup, extension-page

From b3295e20bbc233a878416a150247f0fa5272b359 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Fri, 13 Oct 2023 18:48:51 +0800 Subject: [PATCH 028/315] refactor: migrate ios webview to app-shared group --- .../Base.lproj/Main.html | 0 .../Resources/Icon.png | Bin .../ViewController.swift | 0 xcode/Userscripts.xcodeproj/project.pbxproj | 22 +++++++++--------- 4 files changed, 11 insertions(+), 11 deletions(-) rename xcode/{App-iOS => App-Shared}/Base.lproj/Main.html (100%) rename xcode/{App-iOS => App-Shared}/Resources/Icon.png (100%) rename xcode/{App-iOS => App-Shared}/ViewController.swift (100%) diff --git a/xcode/App-iOS/Base.lproj/Main.html b/xcode/App-Shared/Base.lproj/Main.html similarity index 100% rename from xcode/App-iOS/Base.lproj/Main.html rename to xcode/App-Shared/Base.lproj/Main.html diff --git a/xcode/App-iOS/Resources/Icon.png b/xcode/App-Shared/Resources/Icon.png similarity index 100% rename from xcode/App-iOS/Resources/Icon.png rename to xcode/App-Shared/Resources/Icon.png diff --git a/xcode/App-iOS/ViewController.swift b/xcode/App-Shared/ViewController.swift similarity index 100% rename from xcode/App-iOS/ViewController.swift rename to xcode/App-Shared/ViewController.swift diff --git a/xcode/Userscripts.xcodeproj/project.pbxproj b/xcode/Userscripts.xcodeproj/project.pbxproj index 4f17eb62..f12ffc6c 100644 --- a/xcode/Userscripts.xcodeproj/project.pbxproj +++ b/xcode/Userscripts.xcodeproj/project.pbxproj @@ -190,10 +190,21 @@ 0316A9992AD7709600AAEC8A /* App-Shared */ = { isa = PBXGroup; children = ( + 039F59512AD95485002E9977 /* Resources */, + 4A4CF6E9270A38BD00111584 /* ViewController.swift */, ); path = "App-Shared"; sourceTree = ""; }; + 039F59512AD95485002E9977 /* Resources */ = { + isa = PBXGroup; + children = ( + 4A4CF6E3270A38BD00111584 /* Icon.png */, + 4A4CF6E0270A38BD00111584 /* Main.html */, + ); + path = Resources; + sourceTree = ""; + }; 4A143AAA279DE6FF0029BFD0 /* Tests-Mac */ = { isa = PBXGroup; children = ( @@ -208,11 +219,9 @@ 4A4CF6F2270A38BF00111584 /* Info.plist */, 4A4CF718270A38C700111584 /* App.entitlements */, 030C64F52AC62CC900548FBD /* Settings.bundle */, - 4A4CF6DF270A38BD00111584 /* Resources */, 4A4CF6F1270A38BF00111584 /* Assets.xcassets */, 4A4CF6DD270A38BD00111584 /* AppDelegate.swift */, 4A4CF6E7270A38BD00111584 /* SceneDelegate.swift */, - 4A4CF6E9270A38BD00111584 /* ViewController.swift */, 030C64FB2AD0B24C00548FBD /* Initialization.swift */, 4A4CF6EB270A38BD00111584 /* LaunchScreen.storyboard */, 4A4CF6EE270A38BD00111584 /* Main.storyboard */, @@ -220,15 +229,6 @@ path = "App-iOS"; sourceTree = ""; }; - 4A4CF6DF270A38BD00111584 /* Resources */ = { - isa = PBXGroup; - children = ( - 4A4CF6E3270A38BD00111584 /* Icon.png */, - 4A4CF6E0270A38BD00111584 /* Main.html */, - ); - path = Resources; - sourceTree = ""; - }; 4A57B9E3227235CD008A9763 = { isa = PBXGroup; children = ( From 3470b8434fd6300d495e90fba643ae560efc61e4 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sun, 15 Oct 2023 17:50:49 +0800 Subject: [PATCH 029/315] refactor(preferences): centralize common functions to simplify calls --- xcode/App-iOS/Initialization.swift | 6 ++++-- xcode/Ext-Safari/Functions.swift | 5 ++++- xcode/Shared/Preferences.swift | 21 +++++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/xcode/App-iOS/Initialization.swift b/xcode/App-iOS/Initialization.swift index 394e054e..ab6f6e9f 100644 --- a/xcode/App-iOS/Initialization.swift +++ b/xcode/App-iOS/Initialization.swift @@ -38,10 +38,12 @@ private func createDemoScript() { func initializeFirstStart() { // set the scripts directory to the app document on first use - if Preferences.scriptsDirectoryUrl == getDefaultScriptsDirectoryUrl() { + if isCurrentDefaultScriptsDirectory() { logger?.info("\(#function, privacy: .public) - Initialize default directory") Preferences.scriptsDirectoryUrl = getDocumentsDirectory() } // put a visible file to display the documents directory in files app - createDemoScript() + if isCurrentInitialScriptsDirectory() { + createDemoScript() + } } diff --git a/xcode/Ext-Safari/Functions.swift b/xcode/Ext-Safari/Functions.swift index 6f0c9053..0b7b7ba4 100644 --- a/xcode/Ext-Safari/Functions.swift +++ b/xcode/Ext-Safari/Functions.swift @@ -54,7 +54,10 @@ func normalizeWeight(_ weight: String) -> String { func getSaveLocation() -> URL? { #if os(iOS) - if Preferences.scriptsDirectoryUrl == getDefaultScriptsDirectoryUrl() { return nil } + if isCurrentDefaultScriptsDirectory() { + logger?.info("\(#function, privacy: .public) - Uninitialized save location") + return nil + } #endif let url = Preferences.scriptsDirectoryUrl logger?.debug("\(#function, privacy: .public) - \(url, privacy: .public)") diff --git a/xcode/Shared/Preferences.swift b/xcode/Shared/Preferences.swift index 2ca9c634..7fe6a4f5 100644 --- a/xcode/Shared/Preferences.swift +++ b/xcode/Shared/Preferences.swift @@ -29,6 +29,27 @@ func getDefaultScriptsDirectoryUrl() -> URL { return url } +func isCurrentDefaultScriptsDirectory() -> Bool { + return Preferences.scriptsDirectoryUrl == getDefaultScriptsDirectoryUrl() +} + +func isCurrentInitialScriptsDirectory() -> Bool { + let url = Preferences.scriptsDirectoryUrl.standardizedFileURL + return url == getDocumentsDirectory().standardizedFileURL +} + +func getCurrentScriptsDirectoryString() -> String { + let url = Preferences.scriptsDirectoryUrl.standardizedFileURL + if url == getDocumentsDirectory().standardizedFileURL { + return "Userscripts App Documents" + } + if #available(macOS 13.0, iOS 16.0, *) { + return url.path(percentEncoded: false) + } else { + return url.path.removingPercentEncoding ?? url.path + } +} + // https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties#Property-Wrappers // https://docs.swift.org/swift-book/documentation/the-swift-programming-language/generics/ @propertyWrapper From ba8d8b6d7e7de3dda844b57069e991e9877b240a Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sun, 15 Oct 2023 22:41:38 +0800 Subject: [PATCH 030/315] fix: correctly returns xhr errors when type is blob porting commit fa2641b from pr #558 fix issue #555 --- src/ext/background/main.js | 27 +++++++++++++++------------ src/ext/content-scripts/main.js | 31 +++++++++++++++++-------------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/ext/background/main.js b/src/ext/background/main.js index 75ce63d6..d8a57ee3 100644 --- a/src/ext/background/main.js +++ b/src/ext/background/main.js @@ -404,18 +404,21 @@ function handleMessage(request, sender, sendResponse) { if (["", "text"].indexOf(xhr.responseType) !== -1) { x.responseText = xhr.responseText; } - // need to convert arraybuffer data to postMessage - if (xhr.responseType === "arraybuffer") { - const arr = Array.from(new Uint8Array(xhr.response)); - x.response = arr; - } - // need to blob arraybuffer data to postMessage - if (xhr.responseType === "blob") { - const base64data = await readAsDataURL(xhr.response); - x.response = { - data: base64data, - type: xhr.responseType - }; + // only process when xhr is complete and data exist + if (xhr.readyState === 4 && xhr.response !== null) { + // need to convert arraybuffer data to postMessage + if (xhr.responseType === "arraybuffer") { + const arr = Array.from(new Uint8Array(xhr.response)); + x.response = arr; + } + // need to convert blob data to postMessage + if (xhr.responseType === "blob") { + const base64data = await readAsDataURL(xhr.response); + x.response = { + data: base64data, + type: xhr.responseType + }; + } } port.postMessage({name: e, event, response: x}); }; diff --git a/src/ext/content-scripts/main.js b/src/ext/content-scripts/main.js index 2060a321..311b1eb4 100644 --- a/src/ext/content-scripts/main.js +++ b/src/ext/content-scripts/main.js @@ -160,21 +160,24 @@ const apis = { ) { // process xhr response const r = msg.response; - if (r.responseType === "arraybuffer") { - // arraybuffer responses had their data converted in background - // convert it back to arraybuffer - try { - const buffer = new Uint8Array(r.response).buffer; - r.response = buffer; - } catch (err) { - console.error("error parsing xhr arraybuffer", err); + // only process when xhr is complete and data exist + if (r.readyState === 4 && r.response !== null) { + if (r.responseType === "arraybuffer") { + // arraybuffer responses had their data converted in background + // convert it back to arraybuffer + try { + const buffer = new Uint8Array(r.response).buffer; + r.response = buffer; + } catch (err) { + console.error("error parsing xhr arraybuffer", err); + } + } else if (r.responseType === "blob" && r.response.data) { + // blob responses had their data converted in background + // convert it back to blob + const resp = await fetch(r.response.data); + const b = await resp.blob(); + r.response = b; } - } else if (r.responseType === "blob" && r.response.data) { - // blob responses had their data converted in background - // convert it back to blob - const resp = await fetch(r.response.data); - const b = await resp.blob(); - r.response = b; } // call userscript method details[msg.name](msg.response); From 69bb257f82f451ea44f20a41ead2a745d0ceb3b0 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Wed, 18 Oct 2023 06:18:56 +0800 Subject: [PATCH 031/315] refactor(app): update loading of ios app webview --- entry-app-webview.html | 4 +- src/app/App.svelte | 37 +++++++++++-- src/app/scripts.js | 15 ------ xcode/App-Shared/Base.lproj/Main.html | 1 - xcode/App-Shared/Resources/Icon.png | Bin 24309 -> 0 bytes xcode/App-Shared/USchemeHandler.swift | 57 ++++++++++++++++++++ xcode/App-Shared/ViewController.swift | 55 +++++++++++-------- xcode/Userscripts.xcodeproj/project.pbxproj | 24 +++------ 8 files changed, 132 insertions(+), 61 deletions(-) delete mode 100644 src/app/scripts.js delete mode 100644 xcode/App-Shared/Base.lproj/Main.html delete mode 100644 xcode/App-Shared/Resources/Icon.png create mode 100644 xcode/App-Shared/USchemeHandler.swift diff --git a/entry-app-webview.html b/entry-app-webview.html index 62b414e7..10167879 100644 --- a/entry-app-webview.html +++ b/entry-app-webview.html @@ -2,13 +2,13 @@ - + + Userscripts App
- diff --git a/src/app/App.svelte b/src/app/App.svelte index 0dfaca6a..ef70969b 100644 --- a/src/app/App.svelte +++ b/src/app/App.svelte @@ -1,20 +1,43 @@
- Userscripts App Icon + Userscripts App Icon

You can turn on the Userscripts iOS Safari extension in Settings. Read the docs.

- +
CURRENT DIRECTORY:
-
init
+
{directory}
diff --git a/src/app/scripts.js b/src/app/scripts.js deleted file mode 100644 index 1b559fa4..00000000 --- a/src/app/scripts.js +++ /dev/null @@ -1,15 +0,0 @@ -/* eslint-disable no-undef */ -/* eslint-disable no-unused-vars */ -const directory = document.querySelector("#directory"); -const button = document.querySelector("#set_directory"); -const version = document.querySelector("#version"); -const build = document.querySelector("#build"); -const setDirectory = () => webkit.messageHandlers.controller.postMessage("SET_READ_LOCATION"); -function printDirectory(location) { - directory.innerText = location; -} -function printVersion(v, b) { - version.innerText = v; - build.innerText = b; -} -button.addEventListener("click", setDirectory); diff --git a/xcode/App-Shared/Base.lproj/Main.html b/xcode/App-Shared/Base.lproj/Main.html deleted file mode 100644 index bfd9ae28..00000000 --- a/xcode/App-Shared/Base.lproj/Main.html +++ /dev/null @@ -1 +0,0 @@ -
Userscripts App Icon

You can turn on the Userscripts iOS Safari extension in Settings. Read the docs.

CURRENT DIRECTORY:
init
diff --git a/xcode/App-Shared/Resources/Icon.png b/xcode/App-Shared/Resources/Icon.png deleted file mode 100644 index 9fe96182de0e89765ffbef258c50c5a7d2d8b767..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24309 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 Void) { @@ -57,7 +66,7 @@ class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHan logger?.error("\(#function, privacy: .public) - Userscripts iOS received a message without a name") return } - if name == "SET_READ_LOCATION" { + if name == "CHANGE_DIRECTORY" { // https://developer.apple.com/documentation/uikit/view_controllers/providing_access_to_directories logger?.info("\(#function, privacy: .public) - Userscripts iOS has requested to set the readLocation") let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.folder]) @@ -69,6 +78,6 @@ class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHan func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) { Preferences.scriptsDirectoryUrl = url - webView.evaluateJavaScript("printDirectory('\(Preferences.scriptsDirectoryUrl.absoluteString)')") + webView.evaluateJavaScript("APP.printDirectory('\(getCurrentScriptsDirectoryString())')") } } diff --git a/xcode/Userscripts.xcodeproj/project.pbxproj b/xcode/Userscripts.xcodeproj/project.pbxproj index f12ffc6c..2de34dd8 100644 --- a/xcode/Userscripts.xcodeproj/project.pbxproj +++ b/xcode/Userscripts.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ 030C64FC2AD0B24C00548FBD /* Initialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030C64FB2AD0B24C00548FBD /* Initialization.swift */; }; 0336619F294DF7C900CFE179 /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0336619E294DF7C900CFE179 /* Functions.swift */; }; 033661A529510B7900CFE179 /* View.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033661A329510B7900CFE179 /* View.storyboard */; }; + 039F59532AD9591C002E9977 /* dist in Resources */ = {isa = PBXBuildFile; fileRef = 039F59522AD9591C002E9977 /* dist */; }; + 039F59562ADA2FE2002E9977 /* USchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039F59552ADA2FE2002E9977 /* USchemeHandler.swift */; }; 03C24F972ABD2CBB00F130F9 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C24F962ABD2CBB00F130F9 /* Preferences.swift */; }; 03C24F982ABD2CBB00F130F9 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C24F962ABD2CBB00F130F9 /* Preferences.swift */; }; 03C24F992ABD2CBB00F130F9 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C24F962ABD2CBB00F130F9 /* Preferences.swift */; }; @@ -28,8 +30,6 @@ 4A36A61A268266B30018536B /* images in Resources */ = {isa = PBXBuildFile; fileRef = 4A36A616268266B30018536B /* images */; }; 4A36A61B268266B30018536B /* _locales in Resources */ = {isa = PBXBuildFile; fileRef = 4A36A617268266B30018536B /* _locales */; }; 4A4CF6DE270A38BD00111584 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A4CF6DD270A38BD00111584 /* AppDelegate.swift */; }; - 4A4CF6E2270A38BD00111584 /* Main.html in Resources */ = {isa = PBXBuildFile; fileRef = 4A4CF6E0270A38BD00111584 /* Main.html */; }; - 4A4CF6E4270A38BD00111584 /* Icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 4A4CF6E3270A38BD00111584 /* Icon.png */; }; 4A4CF6E8270A38BD00111584 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A4CF6E7270A38BD00111584 /* SceneDelegate.swift */; }; 4A4CF6EA270A38BD00111584 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A4CF6E9270A38BD00111584 /* ViewController.swift */; }; 4A4CF6ED270A38BD00111584 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4A4CF6EB270A38BD00111584 /* LaunchScreen.storyboard */; }; @@ -106,6 +106,8 @@ 030C64FB2AD0B24C00548FBD /* Initialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Initialization.swift; sourceTree = ""; }; 0336619E294DF7C900CFE179 /* Functions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = ""; }; 033661A429510B7900CFE179 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/View.storyboard; sourceTree = ""; }; + 039F59522AD9591C002E9977 /* dist */ = {isa = PBXFileReference; lastKnownFileType = folder; path = dist; sourceTree = ""; }; + 039F59552ADA2FE2002E9977 /* USchemeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = USchemeHandler.swift; sourceTree = ""; }; 03C24F962ABD2CBB00F130F9 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; 03E20D05291E170000B4F692 /* dist */ = {isa = PBXFileReference; lastKnownFileType = folder; path = dist; sourceTree = ""; }; 4A143AA9279DE6FF0029BFD0 /* Mac-Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Mac-Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -114,8 +116,6 @@ 4A36A617268266B30018536B /* _locales */ = {isa = PBXFileReference; lastKnownFileType = folder; path = _locales; sourceTree = ""; }; 4A4CF6DB270A38BD00111584 /* Userscripts-iOS-App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Userscripts-iOS-App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 4A4CF6DD270A38BD00111584 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 4A4CF6E1270A38BD00111584 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = Base; path = ../Base.lproj/Main.html; sourceTree = ""; }; - 4A4CF6E3270A38BD00111584 /* Icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Icon.png; sourceTree = ""; }; 4A4CF6E7270A38BD00111584 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 4A4CF6E9270A38BD00111584 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 4A4CF6EC270A38BD00111584 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -192,6 +192,7 @@ children = ( 039F59512AD95485002E9977 /* Resources */, 4A4CF6E9270A38BD00111584 /* ViewController.swift */, + 039F59552ADA2FE2002E9977 /* USchemeHandler.swift */, ); path = "App-Shared"; sourceTree = ""; @@ -199,8 +200,7 @@ 039F59512AD95485002E9977 /* Resources */ = { isa = PBXGroup; children = ( - 4A4CF6E3270A38BD00111584 /* Icon.png */, - 4A4CF6E0270A38BD00111584 /* Main.html */, + 039F59522AD9591C002E9977 /* dist */, ); path = Resources; sourceTree = ""; @@ -478,12 +478,11 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4A4CF6E4270A38BD00111584 /* Icon.png in Resources */, 4A4CF6F0270A38BD00111584 /* Main.storyboard in Resources */, 4A4CF6ED270A38BD00111584 /* LaunchScreen.storyboard in Resources */, - 4A4CF6E2270A38BD00111584 /* Main.html in Resources */, 030C64F62AC62CC900548FBD /* Settings.bundle in Resources */, 4A4CF710270A38BF00111584 /* Assets.xcassets in Resources */, + 039F59532AD9591C002E9977 /* dist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -568,6 +567,7 @@ 030C64FC2AD0B24C00548FBD /* Initialization.swift in Sources */, 78B25585299D4C27000B2E9B /* Utilities.swift in Sources */, 4A4CF6E8270A38BD00111584 /* SceneDelegate.swift in Sources */, + 039F59562ADA2FE2002E9977 /* USchemeHandler.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -634,14 +634,6 @@ name = View.storyboard; sourceTree = ""; }; - 4A4CF6E0270A38BD00111584 /* Main.html */ = { - isa = PBXVariantGroup; - children = ( - 4A4CF6E1270A38BD00111584 /* Base */, - ); - name = Main.html; - sourceTree = ""; - }; 4A4CF6EB270A38BD00111584 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( From 61f82b9119548cc1321634fa5b4d6edf0342527a Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Wed, 18 Oct 2023 07:20:50 +0800 Subject: [PATCH 032/315] refactor: update dev and build of ios app webview --- package.json | 8 +++-- scripts/build-app.js | 36 ++++++++++++++++++++++ build-ext-v4.js => scripts/build-ext-v4.js | 0 build-ext-v5.js => scripts/build-ext-v5.js | 0 scripts/preview-app.js | 34 ++++++++++++++++++++ 5 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 scripts/build-app.js rename build-ext-v4.js => scripts/build-ext-v4.js (100%) rename build-ext-v5.js => scripts/build-ext-v5.js (100%) create mode 100644 scripts/preview-app.js diff --git a/package.json b/package.json index bd837525..f8c5fd66 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,11 @@ "type": "module", "scripts": { "dev": "vite", - "build": "vite build", - "lint-js": "eslint ./", - "lint-css": "stylelint '**/*.{css,svelte}'" + "build": "pnpm build:app && pnpm build:ext", + "build:app": "node scripts/build-app.js", + "build:ext": "node scripts/build-ext-v4.js", + "lint:js": "eslint ./", + "lint:css": "stylelint '**/*.{css,svelte}'" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^2.4.3", diff --git a/scripts/build-app.js b/scripts/build-app.js new file mode 100644 index 00000000..6fdbcca3 --- /dev/null +++ b/scripts/build-app.js @@ -0,0 +1,36 @@ +/** + * @file Build App WebView resources using the Vite JavaScript API + * @see {@link https://vitejs.dev/guide/api-javascript.html|JavaScript API} + * + * All build processes start at the same time due to asynchronous calls + * The assets name is irrelevant, just need to determine the entry path + */ + +import {build} from "vite"; +import {svelte} from "@sveltejs/vite-plugin-svelte"; + +/** + * Define default vite config options + * Disable auto resolving {@link vite.config.js} + * @see {@link https://vitejs.dev/config/|Config} + * @see {@link https://vitejs.dev/guide/api-javascript.html#inlineconfig|configFile} + */ +const defineConfig = { + base: "./", + configFile: false +}; + +/** + * Build App-Shared WebView resources to xcode dist + */ +build({ + ...defineConfig, + plugins: [svelte()], + build: { + outDir: "xcode/App-Shared/Resources/dist/", + copyPublicDir: false, + rollupOptions: { + input: "entry-app-webview.html" + } + } +}); diff --git a/build-ext-v4.js b/scripts/build-ext-v4.js similarity index 100% rename from build-ext-v4.js rename to scripts/build-ext-v4.js diff --git a/build-ext-v5.js b/scripts/build-ext-v5.js similarity index 100% rename from build-ext-v5.js rename to scripts/build-ext-v5.js diff --git a/scripts/preview-app.js b/scripts/preview-app.js new file mode 100644 index 00000000..ba3bf231 --- /dev/null +++ b/scripts/preview-app.js @@ -0,0 +1,34 @@ +/** + * @file Preview App WebView resources using the Vite JavaScript API + * @see {@link https://vitejs.dev/guide/api-javascript.html|JavaScript API} + */ + +import {preview} from "vite"; + +/** + * Define default vite config options + * Disable auto resolving {@link vite.config.js} + * @see {@link https://vitejs.dev/config/|Config} + * @see {@link https://vitejs.dev/guide/api-javascript.html#inlineconfig|configFile} + */ +const defineConfig = { + base: "./", + configFile: false +}; + +/** + * Preview App-Shared WebView resources from xcode dist + */ +(async () => { + const previewServer = await preview({ + ...defineConfig, + preview: { + // port: 4173, + open: "entry-app-webview.html" + }, + build: { + outDir: "xcode/App-Shared/Resources/dist/" + } + }); + previewServer.printUrls(); +})(); From 5f99654a4f6f6d95d4a50ee39a8f991c8f4e70d1 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Wed, 18 Oct 2023 16:06:06 +0800 Subject: [PATCH 033/315] refactor(dev): improve produce of ext manifest.json can easily use different sources of `manifest.json` files extension version number are automatically introduced distinguish extension name or icons in different config all done automatically during the Xcode build process --- .../Resources/_locales/en/messages.json | 2 +- .../{manifest.json => ext-v4-manifest.json} | 3 +- .../Ext-Safari/Resources/ext-v5-manifest.json | 45 +++++++++++++ xcode/Userscripts.xcodeproj/project.pbxproj | 64 ++++++++++++++++--- xcode/xcconfig/Mac-App.xcconfig | 5 +- xcode/xcconfig/Mac-Safari-Extension.xcconfig | 8 ++- xcode/xcconfig/iOS-App.xcconfig | 5 +- xcode/xcconfig/iOS-Safari-Extension.xcconfig | 8 ++- 8 files changed, 119 insertions(+), 21 deletions(-) rename xcode/Ext-Safari/Resources/{manifest.json => ext-v4-manifest.json} (97%) create mode 100644 xcode/Ext-Safari/Resources/ext-v5-manifest.json diff --git a/xcode/Ext-Safari/Resources/_locales/en/messages.json b/xcode/Ext-Safari/Resources/_locales/en/messages.json index a7cee631..8db4438d 100644 --- a/xcode/Ext-Safari/Resources/_locales/en/messages.json +++ b/xcode/Ext-Safari/Resources/_locales/en/messages.json @@ -1,6 +1,6 @@ { "extension_name": { - "message": "Userscripts", + "message": "_NAME_", "description": "The display name for the extension." }, "extension_description": { diff --git a/xcode/Ext-Safari/Resources/manifest.json b/xcode/Ext-Safari/Resources/ext-v4-manifest.json similarity index 97% rename from xcode/Ext-Safari/Resources/manifest.json rename to xcode/Ext-Safari/Resources/ext-v4-manifest.json index 21e146fc..dde68d2a 100644 --- a/xcode/Ext-Safari/Resources/manifest.json +++ b/xcode/Ext-Safari/Resources/ext-v4-manifest.json @@ -1,10 +1,9 @@ - { "manifest_version": 2, "default_locale": "en", "name": "__MSG_extension_name__", "description": "__MSG_extension_description__", - "version": "4.5.0", + "version": "_VERSION_", "icons": { "48": "images/icon-48.png", "96": "images/icon-96.png", diff --git a/xcode/Ext-Safari/Resources/ext-v5-manifest.json b/xcode/Ext-Safari/Resources/ext-v5-manifest.json new file mode 100644 index 00000000..ead6d695 --- /dev/null +++ b/xcode/Ext-Safari/Resources/ext-v5-manifest.json @@ -0,0 +1,45 @@ +{ + "manifest_version": 2, + "default_locale": "en", + "name": "__MSG_extension_name__", + "description": "__MSG_extension_description__", + "version": "_VERSION_", + "icons": { + "48": "images/icon-48.png", + "96": "images/icon-96.png", + "128": "images/icon-128.png", + "256": "images/icon-256.png", + "512": "images/icon-512.png" + }, + "background": { + "scripts": ["dist/background.js"], + "persistent": false + }, + "browser_action": { + "default_popup": "dist/s/entry-ext-action-popup.html", + "default_icon": { + "16": "images/toolbar-icon-16.png", + "32": "images/toolbar-icon-32.png" + } + }, + "content_scripts": [ + { + "js": ["dist/content.js"], + "matches": [""], + "run_at": "document_start", + "all_frames": true + } + ], + "permissions": [ + "", + "clipboardWrite", + "contextMenus", + "declarativeNetRequestWithHostAccess", + "menus", + "nativeMessaging", + "storage", + "tabs", + "unlimitedStorage", + "webNavigation" + ] +} diff --git a/xcode/Userscripts.xcodeproj/project.pbxproj b/xcode/Userscripts.xcodeproj/project.pbxproj index 2de34dd8..7f38a413 100644 --- a/xcode/Userscripts.xcodeproj/project.pbxproj +++ b/xcode/Userscripts.xcodeproj/project.pbxproj @@ -25,7 +25,6 @@ 4A301B2A270A474400C7E9E1 /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A57BA07227235CE008A9763 /* SafariWebExtensionHandler.swift */; }; 4A301B2B270A474F00C7E9E1 /* _locales in Resources */ = {isa = PBXBuildFile; fileRef = 4A36A617268266B30018536B /* _locales */; }; 4A301B2C270A475200C7E9E1 /* images in Resources */ = {isa = PBXBuildFile; fileRef = 4A36A616268266B30018536B /* images */; }; - 4A301B2F270A476000C7E9E1 /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = 4AD66D2026826D8100B004E8 /* manifest.json */; }; 4A301B32270A49F200C7E9E1 /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AED6491268CDB58001794BF /* Functions.swift */; }; 4A36A61A268266B30018536B /* images in Resources */ = {isa = PBXBuildFile; fileRef = 4A36A616268266B30018536B /* images */; }; 4A36A61B268266B30018536B /* _locales in Resources */ = {isa = PBXBuildFile; fileRef = 4A36A617268266B30018536B /* _locales */; }; @@ -42,7 +41,6 @@ 4A57B9F9227235CE008A9763 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4A57B9F8227235CE008A9763 /* Assets.xcassets */; }; 4A57BA00227235CE008A9763 /* Userscripts-Mac-Safari-Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 4A57B9FF227235CE008A9763 /* Userscripts-Mac-Safari-Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4A57BA08227235CE008A9763 /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A57BA07227235CE008A9763 /* SafariWebExtensionHandler.swift */; }; - 4AD66D2326826D8100B004E8 /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = 4AD66D2026826D8100B004E8 /* manifest.json */; }; 4AED6492268CDB58001794BF /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AED6491268CDB58001794BF /* Functions.swift */; }; 7878ED24299BA27B00E36A24 /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A57BA07227235CE008A9763 /* SafariWebExtensionHandler.swift */; }; 78B25583299D4C27000B2E9B /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B25582299D4C27000B2E9B /* Utilities.swift */; }; @@ -108,6 +106,7 @@ 033661A429510B7900CFE179 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/View.storyboard; sourceTree = ""; }; 039F59522AD9591C002E9977 /* dist */ = {isa = PBXFileReference; lastKnownFileType = folder; path = dist; sourceTree = ""; }; 039F59552ADA2FE2002E9977 /* USchemeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = USchemeHandler.swift; sourceTree = ""; }; + 039F595A2ADFB312002E9977 /* ext-v5-manifest.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "ext-v5-manifest.json"; sourceTree = ""; }; 03C24F962ABD2CBB00F130F9 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; 03E20D05291E170000B4F692 /* dist */ = {isa = PBXFileReference; lastKnownFileType = folder; path = dist; sourceTree = ""; }; 4A143AA9279DE6FF0029BFD0 /* Mac-Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Mac-Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -135,7 +134,7 @@ 4A57BA07227235CE008A9763 /* SafariWebExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariWebExtensionHandler.swift; sourceTree = ""; }; 4A57BA0E227235CE008A9763 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4A57BA1D227235E6008A9763 /* Mac.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Mac.entitlements; sourceTree = ""; }; - 4AD66D2026826D8100B004E8 /* manifest.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = manifest.json; sourceTree = ""; }; + 4AD66D2026826D8100B004E8 /* ext-v4-manifest.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "ext-v4-manifest.json"; sourceTree = ""; }; 4AED6491268CDB58001794BF /* Functions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = ""; }; 78391D4629A4CB3C00979E34 /* Userscripts-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Userscripts-Debug.xcconfig"; sourceTree = ""; }; 78391D4729A4CB3C00979E34 /* Userscripts-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Userscripts-Release.xcconfig"; sourceTree = ""; }; @@ -289,7 +288,8 @@ 4A36A617268266B30018536B /* _locales */, 4A36A616268266B30018536B /* images */, 03E20D05291E170000B4F692 /* dist */, - 4AD66D2026826D8100B004E8 /* manifest.json */, + 4AD66D2026826D8100B004E8 /* ext-v4-manifest.json */, + 039F595A2ADFB312002E9977 /* ext-v5-manifest.json */, ); path = Resources; sourceTree = ""; @@ -346,7 +346,7 @@ 4A4CF6D8270A38BD00111584 /* Frameworks */, 4A4CF6D9270A38BD00111584 /* Resources */, 4A4CF713270A38BF00111584 /* Embed Foundation Extensions */, - 030C64F72AC713A600548FBD /* Update Settings.bundle */, + 030C64F72AC713A600548FBD /* UScript: Introduce values to app Settings.bundle */, ); buildRules = ( ); @@ -365,6 +365,7 @@ 4A4CF6F3270A38BF00111584 /* Sources */, 4A4CF6F4270A38BF00111584 /* Frameworks */, 4A4CF6F5270A38BF00111584 /* Resources */, + 039F59592ADFA415002E9977 /* UScript: Produce ext manifest.json */, ); buildRules = ( ); @@ -401,6 +402,7 @@ 4A57B9FB227235CE008A9763 /* Sources */, 4A57B9FC227235CE008A9763 /* Frameworks */, 4A57B9FD227235CE008A9763 /* Resources */, + 039F59572ADF9558002E9977 /* UScript: Produce ext manifest.json */, ); buildRules = ( ); @@ -492,7 +494,6 @@ files = ( 4A301B2C270A475200C7E9E1 /* images in Resources */, 03E20D07291E170000B4F692 /* dist in Resources */, - 4A301B2F270A476000C7E9E1 /* manifest.json in Resources */, 4A301B2B270A474F00C7E9E1 /* _locales in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -511,7 +512,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4AD66D2326826D8100B004E8 /* manifest.json in Resources */, 03E20D06291E170000B4F692 /* dist in Resources */, 4A36A61A268266B30018536B /* images in Resources */, 4A36A61B268266B30018536B /* _locales in Resources */, @@ -521,7 +521,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 030C64F72AC713A600548FBD /* Update Settings.bundle */ = { + 030C64F72AC713A600548FBD /* UScript: Introduce values to app Settings.bundle */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -529,10 +529,10 @@ inputFileListPaths = ( ); inputPaths = ( - "$(SRCROOT)/$(TARGET_NAME)/Settings.bundle/Root.plist", + "$(SRCROOT)/$(SRC_DIR)/Settings.bundle/Root.plist", "$(TARGET_BUILD_DIR)/$(INFOPLIST_PATH)", ); - name = "Update Settings.bundle"; + name = "UScript: Introduce values to app Settings.bundle"; outputFileListPaths = ( ); outputPaths = ( @@ -542,6 +542,50 @@ shellPath = /bin/sh; shellScript = "# Dynamically introduce US_SHARED_GID in xcconfig for Settings.bundle\nUS_SHARED_GID=$(/usr/libexec/PlistBuddy -c \"Print US_SHARED_GID\" \"$TARGET_BUILD_DIR/$INFOPLIST_PATH\")\nSETTINGS_BUNDLE=$TARGET_BUILD_DIR/$EXECUTABLE_FOLDER_PATH/Settings.bundle/Root.plist\n/usr/libexec/PlistBuddy -c \"Set ApplicationGroupContainerIdentifier $US_SHARED_GID\" \"$SETTINGS_BUNDLE\"\n"; }; + 039F59572ADF9558002E9977 /* UScript: Produce ext manifest.json */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/$(MANIFEST_FILE)", + "$(SRCROOT)/$(SRC_DIR)/Resources/_locales/en/messages.json", + "$(TARGET_BUILD_DIR)/$(INFOPLIST_PATH)", + ); + name = "UScript: Produce ext manifest.json"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/manifest.json", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "sed \"s/_VERSION_/$MAC_APP_VERSION/\" $SRCROOT/$MANIFEST_FILE > $TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/manifest.json\nsed -i \"\" \"s/_NAME_/$MANIFEST_NAME/\" $TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/_locales/en/messages.json\n"; + }; + 039F59592ADFA415002E9977 /* UScript: Produce ext manifest.json */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/$(MANIFEST_FILE)", + "$(SRCROOT)/$(SRC_DIR)/Resources/_locales/en/messages.json", + "$(TARGET_BUILD_DIR)/$(INFOPLIST_PATH)", + ); + name = "UScript: Produce ext manifest.json"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/manifest.json", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "sed \"s/_VERSION_/$IOS_APP_VERSION/\" $SRCROOT/$MANIFEST_FILE > $TARGET_BUILD_DIR/$CONTENTS_FOLDER_PATH/manifest.json\nsed -i \"\" \"s/_NAME_/$MANIFEST_NAME/\" $TARGET_BUILD_DIR/$CONTENTS_FOLDER_PATH/_locales/en/messages.json\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/xcode/xcconfig/Mac-App.xcconfig b/xcode/xcconfig/Mac-App.xcconfig index 37b672da..d477275f 100644 --- a/xcode/xcconfig/Mac-App.xcconfig +++ b/xcode/xcconfig/Mac-App.xcconfig @@ -1,11 +1,12 @@ +SRC_DIR = App-Mac ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon -CODE_SIGN_ENTITLEMENTS = App-Mac/App.entitlements +CODE_SIGN_ENTITLEMENTS = $(SRC_DIR)/App.entitlements COMBINE_HIDPI_IMAGES = YES CURRENT_PROJECT_VERSION = $(MAC_APP_BUILD) ENABLE_HARDENED_RUNTIME = YES GENERATE_INFOPLIST_FILE = YES -INFOPLIST_FILE = App-Mac/Info.plist +INFOPLIST_FILE = $(SRC_DIR)/Info.plist INFOPLIST_KEY_CFBundleDisplayName = $(PROJECT_NAME)-$(TARGET_NAME) INFOPLIST_KEY_CFBundleDisplayName[config=Debug] = $(PROJECT_NAME)-$(TARGET_NAME)-Debug LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks diff --git a/xcode/xcconfig/Mac-Safari-Extension.xcconfig b/xcode/xcconfig/Mac-Safari-Extension.xcconfig index a0d123ef..6e392d8a 100644 --- a/xcode/xcconfig/Mac-Safari-Extension.xcconfig +++ b/xcode/xcconfig/Mac-Safari-Extension.xcconfig @@ -1,8 +1,9 @@ -CODE_SIGN_ENTITLEMENTS = Ext-Safari/Mac.entitlements +SRC_DIR = Ext-Safari +CODE_SIGN_ENTITLEMENTS = $(SRC_DIR)/Mac.entitlements CURRENT_PROJECT_VERSION = $(MAC_APP_BUILD) ENABLE_HARDENED_RUNTIME = YES GENERATE_INFOPLIST_FILE = YES -INFOPLIST_FILE = Ext-Safari/Info.plist +INFOPLIST_FILE = $(SRC_DIR)/Info.plist INFOPLIST_KEY_CFBundleDisplayName = $(PRODUCT_NAME) INFOPLIST_KEY_CFBundleDisplayName[config=Debug] = $(PRODUCT_NAME)-Debug LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks @@ -10,6 +11,9 @@ MARKETING_VERSION = $(MAC_APP_VERSION) PRODUCT_BUNDLE_IDENTIFIER = $(EXT_IDENTIFIER) PRODUCT_NAME = $(PROJECT_NAME)-$(TARGET_NAME) SKIP_INSTALL = YES +MANIFEST_FILE = $(SRC_DIR)/Resources/ext-v4-manifest.json +MANIFEST_NAME = $(PROJECT_NAME) +MANIFEST_NAME[config=Debug] = $(PROJECT_NAME)-Debug // Override this file #include? "Mac-Safari-Extension.dev.xcconfig" diff --git a/xcode/xcconfig/iOS-App.xcconfig b/xcode/xcconfig/iOS-App.xcconfig index 9b41bde9..005ffcaf 100644 --- a/xcode/xcconfig/iOS-App.xcconfig +++ b/xcode/xcconfig/iOS-App.xcconfig @@ -1,10 +1,11 @@ +SRC_DIR = App-iOS ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor -CODE_SIGN_ENTITLEMENTS = App-iOS/App.entitlements +CODE_SIGN_ENTITLEMENTS = $(SRC_DIR)/App.entitlements CURRENT_PROJECT_VERSION = $(IOS_APP_BUILD) GENERATE_INFOPLIST_FILE = YES -INFOPLIST_FILE = App-iOS/Info.plist +INFOPLIST_FILE = $(SRC_DIR)/Info.plist INFOPLIST_KEY_CFBundleDisplayName = $(PROJECT_NAME) INFOPLIST_KEY_CFBundleDisplayName[config=Debug] = $(PROJECT_NAME)-D INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES diff --git a/xcode/xcconfig/iOS-Safari-Extension.xcconfig b/xcode/xcconfig/iOS-Safari-Extension.xcconfig index 7b7e69ac..7df9eae4 100644 --- a/xcode/xcconfig/iOS-Safari-Extension.xcconfig +++ b/xcode/xcconfig/iOS-Safari-Extension.xcconfig @@ -1,7 +1,8 @@ -CODE_SIGN_ENTITLEMENTS = Ext-Safari/iOS.entitlements +SRC_DIR = Ext-Safari +CODE_SIGN_ENTITLEMENTS = $(SRC_DIR)/iOS.entitlements CURRENT_PROJECT_VERSION = $(IOS_APP_BUILD) GENERATE_INFOPLIST_FILE = YES -INFOPLIST_FILE = Ext-Safari/Info.plist +INFOPLIST_FILE = $(SRC_DIR)/Info.plist INFOPLIST_KEY_CFBundleDisplayName = $(PRODUCT_NAME) INFOPLIST_KEY_CFBundleDisplayName[config=Debug] = $(PROJECT_NAME)-D LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @executable_path/../../Frameworks @@ -14,6 +15,9 @@ SKIP_INSTALL = YES SWIFT_EMIT_LOC_STRINGS = YES TARGETED_DEVICE_FAMILY = 1,2 VALIDATE_PRODUCT[config=Release] = YES +MANIFEST_FILE = $(SRC_DIR)/Resources/ext-v4-manifest.json +MANIFEST_NAME = $(PROJECT_NAME) +MANIFEST_NAME[config=Debug] = $(PROJECT_NAME)-Debug // Override this file #include? "iOS-Safari-Extension.dev.xcconfig" From 44a58ea7b98126bfc7f2ef3662812ac33d9d8c9e Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Thu, 19 Oct 2023 11:15:55 +0800 Subject: [PATCH 034/315] refactor: update webview action policy and fail handler --- xcode/App-Shared/USchemeHandler.swift | 19 +++++++++++----- xcode/App-Shared/ViewController.swift | 31 +++++++++++++++++++++------ 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/xcode/App-Shared/USchemeHandler.swift b/xcode/App-Shared/USchemeHandler.swift index 1803a08d..99c8acaf 100644 --- a/xcode/App-Shared/USchemeHandler.swift +++ b/xcode/App-Shared/USchemeHandler.swift @@ -10,17 +10,26 @@ let AppWebViewEntryPage = "entry-app-webview.html" class USchemeHandler: NSObject, WKURLSchemeHandler { func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { + // wrapper of didFailWithError + func failHandler(_ errmsg: String) { + logger?.error("\(#function) - \(errmsg, privacy: .public)") + // redirect to a customized error page +// DispatchQueue.main.async { +// webView.load(URLRequest(url: URL(string: "\(AppWebViewUrlScheme):///")!)) +// } + urlSchemeTask.didFailWithError(NSError(domain: "USchemeHandler", code: 0, userInfo: nil)) + } + // https://developer.apple.com/documentation/dispatch/dispatchqueue DispatchQueue.global(qos: .userInteractive).async { guard let url = urlSchemeTask.request.url else { - logger?.error("\(#function, privacy: .public) - failed to get request url") + failHandler("failed to get request url") return } guard url.scheme == AppWebViewUrlScheme else { - logger?.error("\(#function, privacy: .public) - unexpected url scheme: \(url, privacy: .public)") + failHandler("unexpected url scheme: \(url)") return } - print(url) var name: String if #available(macOS 13.0, iOS 16.0, *) { name = url.path(percentEncoded: false) @@ -31,11 +40,11 @@ class USchemeHandler: NSObject, WKURLSchemeHandler { name = AppWebViewEntryPage } guard let file = Bundle.main.url(forResource: name, withExtension: nil, subdirectory: "dist") else { - logger?.error("\(#function, privacy: .public) - file not found: \(url, privacy: .public)") + failHandler("file not found: \(url)") return } guard let data = try? Data(contentsOf: file) else { - logger?.error("\(#function, privacy: .public) - faild to get data from: \(url, privacy: .public)") + failHandler("faild to get data from: \(url)") return } diff --git a/xcode/App-Shared/ViewController.swift b/xcode/App-Shared/ViewController.swift index 0c972fe8..fc811109 100644 --- a/xcode/App-Shared/ViewController.swift +++ b/xcode/App-Shared/ViewController.swift @@ -48,17 +48,34 @@ class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHan func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { if navigationAction.navigationType == .linkActivated { - guard - let url = navigationAction.request.url, - UIApplication.shared.canOpenURL(url) - else { + guard let url = navigationAction.request.url else { + decisionHandler(.allow) + return + } + // allow registration scheme + if url.scheme == AppWebViewUrlScheme { + decisionHandler(.allow) + return + } + // allow specified url prefixes +// let allowPrefixes = [ +// "https://github.com/quoid/userscripts" +// ] +// for prefix in allowPrefixes { +// if url.absoluteString.lowercased().hasPrefix(prefix) { +// decisionHandler(.allow) +// return +// } +// } + // open from external app like safari + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + decisionHandler(.cancel) return } - UIApplication.shared.open(url) - decisionHandler(.cancel) - } else { decisionHandler(.allow) } + decisionHandler(.allow) } func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { From 243f27ac2282f8e144ba191f968b2fc612913f9b Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Thu, 19 Oct 2023 17:26:19 +0800 Subject: [PATCH 035/315] feat(app): click on the directory to open the folder --- src/app/App.svelte | 19 +++++++++++++++---- src/app/app.css | 14 ++++++++++++++ xcode/App-Shared/ViewController.swift | 9 +++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/app/App.svelte b/src/app/App.svelte index ef70969b..870c318f 100644 --- a/src/app/App.svelte +++ b/src/app/App.svelte @@ -19,6 +19,10 @@ function changeDirectory() { window.webkit?.messageHandlers.controller.postMessage("CHANGE_DIRECTORY"); } + + function openDirectory() { + window.webkit?.messageHandlers.controller.postMessage("OPEN_DIRECTORY"); + }
@@ -33,11 +37,13 @@

You can turn on the Userscripts iOS Safari extension in Settings. Read the docs.

-
CURRENT DIRECTORY:
-
{directory}
+
+ +
diff --git a/src/app/app.css b/src/app/app.css index fba748df..2aa6eab7 100644 --- a/src/app/app.css +++ b/src/app/app.css @@ -34,4 +34,18 @@ body { #app { height: 100%; +} + +button { + border: none; + cursor: pointer; + padding: 0; + user-select: none; +} + +.link { + background: none; + color: var(--color-blue); + cursor: pointer; + text-decoration: underline; } \ No newline at end of file diff --git a/xcode/App-Shared/ViewController.swift b/xcode/App-Shared/ViewController.swift index fc811109..df9befb9 100644 --- a/xcode/App-Shared/ViewController.swift +++ b/xcode/App-Shared/ViewController.swift @@ -91,6 +91,15 @@ class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHan documentPicker.directoryURL = getDocumentsDirectory() present(documentPicker, animated: true, completion: nil) } + if name == "OPEN_DIRECTORY" { + guard var components = URLComponents(url: Preferences.scriptsDirectoryUrl, resolvingAgainstBaseURL: true) else { + return + } + components.scheme = "shareddocuments" + if let url = components.url, UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } + } } func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) { From 69fb292525c941a660f9e100618914892e3dcd78 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sat, 21 Oct 2023 19:16:03 +0800 Subject: [PATCH 036/315] refactor: improve package and types import --- src/ext/action-popup/main.js | 2 +- src/ext/extension-page/Components/Editor/CodeMirror.svelte | 1 + src/ext/extension-page/{cm.css => codemirror.css} | 0 src/ext/extension-page/main.js | 3 +-- xcode/App-Mac/Functions.swift | 1 - xcode/App-Mac/ViewController.swift | 2 -- xcode/Ext-Safari/Functions.swift | 2 -- xcode/Ext-Safari/SafariWebExtensionHandler.swift | 1 - xcode/Shared/Preferences.swift | 1 - xcode/Shared/Utilities.swift | 5 ++--- xcode/Userscripts.xcodeproj/project.pbxproj | 2 +- 11 files changed, 6 insertions(+), 14 deletions(-) rename src/ext/extension-page/{cm.css => codemirror.css} (100%) diff --git a/src/ext/action-popup/main.js b/src/ext/action-popup/main.js index 03752163..e151d8de 100644 --- a/src/ext/action-popup/main.js +++ b/src/ext/action-popup/main.js @@ -1,6 +1,6 @@ -import "./app.css"; import "../shared/reset.css"; import "../shared/variables.css"; +import "./app.css"; import App from "./App.svelte"; if (import.meta.env.DEV) { // vite feat that only import in dev mode diff --git a/src/ext/extension-page/Components/Editor/CodeMirror.svelte b/src/ext/extension-page/Components/Editor/CodeMirror.svelte index 9fce5705..849662ef 100644 --- a/src/ext/extension-page/Components/Editor/CodeMirror.svelte +++ b/src/ext/extension-page/Components/Editor/CodeMirror.svelte @@ -27,6 +27,7 @@ - + + + + + + Userscripts App + + +
+ + diff --git a/entry-ext-action-popup.html b/entry-ext-action-popup.html index e135d374..c329fa52 100644 --- a/entry-ext-action-popup.html +++ b/entry-ext-action-popup.html @@ -1,14 +1,14 @@ - + - - - - - Userscripts popup - - - -
- - + + + + + Userscripts popup + + + +
+ + diff --git a/entry-ext-extension-page.html b/entry-ext-extension-page.html index fbbce42d..16e4944d 100644 --- a/entry-ext-extension-page.html +++ b/entry-ext-extension-page.html @@ -1,15 +1,15 @@ - + - - - - - Userscripts page - - - -
- - - + + + + + Userscripts page + + + +
+ + + diff --git a/index.html b/index.html index a0b12e43..215948a7 100644 --- a/index.html +++ b/index.html @@ -1,26 +1,28 @@ - + - - - - Userscripts dev - - - -
- - - + + + + Userscripts dev + + + +
+ + + diff --git a/jsconfig.json b/jsconfig.json index a8284bdf..84f7a207 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,33 +1,33 @@ { - "compilerOptions": { - "moduleResolution": "Node", - "target": "ESNext", - "module": "ESNext", - /** - * svelte-preprocess cannot figure out whether you have - * a value or a type, so tell TypeScript to enforce using - * `import type` instead of `import` for Types. - */ - "importsNotUsedAsValues": "error", - "isolatedModules": true, - "resolveJsonModule": true, - /** - * To have warnings / errors of the Svelte compiler at the - * correct position, enable source maps by default. - */ - "sourceMap": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - /** - * Typecheck JS in `.svelte` and `.js` files by default. - * Disable this if you'd like to use dynamic types. - */ - // "checkJs": true - }, - /** - * Use global.d.ts instead of compilerOptions.types - * to avoid limiting type declarations. - */ - "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] + "compilerOptions": { + "moduleResolution": "Node", + "target": "ESNext", + "module": "ESNext", + /** + * svelte-preprocess cannot figure out whether you have + * a value or a type, so tell TypeScript to enforce using + * `import type` instead of `import` for Types. + */ + "verbatimModuleSyntax": true, + "isolatedModules": true, + "resolveJsonModule": true, + /** + * To have warnings / errors of the Svelte compiler at the + * correct position, enable source maps by default. + */ + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable this if you'd like to use dynamic types. + */ + // "checkJs": true + }, + /** + * Use global.d.ts instead of compilerOptions.types + * to avoid limiting type declarations. + */ + "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] } diff --git a/package.json b/package.json index dba82fe9..c6541767 100644 --- a/package.json +++ b/package.json @@ -1,30 +1,30 @@ { - "name": "userscripts", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "npm run build:app && npm run build:ext", - "build:app": "node scripts/build-app.js", - "build:ext": "node scripts/build-ext-v4.js", - "lint:js": "eslint . && prettier --check . --plugin prettier-plugin-svelte", - "lint:css": "stylelint '**/*.{css,html,svelte}'", - "prettier": "prettier --write . --plugin prettier-plugin-svelte" - }, - "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^2.4.5", - "autoprefixer": "^10.4.16", - "cm-show-invisibles": "^3.1.0", - "codemirror": "^5.65.15", - "eslint": "^8.52.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-svelte": "^2.34.0", - "prettier": "3.0.3", - "prettier-plugin-svelte": "3.0.3", - "stylelint": "^15.11.0", - "stylelint-config-html": "^1.1.0", - "stylelint-config-standard": "^34.0.0", - "svelte": "4.2.0", - "vite": "^4.5.0" - } + "name": "userscripts", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "npm run build:app && npm run build:ext", + "build:app": "node scripts/build-app.js", + "build:ext": "node scripts/build-ext-v4.js", + "lint:js": "eslint . && prettier --check . --plugin prettier-plugin-svelte", + "lint:css": "stylelint '**/*.{css,html,svelte}'", + "prettier": "prettier --write . --plugin prettier-plugin-svelte" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^2.4.5", + "autoprefixer": "^10.4.16", + "cm-show-invisibles": "^3.1.0", + "codemirror": "^5.65.15", + "eslint": "^8.52.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-svelte": "^2.34.0", + "prettier": "3.0.3", + "prettier-plugin-svelte": "3.0.3", + "stylelint": "^15.11.0", + "stylelint-config-html": "^1.1.0", + "stylelint-config-standard": "^34.0.0", + "svelte": "4.2.0", + "vite": "^4.5.0" + } } diff --git a/scripts/build-app.js b/scripts/build-app.js index 4563de35..2fedc7ce 100644 --- a/scripts/build-app.js +++ b/scripts/build-app.js @@ -6,8 +6,8 @@ * The assets name is irrelevant, just need to determine the entry path */ -import {build} from "vite"; -import {svelte} from "@sveltejs/vite-plugin-svelte"; +import { build } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; /** * Define default vite config options @@ -16,21 +16,21 @@ import {svelte} from "@sveltejs/vite-plugin-svelte"; * @see {@link https://vitejs.dev/guide/api-javascript.html#inlineconfig configFile} */ const defineConfig = { - base: "./", - configFile: false + base: "./", + configFile: false, }; /** * Build App-Shared WebView resources to xcode dist */ build({ - ...defineConfig, - plugins: [svelte()], - build: { - outDir: "xcode/App-Shared/Resources/dist/", - copyPublicDir: false, - rollupOptions: { - input: "entry-app-webview.html" - } - } + ...defineConfig, + plugins: [svelte()], + build: { + outDir: "xcode/App-Shared/Resources/dist/", + copyPublicDir: false, + rollupOptions: { + input: "entry-app-webview.html", + }, + }, }); diff --git a/scripts/build-ext-v4.js b/scripts/build-ext-v4.js index fb6379db..c2f37010 100644 --- a/scripts/build-ext-v4.js +++ b/scripts/build-ext-v4.js @@ -14,8 +14,8 @@ * The assets name is irrelevant, just need to determine the entry path */ -import {build} from "vite"; -import {svelte} from "@sveltejs/vite-plugin-svelte"; +import { build } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; /** * Define default vite config options @@ -24,8 +24,8 @@ import {svelte} from "@sveltejs/vite-plugin-svelte"; * @see {@link https://vitejs.dev/guide/api-javascript.html#inlineconfig configFile} */ const defineConfig = { - base: "./", - configFile: false + base: "./", + configFile: false, }; /** @@ -34,17 +34,14 @@ const defineConfig = { * Using a subdirectory avoid emptying other built files */ build({ - ...defineConfig, - plugins: [svelte()], - build: { - outDir: "xcode/Ext-Safari/Resources/dist/s/", - rollupOptions: { - input: [ - "entry-ext-action-popup.html", - "entry-ext-extension-page.html" - ] - } - } + ...defineConfig, + plugins: [svelte()], + build: { + outDir: "xcode/Ext-Safari/Resources/dist/s/", + rollupOptions: { + input: ["entry-ext-action-popup.html", "entry-ext-extension-page.html"], + }, + }, }); /** @@ -52,21 +49,21 @@ build({ * Each entry in the array will generate a separate script */ [ - {background: "src/ext/background/main.js"}, - {content: "src/ext/content-scripts/main.js"} -].forEach(input => { - build({ - ...defineConfig, - build: { - outDir: "xcode/Ext-Safari/Resources/dist/", - emptyOutDir: false, - copyPublicDir: false, - rollupOptions: { - input, - output: { - entryFileNames: "[name].js" - } - } - } - }); + { background: "src/ext/background/main.js" }, + { content: "src/ext/content-scripts/main.js" }, +].forEach((input) => { + build({ + ...defineConfig, + build: { + outDir: "xcode/Ext-Safari/Resources/dist/", + emptyOutDir: false, + copyPublicDir: false, + rollupOptions: { + input, + output: { + entryFileNames: "[name].js", + }, + }, + }, + }); }); diff --git a/scripts/build-ext-v5.js b/scripts/build-ext-v5.js index 6609f418..6e4d0f66 100644 --- a/scripts/build-ext-v5.js +++ b/scripts/build-ext-v5.js @@ -13,8 +13,8 @@ * The assets name is irrelevant, just need to determine the entry path */ -import {build} from "vite"; -import {svelte} from "@sveltejs/vite-plugin-svelte"; +import { build } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; /** * Define default vite config options @@ -23,8 +23,8 @@ import {svelte} from "@sveltejs/vite-plugin-svelte"; * @see {@link https://vitejs.dev/guide/api-javascript.html#inlineconfig configFile} */ const defineConfig = { - base: "./", - configFile: false + base: "./", + configFile: false, }; /** @@ -33,42 +33,40 @@ const defineConfig = { * Using a subdirectory avoid emptying other built files */ build({ - ...defineConfig, - plugins: [svelte()], - build: { - outDir: "xcode/Ext-Safari/Resources/dist/s/", - rollupOptions: { - input: { - background: "src/ext/background/main.js", - "action-popup": "entry-ext-action-popup.html", - "extension-page": "entry-ext-extension-page.html" - }, - output: { - entryFileNames: "[name].js" - } - } - } + ...defineConfig, + plugins: [svelte()], + build: { + outDir: "xcode/Ext-Safari/Resources/dist/s/", + rollupOptions: { + input: { + background: "src/ext/background/main.js", + "action-popup": "entry-ext-action-popup.html", + "extension-page": "entry-ext-extension-page.html", + }, + output: { + entryFileNames: "[name].js", + }, + }, + }, }); /** * Build independent scripts for safari * Each entry in the array will generate a separate script */ -[ - {content: "src/ext/content-scripts/main.js"} -].forEach(input => { - build({ - ...defineConfig, - build: { - outDir: "xcode/Ext-Safari/Resources/dist/", - emptyOutDir: false, - copyPublicDir: false, - rollupOptions: { - input, - output: { - entryFileNames: "[name].js" - } - } - } - }); +[{ content: "src/ext/content-scripts/main.js" }].forEach((input) => { + build({ + ...defineConfig, + build: { + outDir: "xcode/Ext-Safari/Resources/dist/", + emptyOutDir: false, + copyPublicDir: false, + rollupOptions: { + input, + output: { + entryFileNames: "[name].js", + }, + }, + }, + }); }); diff --git a/scripts/preview-app.js b/scripts/preview-app.js index 5784b2bf..691f601c 100644 --- a/scripts/preview-app.js +++ b/scripts/preview-app.js @@ -3,7 +3,7 @@ * @see {@link https://vitejs.dev/guide/api-javascript.html JavaScript API} */ -import {preview} from "vite"; +import { preview } from "vite"; /** * Define default vite config options @@ -12,23 +12,23 @@ import {preview} from "vite"; * @see {@link https://vitejs.dev/guide/api-javascript.html#inlineconfig configFile} */ const defineConfig = { - base: "./", - configFile: false + base: "./", + configFile: false, }; /** * Preview App-Shared WebView resources from xcode dist */ (async () => { - const previewServer = await preview({ - ...defineConfig, - preview: { - // port: 4173, - open: "entry-app-webview.html" - }, - build: { - outDir: "xcode/App-Shared/Resources/dist/" - } - }); - previewServer.printUrls(); + const previewServer = await preview({ + ...defineConfig, + preview: { + // port: 4173, + open: "entry-app-webview.html", + }, + build: { + outDir: "xcode/App-Shared/Resources/dist/", + }, + }); + previewServer.printUrls(); })(); diff --git a/src/app/App.svelte b/src/app/App.svelte index 870c318f..c5f0caba 100644 --- a/src/app/App.svelte +++ b/src/app/App.svelte @@ -1,125 +1,128 @@
- Userscripts App Icon - -

You can turn on the Userscripts iOS Safari extension in Settings. - Read the docs. -

- -
CURRENT DIRECTORY:
-
- -
+ Userscripts App Icon + +

+ You can turn on the Userscripts iOS Safari extension in Settings. + + Read the docs. + +

+ +
CURRENT DIRECTORY:
+
+ +
diff --git a/src/app/app.css b/src/app/app.css index 2aa6eab7..2eaa3de0 100644 --- a/src/app/app.css +++ b/src/app/app.css @@ -1,51 +1,51 @@ html { - font-size: 100%; - height: 100vh; - overflow: hidden; + font-size: 100%; + height: 100vh; + overflow: hidden; } body { - background-color: var(--color-bg-secondary); - color: var(--text-color-primary); - font: var(--text-medium); - height: 100%; - letter-spacing: var(--letter-spacing-medium); - position: relative; - text-rendering: optimizelegibility; - -webkit-font-smoothing: antialiased; + background-color: var(--color-bg-secondary); + color: var(--text-color-primary); + font: var(--text-medium); + height: 100%; + letter-spacing: var(--letter-spacing-medium); + position: relative; + text-rendering: optimizelegibility; + -webkit-font-smoothing: antialiased; } @media screen and (height <= 400px) { - html { - font-size: 80%; - } + html { + font-size: 80%; + } - p { - font: var(--text-large); - letter-spacing: var(--letter-spacing-large); - } + p { + font: var(--text-large); + letter-spacing: var(--letter-spacing-large); + } } @media screen and (height >= 700px) and (width >= 600px) { - html { - font-size: 150%; - } + html { + font-size: 150%; + } } #app { - height: 100%; + height: 100%; } button { - border: none; - cursor: pointer; - padding: 0; - user-select: none; + border: none; + cursor: pointer; + padding: 0; + user-select: none; } .link { - background: none; - color: var(--color-blue); - cursor: pointer; - text-decoration: underline; -} \ No newline at end of file + background: none; + color: var(--color-blue); + cursor: pointer; + text-decoration: underline; +} diff --git a/src/app/main.js b/src/app/main.js index d7459fa0..62676104 100644 --- a/src/app/main.js +++ b/src/app/main.js @@ -4,7 +4,7 @@ import "./app.css"; import App from "./App.svelte"; const app = new App({ - target: document.getElementById("app") + target: document.getElementById("app"), }); export default app; diff --git a/src/app/reset.css b/src/app/reset.css index 6c181e6b..5748075f 100644 --- a/src/app/reset.css +++ b/src/app/reset.css @@ -1,44 +1,44 @@ *, *::before, *::after { - box-sizing: border-box; + box-sizing: border-box; } body, p { - margin: 0; + margin: 0; } ul { - list-style: none; - margin: 0; - padding: 0; + list-style: none; + margin: 0; + padding: 0; } button, input, select, textarea { - font-family: inherit; - font-size: 100%; - line-height: inherit; - margin: 0; + font-family: inherit; + font-size: 100%; + line-height: inherit; + margin: 0; } button, input[type="button"], input[type="reset"], input[type="submit"] { - -webkit-appearance: button; + -webkit-appearance: button; } ::-webkit-search-decoration, ::-webkit-search-cancel-button, ::-webkit-search-results-button, ::-webkit-search-results-decoration { - -webkit-appearance: none; + -webkit-appearance: none; } img { - border-style: none; + border-style: none; } diff --git a/src/app/variables.css b/src/app/variables.css index acdc671d..0c641d23 100644 --- a/src/app/variables.css +++ b/src/app/variables.css @@ -1,51 +1,52 @@ :root { - --border-radius: 0.188rem; - --box-shadow: 0 0.5rem 1rem 0 rgb(0 0 0 / 0.25); - --color-bg-primary: #323639; - --color-bg-secondary: #2f3337; - --color-script-highlighted: #364049; /* rgba(116, 178, 235, 0.1); */ - --color-black: #1d2023; - --color-blue: #74b1eb; - --color-green: #60f36c; - --color-grey: rgb(255 255 255 / 0.15); - --color-red: #ff453a; - --color-yellow: #e4f360; - --letter-spacing-large: -0.031rem; - --letter-spacing-default: -0.029rem; - --letter-spacing-medium: -0.018rem; - --letter-spacing-small: -0.008rem; - --opacity-disabled: 0.3; - --text-color-primary: rgb(255 255 255); - --text-color-secondary: rgb(255 255 255 / 0.65); - --text-color-disabled: rgb(255 255 255 / 0.4); - --font-family: system-ui, -apple-system, "Helvetica Neue", "Helvetica", sans-serif; - --text-default: 1rem/1.5rem var(--font-family); - --text-large: 1.25rem/1.5rem var(--font-family); - --text-medium: 0.875rem/1.313rem var(--font-family); - --text-small: 0.719rem/1rem var(--font-family); + --border-radius: 0.188rem; + --box-shadow: 0 0.5rem 1rem 0 rgb(0 0 0 / 0.25); + --color-bg-primary: #323639; + --color-bg-secondary: #2f3337; + --color-script-highlighted: #364049; /* rgba(116, 178, 235, 0.1); */ + --color-black: #1d2023; + --color-blue: #74b1eb; + --color-green: #60f36c; + --color-grey: rgb(255 255 255 / 0.15); + --color-red: #ff453a; + --color-yellow: #e4f360; + --letter-spacing-large: -0.031rem; + --letter-spacing-default: -0.029rem; + --letter-spacing-medium: -0.018rem; + --letter-spacing-small: -0.008rem; + --opacity-disabled: 0.3; + --text-color-primary: rgb(255 255 255); + --text-color-secondary: rgb(255 255 255 / 0.65); + --text-color-disabled: rgb(255 255 255 / 0.4); + --font-family: system-ui, -apple-system, "Helvetica Neue", "Helvetica", + sans-serif; + --text-default: 1rem/1.5rem var(--font-family); + --text-large: 1.25rem/1.5rem var(--font-family); + --text-medium: 0.875rem/1.313rem var(--font-family); + --text-small: 0.719rem/1rem var(--font-family); - /* editor variables */ - --editor-font: monaco, monospace; - --editor-font-size: 14px; - --editor-line-height: 24px; - --editor-invisible: rgb(255 255 255 / 0.15); - --editor-active-line: var(--color-bg-secondary); - --editor-selected-bg: rgb(116 178 235 / 0.35); - --editor-matched-highlight: rgb(116 178 235 / 0.2); - --editor-search-highlight: rgb(255 166 0 / 0.3); - --editor-number: #77e26a; - --editor-comment: rgb(255 255 255 / 0.35); - --editor-def: #efc371; - --editor-default: #cdcfd1; - --editor-keyword: #96c3ed; - --editor-atom: #59ebf5; - --editor-operator: #8c99a7; - --editor-property: #e86c8a; - --editor-string: #f5eea2; - --editor-string-2: #cdabff; - --editor-error: var(--color-red); - --editor-cursor: #e3e7eb; - --editor-matching-bracket-color: #fff; - --editor-matching-bracket-border: var(--editor-number); - --editor-non-matching-bracket: var(--editor-error); + /* editor variables */ + --editor-font: monaco, monospace; + --editor-font-size: 14px; + --editor-line-height: 24px; + --editor-invisible: rgb(255 255 255 / 0.15); + --editor-active-line: var(--color-bg-secondary); + --editor-selected-bg: rgb(116 178 235 / 0.35); + --editor-matched-highlight: rgb(116 178 235 / 0.2); + --editor-search-highlight: rgb(255 166 0 / 0.3); + --editor-number: #77e26a; + --editor-comment: rgb(255 255 255 / 0.35); + --editor-def: #efc371; + --editor-default: #cdcfd1; + --editor-keyword: #96c3ed; + --editor-atom: #59ebf5; + --editor-operator: #8c99a7; + --editor-property: #e86c8a; + --editor-string: #f5eea2; + --editor-string-2: #cdabff; + --editor-error: var(--color-red); + --editor-cursor: #e3e7eb; + --editor-matching-bracket-color: #fff; + --editor-matching-bracket-border: var(--editor-number); + --editor-non-matching-bracket: var(--editor-error); } diff --git a/src/dev/App.svelte b/src/dev/App.svelte index 1d9c1db6..2ac83b9c 100644 --- a/src/dev/App.svelte +++ b/src/dev/App.svelte @@ -1,38 +1,46 @@
-
- - - -
-

Userscripts dev

+
+ + + +
+

Userscripts dev

-

- native-app, - action-popup, - extension-page -

+

+ + native-app + , + + action-popup + , + + extension-page + +

-

- This is the development preview environment -

+

This is the development preview environment

diff --git a/src/dev/app.css b/src/dev/app.css index 1fedad38..19d0543e 100644 --- a/src/dev/app.css +++ b/src/dev/app.css @@ -1,82 +1,82 @@ /* stylelint-disable */ :root { - font-family: Inter, Avenir, Helvetica, Arial, sans-serif; - font-size: 16px; - line-height: 24px; - font-weight: 400; + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; } a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; + font-weight: 500; + color: #646cff; + text-decoration: inherit; } a:hover { - color: #535bf2; + color: #535bf2; } body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; } h1 { - font-size: 3.2em; - line-height: 1.1; + font-size: 3.2em; + line-height: 1.1; } .card { - padding: 2em; + padding: 2em; } #app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; } button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; } button:hover { - border-color: #646cff; + border-color: #646cff; } button:focus, button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; + outline: 4px auto -webkit-focus-ring-color; } @media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } } diff --git a/src/dev/main.js b/src/dev/main.js index 37103c68..b1acd03a 100644 --- a/src/dev/main.js +++ b/src/dev/main.js @@ -2,7 +2,7 @@ import "./app.css"; import App from "./App.svelte"; const app = new App({ - target: document.getElementById("app") + target: document.getElementById("app"), }); export default app; diff --git a/src/ext/action-popup/App.svelte b/src/ext/action-popup/App.svelte index 92fbc72a..4cd34226 100644 --- a/src/ext/action-popup/App.svelte +++ b/src/ext/action-popup/App.svelte @@ -1,739 +1,760 @@ - +
- - showUpdates = true} - title={"Show updates"} - {disabled} - /> - - + + (showUpdates = true)} + title={"Show updates"} + {disabled} + /> + +
{#if !active} - + {/if} {#if showInstallPrompt} -
- Userscript - {#if scriptChecking} - {showInstallPrompt} - {:else} - {scriptInstalled ? "Installed" : "Detected"}: - - {/if} -
+
+ Userscript + {#if scriptChecking} + {showInstallPrompt} + {:else} + {scriptInstalled ? "Installed" : "Detected"}: + + {/if} +
{/if} {#if errorNotification} -
- {errorNotification} - errorNotification = undefined} - title={"Clear error"} - /> -
+
+ {errorNotification} + (errorNotification = undefined)} + title={"Clear error"} + /> +
{/if} -
- {#if loading} - - {:else} - {#if inactive} -
Popup inactive on extension page
- {:else if firstGuide} -
-

Welcome, first use please: 

- -

to complete the initialization

-
- {:else if initError} -
- Something went wrong:  - -
- {:else if items.length < 1} -
No matched userscripts
- {:else} -
- {#each list as item, i (item.filename)} - toggleItem(item)} - /> - {/each} -
- {/if} - {/if} +
+ {#if loading} + + {:else if inactive} +
Popup inactive on extension page
+ {:else if firstGuide} +
+

Welcome, first use please: 

+ +

to complete the initialization

+
+ {:else if initError} +
+ Something went wrong:  + +
+ {:else if items.length < 1} +
No matched userscripts
+ {:else} +
+ {#each list as item, i (item.filename)} + toggleItem(item)} + /> + {/each} +
+ {/if}
{#if !inactive && platform === "macos"} - + {/if} {#if showUpdates} - showUpdates = false} - showLoaderOnDisabled={true} - abortClick={abortUpdates} - abort={showUpdates} - > - - + (showUpdates = false)} + showLoaderOnDisabled={true} + abortClick={abortUpdates} + abort={showUpdates} + > + + {:else if showInstall} - showInstall = false} - showLoaderOnDisabled={true} - > - showInstall = false} - installConfirmClick={installConfirm} - /> - + (showInstall = false)} + showLoaderOnDisabled={true} + > + (showInstall = false)} + installConfirmClick={installConfirm} + /> + {:else if showAll} - { - showAll = false; - refreshView(); - }} - showLoaderOnDisabled={false} - > - - + { + showAll = false; + refreshView(); + }} + showLoaderOnDisabled={false} + > + + {/if} diff --git a/src/ext/action-popup/Components/PopupItem.svelte b/src/ext/action-popup/Components/PopupItem.svelte index 01e091de..f83fee4b 100644 --- a/src/ext/action-popup/Components/PopupItem.svelte +++ b/src/ext/action-popup/Components/PopupItem.svelte @@ -1,97 +1,97 @@
- -
{name}
- {#if subframe}
SUB
{/if} - + +
{name}
+ {#if subframe}
SUB
{/if} +
diff --git a/src/ext/action-popup/Components/View.svelte b/src/ext/action-popup/Components/View.svelte index 71115f7b..00b6d61f 100644 --- a/src/ext/action-popup/Components/View.svelte +++ b/src/ext/action-popup/Components/View.svelte @@ -1,80 +1,76 @@
-
- {headerTitle} - -
-
- {#if loading && showLoaderOnDisabled} - - {:else} -
Slot content is required...
- {/if} -
+
+ {headerTitle} + +
+
+ {#if loading && showLoaderOnDisabled} + + {:else} +
Slot content is required...
+ {/if} +
diff --git a/src/ext/action-popup/Components/Views/AllItemsView.svelte b/src/ext/action-popup/Components/Views/AllItemsView.svelte index a5a59553..b5457123 100644 --- a/src/ext/action-popup/Components/Views/AllItemsView.svelte +++ b/src/ext/action-popup/Components/Views/AllItemsView.svelte @@ -1,58 +1,58 @@ {#if allItems.length} -
- {#each list as item (item.filename)} - allItemsToggleItem(item)} - /> - {/each} -
+
+ {#each list as item (item.filename)} + allItemsToggleItem(item)} + /> + {/each} +
{:else} -
No valid files found in directory
+
No valid files found in directory
{/if} diff --git a/src/ext/action-popup/Components/Views/InstallView.svelte b/src/ext/action-popup/Components/Views/InstallView.svelte index 9d9cd3fe..44f40869 100644 --- a/src/ext/action-popup/Components/Views/InstallView.svelte +++ b/src/ext/action-popup/Components/Views/InstallView.svelte @@ -1,180 +1,179 @@
- {#if installError} -
- {@html iconError} -
Couldn't install userscript
-

{installError}

-
- {:else if userscript} -
    -
  • {userscript.name}
  • - {#if userscript.description} -
  • {userscript.description}
  • - {/if} - {#if userscript.match} -
  • -
    @match
    - {#each userscript.match as match} -
    {match}
    - {/each} -
  • - {/if} - {#if userscript.include} -
  • -
    @include
    - {#each userscript.include as include} -
    {include}
    - {/each} -
  • - {/if} - {#if userscript.require} -
  • -
    @require
    - {#each userscript.require as require} -
    {require}
    - {/each} -
  • - {/if} - {#if userscript.grant} -
  • -
    @grant
    - {#each userscript.grant as grant} -
    {grant}
    - {/each} -
  • - {/if} -
-
-
{@html iconWarn}
-
Be sure you trust the author before installing. Nefarious code can exploit your security and privacy.
-
-
- - -
- {/if} + {#if installError} +
+ {@html iconError} +
Couldn't install userscript
+

{installError}

+
+ {:else if userscript} +
    +
  • {userscript.name}
  • + {#if userscript.description} +
  • {userscript.description}
  • + {/if} + {#if userscript.match} +
  • +
    @match
    + {#each userscript.match as match} +
    {match}
    + {/each} +
  • + {/if} + {#if userscript.include} +
  • +
    @include
    + {#each userscript.include as include} +
    {include}
    + {/each} +
  • + {/if} + {#if userscript.require} +
  • +
    @require
    + {#each userscript.require as require} +
    {require}
    + {/each} +
  • + {/if} + {#if userscript.grant} +
  • +
    @grant
    + {#each userscript.grant as grant} +
    {grant}
    + {/each} +
  • + {/if} +
+
+
{@html iconWarn}
+
+ Be sure you trust the author before installing. Nefarious code can + exploit your security and privacy. +
+
+
+ + +
+ {/if}
diff --git a/src/ext/action-popup/Components/Views/UpdateView.svelte b/src/ext/action-popup/Components/Views/UpdateView.svelte index fd121c84..901db462 100644 --- a/src/ext/action-popup/Components/Views/UpdateView.svelte +++ b/src/ext/action-popup/Components/Views/UpdateView.svelte @@ -1,88 +1,82 @@ {#if updates.length} - {#each updates as item (item.name)} - -
-
{item.name}
- - Source - - -
- {/each} -

Be sure you trust the author before saving remote code to your device.

- + {#each updates as item (item.name)} + +
+
{item.name}
+ Source + +
+ {/each} +

Be sure you trust the author before saving remote code to your device.

+ {:else} -
- {@html iconUpdate} -
- There are no file updates available -
- -
-
+
+ {@html iconUpdate} +
+ There are no file updates available +
+ +
+
{/if} diff --git a/src/ext/action-popup/app.css b/src/ext/action-popup/app.css index 8bbc54fe..2d2feca2 100644 --- a/src/ext/action-popup/app.css +++ b/src/ext/action-popup/app.css @@ -1,86 +1,86 @@ html { - font-size: 100%; - overflow: hidden; + font-size: 100%; + overflow: hidden; } body { - background-color: var(--color-bg-secondary); - color: var(--text-color-primary); - font: var(--text-medium); - letter-spacing: var(--letter-spacing-medium); - position: relative; - text-rendering: optimizelegibility; - width: 18rem; - -webkit-font-smoothing: antialiased; + background-color: var(--color-bg-secondary); + color: var(--text-color-primary); + font: var(--text-medium); + letter-spacing: var(--letter-spacing-medium); + position: relative; + text-rendering: optimizelegibility; + width: 18rem; + -webkit-font-smoothing: antialiased; } /* attempts to detect iOS */ @media (hover: none) { - html { - font-size: 125%; - } + html { + font-size: 125%; + } - body { - width: 100vw; - } + body { + width: 100vw; + } } @media screen and (width >= 481px) and (orientation: portrait) and (hover: none) { - body { - width: 18rem; - } + body { + width: 18rem; + } } @media screen and (width >= 1024px) and (orientation: landscape) and (hover: none) { - body { - width: 18rem; - } + body { + width: 18rem; + } } body.ipados { - width: 18rem; - height: 100vh; + width: 18rem; + height: 100vh; } body.ipados #app { - height: 100%; + height: 100%; } body.ios { - height: 100vh; - width: 100vw; + height: 100vh; + width: 100vw; } body.ios #app { - height: 100%; + height: 100%; } noscript { - display: block; + display: block; } button { - border: none; - cursor: pointer; - padding: 0; - user-select: none; + border: none; + cursor: pointer; + padding: 0; + user-select: none; } button:disabled { - opacity: var(--opacity-disabled); - pointer-events: none; + opacity: var(--opacity-disabled); + pointer-events: none; } a, .link { - background: none; - color: var(--color-blue); - cursor: pointer; - text-decoration: underline; + background: none; + color: var(--color-blue); + cursor: pointer; + text-decoration: underline; } .truncate { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } diff --git a/src/ext/action-popup/main.js b/src/ext/action-popup/main.js index ed71b808..48a7c630 100644 --- a/src/ext/action-popup/main.js +++ b/src/ext/action-popup/main.js @@ -3,37 +3,38 @@ import "../shared/variables.css"; import "./app.css"; import App from "./App.svelte"; -if (import.meta.env.MODE === "development") { // vite feat that only import in dev mode - const modules = import.meta.glob("../shared/dev.js", {eager: true}); - window.browser = modules["../shared/dev.js"].browser; - console.info("DEV-ENV", import.meta.env, modules, browser); - // macos popup simulation - const style = document.createElement("style"); - style.textContent = ` +// vite feat that only import in dev mode +if (import.meta.env.MODE === "development") { + const modules = import.meta.glob("../shared/dev.js", { eager: true }); + window.browser = modules["../shared/dev.js"].browser; + console.info("DEV-ENV", import.meta.env, modules, browser); + // macos popup simulation + const style = document.createElement("style"); + style.textContent = ` body { - top: 20px; - left: 20px; - box-sizing: content-box; - border: 2px solid #0c0e0f; - border-radius: 10px; - box-shadow: 2px 2px 20px rgba(0, 0, 0, 0.2); + top: 20px; + left: 20px; + box-sizing: content-box; + border: 2px solid #0c0e0f; + border-radius: 10px; + box-shadow: 2px 2px 20px rgba(0, 0, 0, 0.2); } body:before { - content: ""; - position: absolute; - top: -1px; - left: -1px; - right: -1px; - bottom: -1px; - border: 1px solid #54575a; - border-radius: 9px; + content: ""; + position: absolute; + top: -1px; + left: -1px; + right: -1px; + bottom: -1px; + border: 1px solid #54575a; + border-radius: 9px; } `; - browser.platform === "macos" && document.head.append(style); + browser.platform === "macos" && document.head.append(style); } const app = new App({ - target: document.getElementById("app") + target: document.getElementById("app"), }); export default app; diff --git a/src/ext/background/main.js b/src/ext/background/main.js index d8a57ee3..1df31003 100644 --- a/src/ext/background/main.js +++ b/src/ext/background/main.js @@ -1,119 +1,124 @@ -import {openExtensionPage} from "../shared/utils.js"; -// eslint-disable-next-line import/no-namespace +import { openExtensionPage } from "../shared/utils.js"; import * as settingsStorage from "../shared/settings.js"; // first sorts files by run-at value, then by weight value function userscriptSort(a, b) { - // map the run-at values to numeric values - const runAtValues = { - "document-start": 1, - "document-end": 2, - "document-idle": 3 - }; - const runAtA = a.scriptObject["run-at"]; - const runAtB = b.scriptObject["run-at"]; - if (runAtA !== runAtB && runAtValues[runAtA] && runAtValues[runAtB]) { - return runAtValues[runAtA] > runAtValues[runAtB]; - } - return Number(a.scriptObject.weight) < Number(b.scriptObject.weight); + // map the run-at values to numeric values + const runAtValues = { + "document-start": 1, + "document-end": 2, + "document-idle": 3, + }; + const runAtA = a.scriptObject["run-at"]; + const runAtB = b.scriptObject["run-at"]; + if (runAtA !== runAtB && runAtValues[runAtA] && runAtValues[runAtB]) { + return runAtValues[runAtA] > runAtValues[runAtB]; + } + return Number(a.scriptObject.weight) < Number(b.scriptObject.weight); } async function readAsDataURL(blob) { - return new Promise(resolve => { - const reader = new FileReader(); - reader.readAsDataURL(blob); - reader.onloadend = () => resolve(reader.result); // base64data - }); + return new Promise((resolve) => { + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = () => resolve(reader.result); // base64data + }); } async function getPlatform() { - let platform = localStorage.getItem("platform"); - if (!platform) { - const message = {name: "REQ_PLATFORM"}; - const response = await browser.runtime.sendNativeMessage(message); - if (!response.platform) { - console.error("Failed to get platform"); - return ""; - } - platform = response.platform; - localStorage.setItem("platform", platform); - } - return platform; + let platform = localStorage.getItem("platform"); + if (!platform) { + const message = { name: "REQ_PLATFORM" }; + const response = await browser.runtime.sendNativeMessage(message); + if (!response.platform) { + console.error("Failed to get platform"); + return ""; + } + platform = response.platform; + localStorage.setItem("platform", platform); + } + return platform; } function setClipboard(data, type = "text/plain") { - // future enhancement? - // https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/write - // https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText - const onCopy = e => { - e.stopImmediatePropagation(); - e.preventDefault(); - e.clipboardData.setData(type, data); - document.removeEventListener("copy", onCopy, true); - }; + // future enhancement? + // https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/write + // https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText + const onCopy = (e) => { + e.stopImmediatePropagation(); + e.preventDefault(); + e.clipboardData.setData(type, data); + document.removeEventListener("copy", onCopy, true); + }; - const textarea = document.createElement("textarea"); - textarea.textContent = ""; - document.body.appendChild(textarea); - textarea.select(); - document.addEventListener("copy", onCopy, true); - try { - return document.execCommand("copy"); - } catch (error) { - console.warn("setClipboard failed", error); - document.removeEventListener("copy", onCopy, true); - return false; - } finally { - document.body.removeChild(textarea); - } + const textarea = document.createElement("textarea"); + textarea.textContent = ""; + document.body.appendChild(textarea); + textarea.select(); + document.addEventListener("copy", onCopy, true); + try { + return document.execCommand("copy"); + } catch (error) { + console.warn("setClipboard failed", error); + document.removeEventListener("copy", onCopy, true); + return false; + } finally { + document.body.removeChild(textarea); + } } async function setBadgeCount() { - const clearBadge = () => browser.browserAction.setBadgeText({text: ""}); - // @todo until better introduce in ios, only set badge on macOS - const platform = await getPlatform(); - if (platform !== "macos") return clearBadge(); - // @todo settingsStorage.get("global_exclude_match") - const settings = await settingsStorage.get(["global_active", "toolbar_badge_count"]); - if (settings.global_active === false) return clearBadge(); - if (settings.toolbar_badge_count === false) return clearBadge(); + const clearBadge = () => browser.browserAction.setBadgeText({ text: "" }); + // @todo until better introduce in ios, only set badge on macOS + const platform = await getPlatform(); + if (platform !== "macos") return clearBadge(); + // @todo settingsStorage.get("global_exclude_match") + const settings = await settingsStorage.get([ + "global_active", + "toolbar_badge_count", + ]); + if (settings.global_active === false) return clearBadge(); + if (settings.toolbar_badge_count === false) return clearBadge(); - const currentTab = await browser.tabs.getCurrent(); - // no active tabs exist (user closed all windows) - if (!currentTab) return clearBadge(); - const url = currentTab.url; - // if url doesn't exist, stop - if (!url) return clearBadge(); - // only check for http/s pages - if (!url.startsWith("http://") && !url.startsWith("https://")) return clearBadge(); - // @todo if url match in global exclude list, clear badge - const frameUrls = new Set(); - const frames = await browser.webNavigation.getAllFrames({tabId: currentTab.id}); - for (let i = 0; i < frames.length; i++) { - const frameUrl = frames[i].url; - if (frameUrl !== url && frameUrl.startsWith("http")) { - frameUrls.add(frameUrl); - } - } - const message = { - name: "POPUP_BADGE_COUNT", - url, - frameUrls: Array.from(frameUrls) - }; - browser.runtime.sendNativeMessage(message, response => { - if (response.error) return console.error(response.error); - const count = response.count; - if (count > 0) { - browser.browserAction.setBadgeText({text: count.toString()}); - } else { - const _url = new URL(url); - if (_url.pathname.endsWith(".user.js")) { - browser.browserAction.setBadgeText({text: "JS"}); - } else { - clearBadge(); - } - } - }); + const currentTab = await browser.tabs.getCurrent(); + // no active tabs exist (user closed all windows) + if (!currentTab) return clearBadge(); + const url = currentTab.url; + // if url doesn't exist, stop + if (!url) return clearBadge(); + // only check for http/s pages + if (!url.startsWith("http://") && !url.startsWith("https://")) + return clearBadge(); + // @todo if url match in global exclude list, clear badge + const frameUrls = new Set(); + const frames = await browser.webNavigation.getAllFrames({ + tabId: currentTab.id, + }); + for (let i = 0; i < frames.length; i++) { + const frameUrl = frames[i].url; + if (frameUrl !== url && frameUrl.startsWith("http")) { + frameUrls.add(frameUrl); + } + } + const message = { + name: "POPUP_BADGE_COUNT", + url, + frameUrls: Array.from(frameUrls), + }; + browser.runtime.sendNativeMessage(message, (response) => { + if (response.error) return console.error(response.error); + const count = response.count; + if (count > 0) { + browser.browserAction.setBadgeText({ text: count.toString() }); + } else { + const _url = new URL(url); + if (_url.pathname.endsWith(".user.js")) { + browser.browserAction.setBadgeText({ text: "JS" }); + } else { + clearBadge(); + } + } + }); } // on startup get declarativeNetRequests @@ -123,380 +128,390 @@ async function setBadgeCount() { // 2. dnr item toggle event in the page occurs // 3. external editor changes script file content async function setSessionRules() { - // not supported below safari 15.4 - if (!browser.declarativeNetRequest.updateSessionRules) return; - await clearAllSessionRules(); - const message = {name: "REQ_REQUESTS"}; - const response = await browser.runtime.sendNativeMessage(message); - if (response.error) { - console.error(response.error); - return; - } - // there are no rules to apply - if (!response.length) return; - // loop through response, parse the rules, push to array and log - const rules = []; - for (let i = 0; i < response.length; i++) { - const rule = response[i]; - const code = JSON.parse(rule.code); - // check if an array or single rule - if (Array.isArray(code)) { - code.forEach(r => rules.push(r)); - console.info(`Setting session rule: ${rule.name} (${code.length})`); - } else { - rules.push(code); - console.info(`Setting session rule: ${rule.name}`); - } - } - // generate unique ids for all rules to ensure no repeats - const ids = randomNumberSet(1000, rules.length); - rules.map((rule, index) => rule.id = ids[index]); - try { - await browser.declarativeNetRequest.updateSessionRules({addRules: rules}); - } catch (error) { - console.error(`Error setting session rules: ${error}`); - return; - } - console.info(`Finished setting ${rules.length} session rules`); + // not supported below safari 15.4 + if (!browser.declarativeNetRequest.updateSessionRules) return; + await clearAllSessionRules(); + const message = { name: "REQ_REQUESTS" }; + const response = await browser.runtime.sendNativeMessage(message); + if (response.error) { + console.error(response.error); + return; + } + // there are no rules to apply + if (!response.length) return; + // loop through response, parse the rules, push to array and log + const rules = []; + for (let i = 0; i < response.length; i++) { + const rule = response[i]; + const code = JSON.parse(rule.code); + // check if an array or single rule + if (Array.isArray(code)) { + code.forEach((r) => rules.push(r)); + console.info(`Setting session rule: ${rule.name} (${code.length})`); + } else { + rules.push(code); + console.info(`Setting session rule: ${rule.name}`); + } + } + // generate unique ids for all rules to ensure no repeats + const ids = randomNumberSet(1000, rules.length); + rules.map((rule, index) => (rule.id = ids[index])); + try { + await browser.declarativeNetRequest.updateSessionRules({ addRules: rules }); + } catch (error) { + console.error(`Error setting session rules: ${error}`); + return; + } + console.info(`Finished setting ${rules.length} session rules`); } async function clearAllSessionRules() { - const rules = await browser.declarativeNetRequest.getSessionRules(); - if (!rules.length) return; - console.info(`Clearing ${rules.length} session rules`); - const ruleIds = rules.map(a => a.id); - await browser.declarativeNetRequest.updateSessionRules({ - removeRuleIds: ruleIds - }); + const rules = await browser.declarativeNetRequest.getSessionRules(); + if (!rules.length) return; + console.info(`Clearing ${rules.length} session rules`); + const ruleIds = rules.map((a) => a.id); + await browser.declarativeNetRequest.updateSessionRules({ + removeRuleIds: ruleIds, + }); } function randomNumberSet(max, count) { - // generates a set of random unique numbers - // returns an array - const numbers = new Set(); - while (numbers.size < count) { - numbers.add((Math.floor(Math.random() * (max - 1 + 1)) + 1)); - } - return [...numbers]; + // generates a set of random unique numbers + // returns an array + const numbers = new Set(); + while (numbers.size < count) { + numbers.add(Math.floor(Math.random() * (max - 1 + 1)) + 1); + } + return [...numbers]; } // the current update logic is similar to setSessionRules() // this feature needs a more detailed redesign in the future // https://github.com/quoid/userscripts/issues/453 async function getContextMenuItems() { - // macos exclusive feature - const platform = await getPlatform(); - if (platform !== "macos") return; - // since it's not possible to get a list of currently active menu items - // on update, all context-menu items are cleared, then re-added - // this is done to ensure fresh code changes appear - await browser.menus.removeAll(); - // get the context-menu scripts - const message = {name: "REQ_CONTEXT_MENU_SCRIPTS"}; - const response = await browser.runtime.sendNativeMessage(message); - if (response.error) { - console.error(response.error); - return; - } - // add menus items - const items = response.files?.menu || []; - if (items.length) { - console.info(`Setting ${items.length} context-menu userscripts`); - } - for (let i = 0; i < items.length; i++) { - const item = items[i]; - // context-menu scripts require @match value - // @include values are ignored - if (!item.scriptObject.matches.length) continue; - addContextMenuItem(item); - } + // macos exclusive feature + const platform = await getPlatform(); + if (platform !== "macos") return; + // since it's not possible to get a list of currently active menu items + // on update, all context-menu items are cleared, then re-added + // this is done to ensure fresh code changes appear + await browser.menus.removeAll(); + // get the context-menu scripts + const message = { name: "REQ_CONTEXT_MENU_SCRIPTS" }; + const response = await browser.runtime.sendNativeMessage(message); + if (response.error) { + console.error(response.error); + return; + } + // add menus items + const items = response.files?.menu || []; + if (items.length) { + console.info(`Setting ${items.length} context-menu userscripts`); + } + for (let i = 0; i < items.length; i++) { + const item = items[i]; + // context-menu scripts require @match value + // @include values are ignored + if (!item.scriptObject.matches.length) continue; + addContextMenuItem(item); + } } async function addContextMenuItem(userscript) { - // context-menu items persist for a session - // to avoid duplication, when created, save the filename to session storage - const savedItems = sessionStorage.getItem("menu"); - // if the session storage key doesn't exist use empty array - const activeItems = savedItems ? JSON.parse(savedItems) : []; - if (activeItems.indexOf(userscript.scriptObject.filename) !== -1) { - // if already saved, remove it, to get fresh code changes - await browser.menus.remove(userscript.scriptObject.filename); - } - // potential bug? https://developer.apple.com/forums/thread/685273 - // https://stackoverflow.com/q/68431201 - // parse through match values and change pathnames to deal with bug - const patterns = userscript.scriptObject.matches; - patterns.forEach((pattern, index) => { - try { - const url = new URL(pattern); - let pathname = url.pathname; - if (pathname.length > 1 && pathname.endsWith("/")) { - pathname = pathname.slice(0, -1); - } - patterns[index] = `${url.protocol}//${url.hostname}${pathname}`; - } catch (error) { - // prevent breaking when non-url pattern present - } - }); + // context-menu items persist for a session + // to avoid duplication, when created, save the filename to session storage + const savedItems = sessionStorage.getItem("menu"); + // if the session storage key doesn't exist use empty array + const activeItems = savedItems ? JSON.parse(savedItems) : []; + if (activeItems.indexOf(userscript.scriptObject.filename) !== -1) { + // if already saved, remove it, to get fresh code changes + await browser.menus.remove(userscript.scriptObject.filename); + } + // potential bug? https://developer.apple.com/forums/thread/685273 + // https://stackoverflow.com/q/68431201 + // parse through match values and change pathnames to deal with bug + const patterns = userscript.scriptObject.matches; + patterns.forEach((pattern, index) => { + try { + const url = new URL(pattern); + let pathname = url.pathname; + if (pathname.length > 1 && pathname.endsWith("/")) { + pathname = pathname.slice(0, -1); + } + patterns[index] = `${url.protocol}//${url.hostname}${pathname}`; + } catch (error) { + // prevent breaking when non-url pattern present + } + }); - browser.menus.create({ - contexts: ["all"], - documentUrlPatterns: patterns, - id: userscript.scriptObject.filename, - title: userscript.scriptObject.name - }, () => { - // add event listener if needed - if (!browser.menus.onClicked.hasListener(contextClick)) { - browser.menus.onClicked.addListener(contextClick); - } - // save the context-menu item reference to sessionStorage - const value = JSON.stringify([userscript.scriptObject.filename]); - sessionStorage.setItem("menu", value); - }); + browser.menus.create( + { + contexts: ["all"], + documentUrlPatterns: patterns, + id: userscript.scriptObject.filename, + title: userscript.scriptObject.name, + }, + () => { + // add event listener if needed + if (!browser.menus.onClicked.hasListener(contextClick)) { + browser.menus.onClicked.addListener(contextClick); + } + // save the context-menu item reference to sessionStorage + const value = JSON.stringify([userscript.scriptObject.filename]); + sessionStorage.setItem("menu", value); + }, + ); } function contextClick(info, tab) { - // when any created context-menu item is clicked, send message to tab - // the content script for that tag will have the context-menu code - // which will get send back in the response if/when found - const message = {name: "CONTEXT_RUN", menuItemId: info.menuItemId}; - browser.tabs.sendMessage(tab.id, message, response => { - // if code is returned, execute on that tab - if (!response.code) return; - browser.tabs.executeScript(tab.id, { - code: response.code - }); - }); + // when any created context-menu item is clicked, send message to tab + // the content script for that tag will have the context-menu code + // which will get send back in the response if/when found + const message = { name: "CONTEXT_RUN", menuItemId: info.menuItemId }; + browser.tabs.sendMessage(tab.id, message, (response) => { + // if code is returned, execute on that tab + if (!response.code) return; + browser.tabs.executeScript(tab.id, { + code: response.code, + }); + }); } async function nativeChecks() { - const response = await browser.runtime.sendNativeMessage({name: "NATIVE_CHECKS"}); - if (response.error) { - settingsStorage.set({error_native: response}); - return false; - } - settingsStorage.reset("error_native"); - return true; + const response = await browser.runtime.sendNativeMessage({ + name: "NATIVE_CHECKS", + }); + if (response.error) { + settingsStorage.set({ error_native: response }); + return false; + } + settingsStorage.reset("error_native"); + return true; } // handles messages sent with browser.runtime.sendMessage function handleMessage(request, sender, sendResponse) { - switch (request.name) { - case "REQ_USERSCRIPTS": { - // get the page url from the content script that sent request - const url = sender.url; - // use frameId to determine if request came from top level window - // if @noframes true, and isTop false, swift layer won't return code - const isTop = sender.frameId === 0; - // send request to swift layer to provide code for page url - const message = {name: "REQ_USERSCRIPTS", url, isTop}; - browser.runtime.sendNativeMessage(message, response => { - // if request failed, send error to content script for logging - if (response.error) return sendResponse(response); - // sort files - response.files.js.sort(userscriptSort); - response.files.css.sort((a, b) => { - return Number(a.weight) < Number(b.weight); - }); - // return sorted files for injection - sendResponse(response); - }); - return true; - } - case "API_CLOSE_TAB": { - const tabId = request.tabId || sender.tab.id; - browser.tabs.remove(tabId, () => sendResponse({success: 1})); - return true; - } - case "API_OPEN_TAB": { - const props = { - active: request.active, - index: sender.tab.index + 1, - url: request.url - }; - browser.tabs.create(props, response => sendResponse(response)); - return true; - } - case "API_ADD_STYLE": { - const tabId = sender.tab.id; - const details = {code: request.css, cssOrigin: "user"}; - browser.tabs.insertCSS(tabId, details, () => { - sendResponse(request.css); - }); - return true; - } - case "API_GET_TAB": { - let tab = null; - if (typeof sender.tab !== "undefined") { - const tabData = sessionStorage.getItem(`tab-${sender.tab.id}`); - try { - // if tabData is null, can still parse it and return that - tab = JSON.parse(tabData); - } catch (error) { - console.error("failed to parse tab data for getTab"); - } - } else { - console.error("unable to deliver tab due to empty tab id"); - } - sendResponse(tab == null ? {} : tab); - break; - } - case "API_SAVE_TAB": { - if (sender.tab != null && sender.tab.id) { - const key = `tab-${sender.tab.id}`; - sessionStorage.setItem(key, JSON.stringify(request.tab)); - sendResponse({success: true}); - } else { - console.error("unable to save tab, empty tab id"); - sendResponse({success: false}); - } - break; - } - case "API_SET_CLIPBOARD": { - const result = setClipboard(request.clipboardData, request.type); - sendResponse(result); - break; - } - case "API_XHR": { - // parse details and set up for XMLHttpRequest - const details = request.details; - const method = details.method ? details.method : "GET"; - const user = details.user || null; - const password = details.password || null; - let body = details.data || null; - if (body != null && details.binary != null) { - const len = body.length; - const arr = new Uint8Array(len); - for (let i = 0; i < len; i++) { - arr[i] = body.charCodeAt(i); - } - body = new Blob([arr], {type: "text/plain"}); - } - // establish a long-lived port connection to content script - const port = browser.tabs.connect(sender.tab.id, { - name: request.xhrPortName - }); - // set up XMLHttpRequest - const xhr = new XMLHttpRequest(); - xhr.withCredentials = (details.user && details.password); - xhr.timeout = details.timeout || 0; - if (details.overrideMimeType) { - xhr.overrideMimeType(details.overrideMimeType); - } - // add required listeners and send result back to the content script - for (const e of request.events) { - if (!details[e]) continue; - xhr[e] = async event => { - // can not send xhr through postMessage - // construct new object to be sent as "response" - const x = { - readyState: xhr.readyState, - response: xhr.response, - responseHeaders: xhr.getAllResponseHeaders(), - responseType: xhr.responseType, - responseURL: xhr.responseURL, - status: xhr.status, - statusText: xhr.statusText, - timeout: xhr.timeout, - withCredentials: xhr.withCredentials - }; - // only include responseText when needed - if (["", "text"].indexOf(xhr.responseType) !== -1) { - x.responseText = xhr.responseText; - } - // only process when xhr is complete and data exist - if (xhr.readyState === 4 && xhr.response !== null) { - // need to convert arraybuffer data to postMessage - if (xhr.responseType === "arraybuffer") { - const arr = Array.from(new Uint8Array(xhr.response)); - x.response = arr; - } - // need to convert blob data to postMessage - if (xhr.responseType === "blob") { - const base64data = await readAsDataURL(xhr.response); - x.response = { - data: base64data, - type: xhr.responseType - }; - } - } - port.postMessage({name: e, event, response: x}); - }; - } - xhr.open(method, details.url, true, user, password); - xhr.responseType = details.responseType || ""; - if (details.headers) { - for (const key in details.headers) { - if (!key.startsWith("Proxy-") && !key.startsWith("Sec-")) { - const val = details.headers[key]; - xhr.setRequestHeader(key, val); - } - } - } - // receive messages from content script and process them - port.onMessage.addListener(msg => { - if (msg.name === "ABORT") xhr.abort(); - if (msg.name === "DISCONNECT") port.disconnect(); - }); - // handle port disconnect and clean tasks - port.onDisconnect.addListener(p => { - if (p?.error) { - console.error(`port disconnected due to an error: ${p.error.message}`); - } - }); - xhr.send(body); - // if onloadend not set in xhr details - // onloadend event won't be passed to content script - // if that happens port DISCONNECT message won't be posted - // if details lacks onloadend attach listener - if (!details.onloadend) { - xhr.onloadend = event => { - port.postMessage({name: "onloadend", event}); - }; - } - break; - } - case "REFRESH_SESSION_RULES": { - setSessionRules(); - break; - } - case "REFRESH_CONTEXT_MENU_SCRIPTS": { - getContextMenuItems(); - break; - } - } + switch (request.name) { + case "REQ_USERSCRIPTS": { + // get the page url from the content script that sent request + const url = sender.url; + // use frameId to determine if request came from top level window + // if @noframes true, and isTop false, swift layer won't return code + const isTop = sender.frameId === 0; + // send request to swift layer to provide code for page url + const message = { name: "REQ_USERSCRIPTS", url, isTop }; + browser.runtime.sendNativeMessage(message, (response) => { + // if request failed, send error to content script for logging + if (response.error) return sendResponse(response); + // sort files + response.files.js.sort(userscriptSort); + response.files.css.sort((a, b) => { + return Number(a.weight) < Number(b.weight); + }); + // return sorted files for injection + sendResponse(response); + }); + return true; + } + case "API_CLOSE_TAB": { + const tabId = request.tabId || sender.tab.id; + browser.tabs.remove(tabId, () => sendResponse({ success: 1 })); + return true; + } + case "API_OPEN_TAB": { + const props = { + active: request.active, + index: sender.tab.index + 1, + url: request.url, + }; + browser.tabs.create(props, (response) => sendResponse(response)); + return true; + } + case "API_ADD_STYLE": { + const tabId = sender.tab.id; + const details = { code: request.css, cssOrigin: "user" }; + browser.tabs.insertCSS(tabId, details, () => { + sendResponse(request.css); + }); + return true; + } + case "API_GET_TAB": { + let tab = null; + if (typeof sender.tab !== "undefined") { + const tabData = sessionStorage.getItem(`tab-${sender.tab.id}`); + try { + // if tabData is null, can still parse it and return that + tab = JSON.parse(tabData); + } catch (error) { + console.error("failed to parse tab data for getTab"); + } + } else { + console.error("unable to deliver tab due to empty tab id"); + } + sendResponse(tab == null ? {} : tab); + break; + } + case "API_SAVE_TAB": { + if (sender.tab != null && sender.tab.id) { + const key = `tab-${sender.tab.id}`; + sessionStorage.setItem(key, JSON.stringify(request.tab)); + sendResponse({ success: true }); + } else { + console.error("unable to save tab, empty tab id"); + sendResponse({ success: false }); + } + break; + } + case "API_SET_CLIPBOARD": { + const result = setClipboard(request.clipboardData, request.type); + sendResponse(result); + break; + } + case "API_XHR": { + // parse details and set up for XMLHttpRequest + const details = request.details; + const method = details.method ? details.method : "GET"; + const user = details.user || null; + const password = details.password || null; + let body = details.data || null; + if (body != null && details.binary != null) { + const len = body.length; + const arr = new Uint8Array(len); + for (let i = 0; i < len; i++) { + arr[i] = body.charCodeAt(i); + } + body = new Blob([arr], { type: "text/plain" }); + } + // establish a long-lived port connection to content script + const port = browser.tabs.connect(sender.tab.id, { + name: request.xhrPortName, + }); + // set up XMLHttpRequest + const xhr = new XMLHttpRequest(); + xhr.withCredentials = details.user && details.password; + xhr.timeout = details.timeout || 0; + if (details.overrideMimeType) { + xhr.overrideMimeType(details.overrideMimeType); + } + // add required listeners and send result back to the content script + for (const e of request.events) { + if (!details[e]) continue; + xhr[e] = async (event) => { + // can not send xhr through postMessage + // construct new object to be sent as "response" + const x = { + readyState: xhr.readyState, + response: xhr.response, + responseHeaders: xhr.getAllResponseHeaders(), + responseType: xhr.responseType, + responseURL: xhr.responseURL, + status: xhr.status, + statusText: xhr.statusText, + timeout: xhr.timeout, + withCredentials: xhr.withCredentials, + }; + // only include responseText when needed + if (["", "text"].indexOf(xhr.responseType) !== -1) { + x.responseText = xhr.responseText; + } + // only process when xhr is complete and data exist + if (xhr.readyState === 4 && xhr.response !== null) { + // need to convert arraybuffer data to postMessage + if (xhr.responseType === "arraybuffer") { + const arr = Array.from(new Uint8Array(xhr.response)); + x.response = arr; + } + // need to convert blob data to postMessage + if (xhr.responseType === "blob") { + const base64data = await readAsDataURL(xhr.response); + x.response = { + data: base64data, + type: xhr.responseType, + }; + } + } + port.postMessage({ name: e, event, response: x }); + }; + } + xhr.open(method, details.url, true, user, password); + xhr.responseType = details.responseType || ""; + if (details.headers) { + for (const key in details.headers) { + if (!key.startsWith("Proxy-") && !key.startsWith("Sec-")) { + const val = details.headers[key]; + xhr.setRequestHeader(key, val); + } + } + } + // receive messages from content script and process them + port.onMessage.addListener((msg) => { + if (msg.name === "ABORT") xhr.abort(); + if (msg.name === "DISCONNECT") port.disconnect(); + }); + // handle port disconnect and clean tasks + port.onDisconnect.addListener((p) => { + if (p?.error) { + console.error( + `port disconnected due to an error: ${p.error.message}`, + ); + } + }); + xhr.send(body); + // if onloadend not set in xhr details + // onloadend event won't be passed to content script + // if that happens port DISCONNECT message won't be posted + // if details lacks onloadend attach listener + if (!details.onloadend) { + xhr.onloadend = (event) => { + port.postMessage({ name: "onloadend", event }); + }; + } + break; + } + case "REFRESH_SESSION_RULES": { + setSessionRules(); + break; + } + case "REFRESH_CONTEXT_MENU_SCRIPTS": { + getContextMenuItems(); + break; + } + } } browser.runtime.onInstalled.addListener(async () => { - nativeChecks(); + nativeChecks(); }); browser.runtime.onStartup.addListener(async () => { - setSessionRules(); - getContextMenuItems(); + setSessionRules(); + getContextMenuItems(); }); // listens for messages from content script, popup and page browser.runtime.onMessage.addListener(handleMessage); // set the badge count browser.tabs.onActivated.addListener(setBadgeCount); -browser.windows.onFocusChanged.addListener(async windowId => { - if (windowId < 1) { // lose focus - return; - } - nativeChecks(); - setBadgeCount(); - setSessionRules(); - getContextMenuItems(); +browser.windows.onFocusChanged.addListener(async (windowId) => { + if (windowId < 1) { + // lose focus + return; + } + nativeChecks(); + setBadgeCount(); + setSessionRules(); + getContextMenuItems(); }); browser.webNavigation.onCompleted.addListener(setBadgeCount); // handle native app messages const port = browser.runtime.connectNative(); -port.onMessage.addListener(message => { - // console.info(message); // DEBUG - if (message.name === "SAVE_LOCATION_CHANGED") { - openExtensionPage(); - if (message?.userInfo?.returnApp === true) browser.runtime.sendNativeMessage({name: "OPEN_APP"}); - } - // if (message.name === "OPEN_EXTENSION_PAGE") { - // openExtensionPage(); - // } +port.onMessage.addListener((message) => { + // console.info(message); // DEBUG + if (message.name === "SAVE_LOCATION_CHANGED") { + openExtensionPage(); + if (message?.userInfo?.returnApp === true) { + browser.runtime.sendNativeMessage({ name: "OPEN_APP" }); + } + } + // if (message.name === "OPEN_EXTENSION_PAGE") { + // openExtensionPage(); + // } }); diff --git a/src/ext/content-scripts/api.js b/src/ext/content-scripts/api.js index 3907cb98..9b9f6337 100644 --- a/src/ext/content-scripts/api.js +++ b/src/ext/content-scripts/api.js @@ -1,232 +1,232 @@ function setValue(key, value) { - if (typeof key !== "string" || !key.length) { - return console.error("setValue invalid key arg"); - } - if (value == null) { - return console.error("setValue invalid value arg"); - } - return new Promise(resolve => { - const item = {}; - item[`${this.US_filename}---${key}`] = value; - browser.storage.local.set(item, () => resolve({success: 1})); - }); + if (typeof key !== "string" || !key.length) { + return console.error("setValue invalid key arg"); + } + if (value == null) { + return console.error("setValue invalid value arg"); + } + return new Promise((resolve) => { + const item = {}; + item[`${this.US_filename}---${key}`] = value; + browser.storage.local.set(item, () => resolve({ success: 1 })); + }); } function getValue(key, defaultValue) { - if (typeof key !== "string" || !key.length) { - return console.error("getValue invalid key arg"); - } - const prefixedKey = `${this.US_filename}---${key}`; - return new Promise(resolve => { - browser.storage.local.get(prefixedKey, item => { - if (Object.keys(item).length === 0) { - if (defaultValue != null) { - resolve(defaultValue); - } else { - resolve(undefined); - } - } else { - resolve(Object.values(item)[0]); - } - }); - }); + if (typeof key !== "string" || !key.length) { + return console.error("getValue invalid key arg"); + } + const prefixedKey = `${this.US_filename}---${key}`; + return new Promise((resolve) => { + browser.storage.local.get(prefixedKey, (item) => { + if (Object.keys(item).length === 0) { + if (defaultValue != null) { + resolve(defaultValue); + } else { + resolve(undefined); + } + } else { + resolve(Object.values(item)[0]); + } + }); + }); } function deleteValue(key) { - if (typeof key !== "string" || !key.length) { - return console.error("deleteValue missing key arg"); - } - return new Promise(resolve => { - const prefixedKey = `${this.US_filename}---${key}`; - browser.storage.local.remove(prefixedKey, () => { - resolve({success: 1}); - }); - }); + if (typeof key !== "string" || !key.length) { + return console.error("deleteValue missing key arg"); + } + return new Promise((resolve) => { + const prefixedKey = `${this.US_filename}---${key}`; + browser.storage.local.remove(prefixedKey, () => { + resolve({ success: 1 }); + }); + }); } function listValues() { - return new Promise(resolve => { - const prefix = `${this.US_filename}---`; - const keys = []; - browser.storage.local.get().then(items => { - for (const key in items) { - if (key.startsWith(prefix)) { - const k = key.replace(prefix, ""); - keys.push(k); - } - } - resolve(keys); - }); - }); + return new Promise((resolve) => { + const prefix = `${this.US_filename}---`; + const keys = []; + browser.storage.local.get().then((items) => { + for (const key in items) { + if (key.startsWith(prefix)) { + const k = key.replace(prefix, ""); + keys.push(k); + } + } + resolve(keys); + }); + }); } function openInTab(url, openInBackground = false) { - if (!url) return console.error("openInTab missing url arg"); - return new Promise(resolve => { - const message = { - name: "API_OPEN_TAB", - url, - active: !openInBackground - }; - browser.runtime.sendMessage(message, response => resolve(response)); - }); + if (!url) return console.error("openInTab missing url arg"); + return new Promise((resolve) => { + const message = { + name: "API_OPEN_TAB", + url, + active: !openInBackground, + }; + browser.runtime.sendMessage(message, (response) => resolve(response)); + }); } function getTab() { - return new Promise(resolve => { - const message = {name: "API_GET_TAB"}; - browser.runtime.sendMessage(message, response => { - resolve(response); - }); - }); + return new Promise((resolve) => { + const message = { name: "API_GET_TAB" }; + browser.runtime.sendMessage(message, (response) => { + resolve(response); + }); + }); } function saveTab(tab) { - if (tab == null) return console.error("saveTab invalid arg"); - return new Promise(resolve => { - const message = { - name: "API_SAVE_TAB", - tab - }; - browser.runtime.sendMessage(message, response => { - resolve(response); - }); - }); + if (tab == null) return console.error("saveTab invalid arg"); + return new Promise((resolve) => { + const message = { + name: "API_SAVE_TAB", + tab, + }; + browser.runtime.sendMessage(message, (response) => { + resolve(response); + }); + }); } function closeTab(tabId) { - return new Promise(resolve => { - const message = { - name: "API_CLOSE_TAB", - tabId - }; - browser.runtime.sendMessage(message, response => resolve(response)); - }); + return new Promise((resolve) => { + const message = { + name: "API_CLOSE_TAB", + tabId, + }; + browser.runtime.sendMessage(message, (response) => resolve(response)); + }); } function addStyle(css) { - if (typeof css !== "string") { - return console.error("addStyle invalid css arg"); - } - return new Promise(resolve => { - const message = { - name: "API_ADD_STYLE", - css - }; - browser.runtime.sendMessage(message, response => resolve(response)); - }); + if (typeof css !== "string") { + return console.error("addStyle invalid css arg"); + } + return new Promise((resolve) => { + const message = { + name: "API_ADD_STYLE", + css, + }; + browser.runtime.sendMessage(message, (response) => resolve(response)); + }); } function setClipboard(clipboardData, type) { - return new Promise(resolve => { - const message = { - name: "API_SET_CLIPBOARD", - clipboardData, - type - }; - browser.runtime.sendMessage(message, response => { - resolve(response); - }); - }); + return new Promise((resolve) => { + const message = { + name: "API_SET_CLIPBOARD", + clipboardData, + type, + }; + browser.runtime.sendMessage(message, (response) => { + resolve(response); + }); + }); } function xhr(details) { - if (details == null) return console.error("xhr invalid details arg"); - if (!details.url) return console.error("xhr details missing url key"); - // generate random port name for single xhr - const xhrPortName = Math.random().toString(36).substring(1, 9); - // strip out functions from details - const detailsParsed = JSON.parse(JSON.stringify(details)); - // get all the "on" events from XMLHttpRequest object - const events = []; - for (const k in XMLHttpRequest.prototype) { - if (k.slice(0, 2) === "on") events.push(k); - } - // check which functions are included in the original details object - // add a bool to indicate if event listeners should be attached - for (const e of events) { - if (typeof details[e] === "function") detailsParsed[e] = true; - } - // define return method, will be populated after port is established - const response = { - abort: () => console.error("xhr has not yet been initialized") - }; - // port listener, most of the messaging logic goes here - const listener = port => { - if (port.name !== xhrPortName) return; - port.onMessage.addListener(async msg => { - if ( - events.includes(msg.name) - && typeof details[msg.name] === "function" - ) { - // process xhr response - const r = msg.response; - // only process when xhr is complete and data exist - if (r.readyState === 4 && r.response !== null) { - if (r.responseType === "arraybuffer") { - // arraybuffer responses had their data converted in background - // convert it back to arraybuffer - try { - const buffer = new Uint8Array(r.response).buffer; - r.response = buffer; - } catch (err) { - console.error("error parsing xhr arraybuffer", err); - } - } else if (r.responseType === "blob" && r.response.data) { - // blob responses had their data converted in background - // convert it back to blob - const resp = await fetch(r.response.data); - const b = await resp.blob(); - r.response = b; - } - } - // call userscript method - details[msg.name](msg.response); - } - // all messages received - // tell background it's safe to close port - if (msg.name === "onloadend") { - port.postMessage({name: "DISCONNECT"}); - } - }); + if (details == null) return console.error("xhr invalid details arg"); + if (!details.url) return console.error("xhr details missing url key"); + // generate random port name for single xhr + const xhrPortName = Math.random().toString(36).substring(1, 9); + // strip out functions from details + const detailsParsed = JSON.parse(JSON.stringify(details)); + // get all the "on" events from XMLHttpRequest object + const events = []; + for (const k in XMLHttpRequest.prototype) { + if (k.slice(0, 2) === "on") events.push(k); + } + // check which functions are included in the original details object + // add a bool to indicate if event listeners should be attached + for (const e of events) { + if (typeof details[e] === "function") detailsParsed[e] = true; + } + // define return method, will be populated after port is established + const response = { + abort: () => console.error("xhr has not yet been initialized"), + }; + // port listener, most of the messaging logic goes here + const listener = (port) => { + if (port.name !== xhrPortName) return; + port.onMessage.addListener(async (msg) => { + if ( + events.includes(msg.name) && + typeof details[msg.name] === "function" + ) { + // process xhr response + const r = msg.response; + // only process when xhr is complete and data exist + if (r.readyState === 4 && r.response !== null) { + if (r.responseType === "arraybuffer") { + // arraybuffer responses had their data converted in background + // convert it back to arraybuffer + try { + const buffer = new Uint8Array(r.response).buffer; + r.response = buffer; + } catch (err) { + console.error("error parsing xhr arraybuffer", err); + } + } else if (r.responseType === "blob" && r.response.data) { + // blob responses had their data converted in background + // convert it back to blob + const resp = await fetch(r.response.data); + const b = await resp.blob(); + r.response = b; + } + } + // call userscript method + details[msg.name](msg.response); + } + // all messages received + // tell background it's safe to close port + if (msg.name === "onloadend") { + port.postMessage({ name: "DISCONNECT" }); + } + }); - // handle port disconnect and clean tasks - port.onDisconnect.addListener(p => { - if (p?.error) { - console.error(`port disconnected due to an error: ${p.error.message}`); - } - browser.runtime.onConnect.removeListener(listener); - }); - // fill the method returned to the user script - response.abort = () => port.postMessage({name: "ABORT"}); - }; - // wait for the background to establish a port connection - browser.runtime.onConnect.addListener(listener); - // pass the basic information to the background through a common message - const message = { - name: "API_XHR", - details: detailsParsed, - xhrPortName, - events - }; - browser.runtime.sendMessage(message); - return response; + // handle port disconnect and clean tasks + port.onDisconnect.addListener((p) => { + if (p?.error) { + console.error(`port disconnected due to an error: ${p.error.message}`); + } + browser.runtime.onConnect.removeListener(listener); + }); + // fill the method returned to the user script + response.abort = () => port.postMessage({ name: "ABORT" }); + }; + // wait for the background to establish a port connection + browser.runtime.onConnect.addListener(listener); + // pass the basic information to the background through a common message + const message = { + name: "API_XHR", + details: detailsParsed, + xhrPortName, + events, + }; + browser.runtime.sendMessage(message); + return response; } export default { - setValue, - getValue, - listValues, - deleteValue, - openInTab, - getTab, - saveTab, - closeTab, - addStyle, - setClipboard, - // notification, - // registerMenuCommand, - // getResourceUrl, - xmlHttpRequest: xhr, - GM_xmlhttpRequest: xhr + setValue, + getValue, + listValues, + deleteValue, + openInTab, + getTab, + saveTab, + closeTab, + addStyle, + setClipboard, + // notification, + // registerMenuCommand, + // getResourceUrl, + xmlHttpRequest: xhr, + GM_xmlhttpRequest: xhr, }; diff --git a/src/ext/content-scripts/main.js b/src/ext/content-scripts/main.js index 6d23a254..9c7dabef 100644 --- a/src/ext/content-scripts/main.js +++ b/src/ext/content-scripts/main.js @@ -11,223 +11,246 @@ const label = randomLabel(); const usTag = window.self === window.top ? "" : `(${label})`; function randomLabel() { - const a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", r = Math.random(); - return a[Math.floor(r * a.length)] + r.toString().slice(5, 6); + const a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const r = Math.random(); + return a[Math.floor(r * a.length)] + r.toString().slice(5, 6); } function triageJS(userscript) { - const runAt = userscript.scriptObject["run-at"]; - if (runAt === "document-start") { - injectJS(userscript); - } else if (runAt === "document-end") { - if (document.readyState !== "loading") { - injectJS(userscript); - } else { - document.addEventListener("DOMContentLoaded", () => { - injectJS(userscript); - }); - } - } else if (runAt === "document-idle") { - if (document.readyState === "complete") { - injectJS(userscript); - } else { - document.addEventListener("readystatechange", () => { - if (document.readyState === "complete") { - injectJS(userscript); - } - }); - } - } + const runAt = userscript.scriptObject["run-at"]; + if (runAt === "document-start") { + injectJS(userscript); + } else if (runAt === "document-end") { + if (document.readyState !== "loading") { + injectJS(userscript); + } else { + document.addEventListener("DOMContentLoaded", () => { + injectJS(userscript); + }); + } + } else if (runAt === "document-idle") { + if (document.readyState === "complete") { + injectJS(userscript); + } else { + document.addEventListener("readystatechange", () => { + if (document.readyState === "complete") { + injectJS(userscript); + } + }); + } + } } function injectJS(userscript) { - const filename = userscript.scriptObject.filename; - const name = userscript.scriptObject.name; - const code = `${userscript.code} //# sourceURL=${filename.replace(/\s/g, "-") + usTag}`; - let injectInto = userscript.scriptObject["inject-into"]; - // change scope to content since strict CSP event detected - if (injectInto === "auto" && (userscript.fallback || cspFallbackAttempted)) { - injectInto = "content"; - console.warn(`Attempting fallback injection for ${name}`); - } else if (window.self === window.top) { - console.info(`Injecting ${name} %c(js)`, "color: #fff600"); - } else { - console.info(`Injecting ${name} %c(js)%c - %cframe(${label})(${window.location})`, "color: #fff600", "color: inherit", "color: #006fff"); - } - if (injectInto !== "content") { - const div = document.createElement("div"); - div.style.display = "none"; - const shadowRoot = div.attachShadow({mode: "closed"}); - document.body.append(div); - const tag = document.createElement("script"); - tag.textContent = code; - shadowRoot.append(tag); - } else { - try { - // eslint-disable-next-line no-new-func - return Function(`{${Object.keys(userscript.apis).join(",")}}`, code)(userscript.apis); - } catch (error) { - console.error(`"${filename}" error:`, error); - } - } + const filename = userscript.scriptObject.filename; + const name = userscript.scriptObject.name; + const code = `${userscript.code} //# sourceURL=${ + filename.replace(/\s/g, "-") + usTag + }`; + let injectInto = userscript.scriptObject["inject-into"]; + // change scope to content since strict CSP event detected + if (injectInto === "auto" && (userscript.fallback || cspFallbackAttempted)) { + injectInto = "content"; + console.warn(`Attempting fallback injection for ${name}`); + } else if (window.self === window.top) { + console.info(`Injecting ${name} %c(js)`, "color: #fff600"); + } else { + console.info( + `Injecting ${name} %c(js)%c - %cframe(${label})(${window.location})`, + "color: #fff600", + "color: inherit", + "color: #006fff", + ); + } + if (injectInto !== "content") { + const div = document.createElement("div"); + div.style.display = "none"; + const shadowRoot = div.attachShadow({ mode: "closed" }); + document.body.append(div); + const tag = document.createElement("script"); + tag.textContent = code; + shadowRoot.append(tag); + } else { + try { + // eslint-disable-next-line no-new-func + return Function( + `{${Object.keys(userscript.apis).join(",")}}`, + code, + )(userscript.apis); + } catch (error) { + console.error(`"${filename}" error:`, error); + } + } } function injectCSS(name, code) { - if (window.self === window.top) { - console.info(`Injecting ${name} %c(css)`, "color: #60f36c"); - } else { - console.info(`Injecting ${name} %c(css)%c - %cframe(${label})(${window.location})`, "color: #60f36c", "color: inherit", "color: #006fff"); - } - // Safari lacks full support for tabs.insertCSS - // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/insertCSS - // specifically frameId and cssOrigin - // if support for those details keys arrives, the method below can be used - // NOTE: manifest V3 does support frameId, but not origin - // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/insertCSS + if (window.self === window.top) { + console.info(`Injecting ${name} %c(css)`, "color: #60f36c"); + } else { + console.info( + `Injecting ${name} %c(css)%c - %cframe(${label})(${window.location})`, + "color: #60f36c", + "color: inherit", + "color: #006fff", + ); + } + // Safari lacks full support for tabs.insertCSS + // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/insertCSS + // specifically frameId and cssOrigin + // if support for those details keys arrives, the method below can be used + // NOTE: manifest V3 does support frameId, but not origin + // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/insertCSS - // write the css code to head of the document - const tag = document.createElement("style"); - tag.textContent = code; - document.head.appendChild(tag); + // write the css code to head of the document + const tag = document.createElement("style"); + tag.textContent = code; + document.head.appendChild(tag); } function cspFallback(e) { - // if a security policy violation event has occurred - // and the directive is script-src or script-src-elem - // it's fair to assume that there is a strict CSP for javascript - // and that injection was blocked for all userscripts - // when any script-src violation is detected, re-attempt injection - if ( - e.effectiveDirective === "script-src" - || e.effectiveDirective === "script-src-elem" - ) { - // get all "auto" code - // since other code can trigger a security policy violation event - // make sure data var is not undefined before attempting fallback - if (!data || cspFallbackAttempted) return; - // update global that tracks security policy violations - cspFallbackAttempted = 1; - // for all userscripts with @inject-into: auto, attempt re-injection - for (let i = 0; i < data.files.js.length; i++) { - const userscript = data.files.js[i]; - if (userscript.scriptObject["inject-into"] !== "auto") continue; - userscript.fallback = 1; - triageJS(userscript); - } - } + // if a security policy violation event has occurred + // and the directive is script-src or script-src-elem + // it's fair to assume that there is a strict CSP for javascript + // and that injection was blocked for all userscripts + // when any script-src violation is detected, re-attempt injection + if ( + e.effectiveDirective === "script-src" || + e.effectiveDirective === "script-src-elem" + ) { + // get all "auto" code + // since other code can trigger a security policy violation event + // make sure data var is not undefined before attempting fallback + if (!data || cspFallbackAttempted) return; + // update global that tracks security policy violations + cspFallbackAttempted = 1; + // for all userscripts with @inject-into: auto, attempt re-injection + for (let i = 0; i < data.files.js.length; i++) { + const userscript = data.files.js[i]; + if (userscript.scriptObject["inject-into"] !== "auto") continue; + userscript.fallback = 1; + triageJS(userscript); + } + } } function injection() { - browser.runtime.sendMessage({name: "REQ_USERSCRIPTS"}, response => { - // cancel injection if errors detected - if (!response || response.error) { - console.error(response?.error || "REQ_USERSCRIPTS returned undefined"); - return; - } - // save response locally in case CSP events occur - data = response; - // combine regular and context-menu scripts - const scripts = [...data.files.js, ...data.files.menu]; - // loop through each userscript and prepare for processing - for (let i = 0; i < scripts.length; i++) { - const userscript = scripts[i]; - const filename = userscript.scriptObject.filename; - const grants = userscript.scriptObject.grant; - const injectInto = userscript.scriptObject["inject-into"]; - // create GM.info object, all userscripts get access to GM.info - userscript.apis = {GM: {}}; - userscript.apis.GM.info = { - script: userscript.scriptObject, - scriptHandler: data.scriptHandler, - scriptHandlerVersion: data.scriptHandlerVersion, - scriptMetaStr: userscript.scriptMetaStr - }; - // add GM_info - userscript.apis.GM_info = userscript.apis.GM.info; - // if @grant explicitly set to none, empty grants array - if (grants.includes("none")) grants.length = 0; - // @grant values exist for page scoped userscript - if (grants.length && injectInto === "page") { - // remove grants - grants.length = 0; - // log warning - console.warn(`${filename} @grant values removed due to @inject-into value: ${injectInto} - https://github.com/quoid/userscripts/issues/265#issuecomment-1213462394`); - } - // @grant exist for auto scoped userscript - if (grants.length && injectInto === "auto") { - // change scope - userscript.scriptObject["inject-into"] = "content"; - // log warning - console.warn(`${filename} @inject-into value set to 'content' due to @grant values: ${grants} - https://github.com/quoid/userscripts/issues/265#issuecomment-1213462394`); - } - // loop through each userscript @grant value, add methods as needed - for (let j = 0; j < grants.length; j++) { - const grant = grants[j]; - const method = grant.split(".")[1] || grant.split(".")[0]; - // ensure API method exists in apis object - if (!Object.keys(apis).includes(method)) continue; - // add granted methods - switch (method) { - case "info": - case "GM_info": - continue; - case "getValue": - case "setValue": - case "deleteValue": - case "listValues": - userscript.apis.GM[method] = apis[method].bind({US_filename: filename}); - break; - case "GM_xmlhttpRequest": - userscript.apis[method] = apis[method]; - break; - default: - userscript.apis.GM[method] = apis[method]; - } - } - // triage userjs item for injection - triageJS(userscript); - } - // loop through each usercss and inject - for (let i = 0; i < data.files.css.length; i++) { - const userstyle = data.files.css[i]; - injectCSS(userstyle.name, userstyle.code); - } - }); + browser.runtime.sendMessage({ name: "REQ_USERSCRIPTS" }, (response) => { + // cancel injection if errors detected + if (!response || response.error) { + console.error(response?.error || "REQ_USERSCRIPTS returned undefined"); + return; + } + // save response locally in case CSP events occur + data = response; + // combine regular and context-menu scripts + const scripts = [...data.files.js, ...data.files.menu]; + // loop through each userscript and prepare for processing + for (let i = 0; i < scripts.length; i++) { + const userscript = scripts[i]; + const filename = userscript.scriptObject.filename; + const grants = userscript.scriptObject.grant; + const injectInto = userscript.scriptObject["inject-into"]; + // create GM.info object, all userscripts get access to GM.info + userscript.apis = { GM: {} }; + userscript.apis.GM.info = { + script: userscript.scriptObject, + scriptHandler: data.scriptHandler, + scriptHandlerVersion: data.scriptHandlerVersion, + scriptMetaStr: userscript.scriptMetaStr, + }; + // add GM_info + userscript.apis.GM_info = userscript.apis.GM.info; + // if @grant explicitly set to none, empty grants array + if (grants.includes("none")) grants.length = 0; + // @grant values exist for page scoped userscript + if (grants.length && injectInto === "page") { + // remove grants + grants.length = 0; + // log warning + console.warn( + `${filename} @grant values removed due to @inject-into value: ${injectInto} - https://github.com/quoid/userscripts/issues/265#issuecomment-1213462394`, + ); + } + // @grant exist for auto scoped userscript + if (grants.length && injectInto === "auto") { + // change scope + userscript.scriptObject["inject-into"] = "content"; + // log warning + console.warn( + `${filename} @inject-into value set to 'content' due to @grant values: ${grants} - https://github.com/quoid/userscripts/issues/265#issuecomment-1213462394`, + ); + } + // loop through each userscript @grant value, add methods as needed + for (let j = 0; j < grants.length; j++) { + const grant = grants[j]; + const method = grant.split(".")[1] || grant.split(".")[0]; + // ensure API method exists in apis object + if (!Object.keys(apis).includes(method)) continue; + // add granted methods + switch (method) { + case "info": + case "GM_info": + continue; + case "getValue": + case "setValue": + case "deleteValue": + case "listValues": + userscript.apis.GM[method] = apis[method].bind({ + US_filename: filename, + }); + break; + case "GM_xmlhttpRequest": + userscript.apis[method] = apis[method]; + break; + default: + userscript.apis.GM[method] = apis[method]; + } + } + // triage userjs item for injection + triageJS(userscript); + } + // loop through each usercss and inject + for (let i = 0; i < data.files.css.length; i++) { + const userstyle = data.files.css[i]; + injectCSS(userstyle.name, userstyle.code); + } + }); } function listeners() { - // listens for messages from background, popup, etc... - browser.runtime.onMessage.addListener(request => { - const name = request.name; - if (name === "CONTEXT_RUN") { - // from bg script when context-menu item is clicked - // double check to ensure context-menu scripts only run in top windows - if (window !== window.top) return; + // listens for messages from background, popup, etc... + browser.runtime.onMessage.addListener((request) => { + const name = request.name; + if (name === "CONTEXT_RUN") { + // from bg script when context-menu item is clicked + // double check to ensure context-menu scripts only run in top windows + if (window !== window.top) return; - // loop through context-menu scripts saved to data object and find match - // if no match found, nothing will execute and error will log - const filename = request.menuItemId; - for (let i = 0; i < data.files.menu.length; i++) { - const item = data.files.menu[i]; - if (item.scriptObject.filename === filename) { - console.info(`Injecting ${filename} %c(js)`, "color: #fff600"); - injectJS(item); - return; - } - } - console.error(`Couldn't find ${filename} code!`); - } - }); - // listen for CSP violations - document.addEventListener("securitypolicyviolation", cspFallback); + // loop through context-menu scripts saved to data object and find match + // if no match found, nothing will execute and error will log + const filename = request.menuItemId; + for (let i = 0; i < data.files.menu.length; i++) { + const item = data.files.menu[i]; + if (item.scriptObject.filename === filename) { + console.info(`Injecting ${filename} %c(js)`, "color: #fff600"); + injectJS(item); + return; + } + } + console.error(`Couldn't find ${filename} code!`); + } + }); + // listen for CSP violations + document.addEventListener("securitypolicyviolation", cspFallback); } async function initialize() { - const results = await browser.storage.local.get("US_GLOBAL_ACTIVE"); - if (results?.US_GLOBAL_ACTIVE === false) return console.info("Userscripts off"); - // start the injection process and add the listeners - injection(); - listeners(); + const results = await browser.storage.local.get("US_GLOBAL_ACTIVE"); + if (results?.US_GLOBAL_ACTIVE === false) + return console.info("Userscripts off"); + // start the injection process and add the listeners + injection(); + listeners(); } initialize(); diff --git a/src/ext/extension-page/App.svelte b/src/ext/extension-page/App.svelte index 62e46a66..59856083 100644 --- a/src/ext/extension-page/App.svelte +++ b/src/ext/extension-page/App.svelte @@ -1,133 +1,135 @@ - + {#if $state.includes("init")} -
- {@html logo} - {#if $state.includes("init-error")} - Failed to initialize app, check the browser console - {:else} - Initializing app... - {/if} -
+
+ {@html logo} + {#if $state.includes("init-error")} + Failed to initialize app, check the browser console + {:else} + Initializing app... + {/if} +
{/if}
- - + +
    - {#each $notifications as item (item.id)} - notifications.remove(item.id)} {item}/> - {/each} + {#each $notifications as item (item.id)} + notifications.remove(item.id)} {item} /> + {/each}
-{#if $state.includes("settings")}{/if} +{#if $state.includes("settings")}{/if} diff --git a/src/ext/extension-page/Components/Editor/CodeMirror.svelte b/src/ext/extension-page/Components/Editor/CodeMirror.svelte index 849662ef..e0c39081 100644 --- a/src/ext/extension-page/Components/Editor/CodeMirror.svelte +++ b/src/ext/extension-page/Components/Editor/CodeMirror.svelte @@ -1,390 +1,409 @@ + + {#if instance} - searchActive = false} - instance={instance} - /> + (searchActive = false)} + {instance} + /> {/if} diff --git a/src/ext/extension-page/Components/Editor/Editor.svelte b/src/ext/extension-page/Components/Editor/Editor.svelte index e8f1ed49..46163ec6 100644 --- a/src/ext/extension-page/Components/Editor/Editor.svelte +++ b/src/ext/extension-page/Components/Editor/Editor.svelte @@ -1,329 +1,346 @@
- {#if $state.includes("editor-loading") || $state.includes("fetching")} - - {/if} - {#if !activeItem} -
No Item Selected
- {/if} -
-
-
- -
{name}
-
-
-
- {#if $state.includes("saving")} - Saving... - {:else if $state.includes("trashing")} - (◞‸◟) - {:else if $state.includes("updating")} - Updating code, - {:else if remote} - Code was remotely fetched, check carefully before saving! - {:else if temp} - Ready for code! - {:else} - Last modified: {lastModified} - {/if} -
-
-
-
- - - -
-
-
- -
- + {#if $state.includes("editor-loading") || $state.includes("fetching")} + + {/if} + {#if !activeItem} +
No Item Selected
+ {/if} +
+
+
+ +
{name}
+
+
+
+ {#if $state.includes("saving")} + Saving... + {:else if $state.includes("trashing")} + (◞‸◟) + {:else if $state.includes("updating")} + Updating code, + {:else if remote} + Code was remotely fetched, + check carefully before saving! + {:else if temp} + Ready for code! + {:else} + Last modified: {lastModified} + {/if} +
+
+
+
+ + + +
+
+
+ +
+
diff --git a/src/ext/extension-page/Components/Editor/EditorSearch.svelte b/src/ext/extension-page/Components/Editor/EditorSearch.svelte index ff1833cb..cc1f70db 100644 --- a/src/ext/extension-page/Components/Editor/EditorSearch.svelte +++ b/src/ext/extension-page/Components/Editor/EditorSearch.svelte @@ -1,199 +1,207 @@ {#if active} - + {/if} diff --git a/src/ext/extension-page/Components/Notification.svelte b/src/ext/extension-page/Components/Notification.svelte index 9896909b..36ae34ed 100644 --- a/src/ext/extension-page/Components/Notification.svelte +++ b/src/ext/extension-page/Components/Notification.svelte @@ -1,148 +1,153 @@
  • - -
    -
    {@html icon}
    - {item.message} - -
    + +
    +
    {@html icon}
    + {item.message} + +
  • diff --git a/src/ext/extension-page/Components/Settings.svelte b/src/ext/extension-page/Components/Settings.svelte index 40f7f7b3..730e3752 100644 --- a/src/ext/extension-page/Components/Settings.svelte +++ b/src/ext/extension-page/Components/Settings.svelte @@ -1,318 +1,350 @@
    - - -
    state.remove("settings")}>
    - + + +
    state.remove("settings")}>
    +
    diff --git a/src/ext/extension-page/Components/Sidebar/Sidebar.svelte b/src/ext/extension-page/Components/Sidebar/Sidebar.svelte index 4504b4a1..c554c4ea 100644 --- a/src/ext/extension-page/Components/Sidebar/Sidebar.svelte +++ b/src/ext/extension-page/Components/Sidebar/Sidebar.svelte @@ -1,260 +1,266 @@ -
    + Userscripts App Icon + +

    You can turn on the Userscripts iOS Safari extension in Settings. + Read the docs. +

    + +
    CURRENT DIRECTORY:
    +
    init
    +