diff --git a/public/ext/shared/_locales/en/messages.json b/public/ext/shared/_locales/en/messages.json index a39bfc24..c17c6cad 100644 --- a/public/ext/shared/_locales/en/messages.json +++ b/public/ext/shared/_locales/en/messages.json @@ -85,6 +85,24 @@ "settings_global_active_desc": { "message": "Toggle on/off script injection for the pages you visit" }, + "settings_theme_mode": { + "message": "Theme mode" + }, + "settings_theme_mode_desc": { + "message": "Toggle theme mode" + }, + "settings_theme_mode_auto": { + "message": "Auto (System)" + }, + "settings_theme_mode_dark": { + "message": "Dark" + }, + "settings_theme_mode_light": { + "message": "Light" + }, + "settings_theme_mode_custom": { + "message": "Custom" + }, "settings_global_exclude_match": { "message": "Global Exclude Match patterns" }, diff --git a/src/ext/action-popup/App.svelte b/src/ext/action-popup/App.svelte index 5740e7b5..6d593530 100644 --- a/src/ext/action-popup/App.svelte +++ b/src/ext/action-popup/App.svelte @@ -242,6 +242,12 @@ } platform = pltfm.platform; + /* Override theme mode */ + const theme = await settingsStorage.get("theme_mode"); + if (theme && theme !== "auto") { + document.body.style.setProperty("color-scheme", theme); + } + // display native error if there is const errorNative = await settingsStorage.get("error_native"); if (errorNative.error) { diff --git a/src/ext/extension-page/App.svelte b/src/ext/extension-page/App.svelte index 22487e8f..1c4ce4d3 100644 --- a/src/ext/extension-page/App.svelte +++ b/src/ext/extension-page/App.svelte @@ -19,6 +19,15 @@ } }); + /* Override theme mode */ + $: if ($settings["theme_mode"]) { + if ($settings["theme_mode"] === "auto") { + document.body.style.removeProperty("color-scheme"); + } else { + document.body.style.setProperty("color-scheme", $settings["theme_mode"]); + } + } + // disables default cmd+s (save) and cmd+f (find) behavior function preventKeyCommands(e) { if (e.metaKey && (e.code === "KeyS" || e.code === "KeyF")) { diff --git a/src/ext/shared/settings.js b/src/ext/shared/settings.js index 37d83e1d..6154d6bd 100644 --- a/src/ext/shared/settings.js +++ b/src/ext/shared/settings.js @@ -124,6 +124,14 @@ const settingsDefinition = /** @type {const} */ [ group: "general", nodeType: "Toggle", }, + { + name: "theme_mode", + type: "string", + values: ["auto", "dark", "light"], + default: "auto", + group: "general", + nodeType: "select", + }, { name: "global_active", type: "boolean", diff --git a/src/shared/variables.css b/src/shared/variables.css index 225c2ae6..f014eba2 100644 --- a/src/shared/variables.css +++ b/src/shared/variables.css @@ -24,80 +24,52 @@ --editor-matched-highlight: rgb(116 178 235 / 0.2); --editor-search-highlight: rgb(255 166 0 / 0.3); - /* Dark theme (default) */ - --color-bg-primary: #323639; - --color-bg-secondary: #2f3337; - --color-bg-theme: var(--color-black); - --color-script-highlighted: #364049; - --color-black: #1d2023; - --color-white: #fff; - --color-blue: #74b1eb; - --color-green: #60f36c; - --color-grey: rgb(255 255 255 / 0.15); - --color-red: #ff453a; - --color-yellow: #e4f360; - --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); - --box-shadow: 0 0.5rem 1rem 0 rgb(0 0 0 / 0.25); - --button-color: var(--color-black); - --border-color: var(--color-black); + /* Color Theme */ + color-scheme: light dark; + --color-bg-primary: light-dark(#fafafa, #323639); + --color-bg-secondary: light-dark(#f1f1f1, #2f3337); + --color-bg-theme: light-dark(var(--color-white), var(--color-black)); + --color-script-highlighted: light-dark(#e2ebf3, #364049); + --color-black: light-dark(#333, #1d2023); + --color-white: light-dark(#fff, #fff); + --color-blue: light-dark(#317eff, #74b1eb); + --color-green: light-dark(#2bb239, #60f36c); + --color-grey: light-dark(rgb(51 51 51 / 0.15), rgb(255 255 255 / 0.15)); + --color-red: light-dark(#ff453a, #ff453a); + --color-yellow: light-dark(#b8722c, #e4f360); + --text-color-primary: light-dark(rgb(51 51 51), rgb(255 255 255)); + --text-color-secondary: light-dark( + rgb(51 51 51 / 0.65), + rgb(255 255 255 / 0.65) + ); + --text-color-disabled: light-dark( + rgb(51 51 51 / 0.35), + rgb(255 255 255 / 0.4) + ); + --box-shadow: 0 0.5rem 1rem 0 light-dark(rgb(0 0 0 / 0.15), rgb(0 0 0 / 0.25)); + --button-color: light-dark(var(--color-white), var(--color-black)); + --border-color: light-dark(var(--color-grey), var(--color-black)); - /* Editor dark theme colors */ - --editor-invisible: rgb(255 255 255 / 0.15); - --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-cursor: #e3e7eb; - --editor-matching-bracket-color: #fff; - --editor-matching-bracket-border: var(--editor-number); - --editor-blurred-background: rgb(50 54 57 / 0.65); -} - -/* Light theme */ -@media (prefers-color-scheme: light) { - :root { - color-scheme: light dark; - --color-bg-primary: #fafafa; - --color-bg-secondary: #f1f1f1; - --color-bg-theme: var(--color-white); - --color-script-highlighted: #e2ebf3; - --color-black: #333; - --color-white: #fff; - --color-blue: #317eff; - --color-green: #2bb239; - --color-grey: rgb(51 51 51 / 0.15); - --color-red: #ff453a; - --color-yellow: #b8722c; - --text-color-primary: rgb(51 51 51); - --text-color-secondary: rgb(51 51 51 / 0.65); - --text-color-disabled: rgb(51 51 51 / 0.35); - --box-shadow: 0 0.25rem 1rem 0 rgb(0 0 0 / 0.15); - --button-color: var(--color-white); - --border-color: var(--color-grey); - - /* Editor light theme colors */ - --editor-invisible: rgb(51 51 51 / 0.15); - --editor-number: var(--color-green); - --editor-comment: var(--text-color-disabled); - --editor-def: #b86d17; - --editor-default: #777; - --editor-keyword: #3a68ff; - --editor-atom: #00adad; - --editor-operator: #8c99a7; - --editor-property: #ff453a; - --editor-string: var(--editor-atom); - --editor-string-2: #a34fe6; - --editor-cursor: #7e7e7f; - --editor-matching-bracket-color: var(--text-color-primary); - --editor-matching-bracket-border: #ff00ea; - --editor-blurred-background: rgb(255 255 255 / 0.65); - } + /* Editor colors */ + --editor-invisible: light-dark(rgb(51 51 51 / 0.15), rgb(255 255 255 / 0.15)); + --editor-number: light-dark(var(--color-green), #77e26a); + --editor-comment: light-dark( + var(--text-color-disabled), + rgb(255 255 255 / 0.35) + ); + --editor-def: light-dark(#b86d17, #efc371); + --editor-default: light-dark(#777, #cdcfd1); + --editor-keyword: light-dark(#3a68ff, #96c3ed); + --editor-atom: light-dark(#00adad, #59ebf5); + --editor-operator: light-dark(#8c99a7, #8c99a7); + --editor-property: light-dark(#ff453a, #e86c8a); + --editor-string: light-dark(var(--editor-atom), #f5eea2); + --editor-string-2: light-dark(#a34fe6, #cdabff); + --editor-cursor: light-dark(#7e7e7f, #e3e7eb); + --editor-matching-bracket-color: light-dark(var(--text-color-primary), #fff); + --editor-matching-bracket-border: light-dark(#ff00ea, var(--editor-number)); + --editor-blurred-background: light-dark( + rgb(255 255 255 / 0.65), + rgb(50 54 57 / 0.65) + ); } diff --git a/xcode/Ext-Safari/Functions.swift b/xcode/Ext-Safari/Functions.swift index 582aef0b..220a380a 100644 --- a/xcode/Ext-Safari/Functions.swift +++ b/xcode/Ext-Safari/Functions.swift @@ -94,16 +94,20 @@ func isEncoded(_ str: String) -> Bool { return false } +// parser REs - force try b/c pattern is known to be valid regex +let re0 = try! NSRegularExpression(pattern: #"(?:(\/\/ ==UserScript==[ \t]*?\r?\n([\S\s]*?)\r?\n\/\/ ==\/UserScript==)([\S\s]*)|(\/\* ==UserStyle==[ \t]*?\r?\n([\S\s]*?)\r?\n==\/UserStyle== \*\/)([\S\s]*))"#, options: []) +// RE for meta line +let re1 = try! NSRegularExpression(pattern: #"^(?:[ \t]*(?:\/\/)?[ \t]*@)([\w-]+)[ \t]+([^\s]+[^\r\n\t\v\f]*)"#, options: []) +// this pattern checks for specific keys that won't have values +let re2 = try! NSRegularExpression(pattern: #"^(?:[ \t]*(?:\/\/)?[ \t]*@)(noframes)[ \t]*$"#, options: []) + // parser func parse(_ content: String) -> [String: Any]? { // returns structured data from content of file // will fail to parse if metablock or required @name key missing - let pattern = #"(?:(\/\/ ==UserScript==[ \t]*?\r?\n([\S\s]*?)\r?\n\/\/ ==\/UserScript==)([\S\s]*)|(\/\* ==UserStyle==[ \t]*?\r?\n([\S\s]*?)\r?\n==\/UserStyle== \*\/)([\S\s]*))"# - // force try b/c pattern is known to be valid regex - let regex = try! NSRegularExpression(pattern: pattern, options: []) let range = NSRange(location: 0, length: content.utf16.count) // return nil/fail if metablock missing - guard let match = regex.firstMatch(in: content, options: [], range: range) else { + guard let match = re0.firstMatch(in: content, options: [], range: range) else { logger?.debug("\(#function, privacy: .public) - Non matched content: \(content, privacy: .public)") return nil } @@ -138,17 +142,11 @@ func parse(_ content: String) -> [String: Any]? { let metaArray = content[metas].split(whereSeparator: \.isNewline) // loop through metadata lines and populate metadata dictionary for meta in metaArray { - let p = #"^(?:[ \t]*(?:\/\/)?[ \t]*@)([\w-]+)[ \t]+([^\s]+[^\r\n\t\v\f]*)"# - // this pattern checks for specific keys that won't have values - let p2 = #"^(?:[ \t]*(?:\/\/)?[ \t]*@)(noframes)[ \t]*$"# // the individual meta string, ie. // @name File Name let metaString = String(meta).trimmingCharacters(in: .whitespaces) - // force try b/c pattern is known to be valid regex - let re = try! NSRegularExpression(pattern: p, options: []) - let re2 = try! NSRegularExpression(pattern: p2, options: []) let range = NSRange(location: 0, length: metaString.utf16.count) // key lines not properly prefixed & without values will be skipped - if let m = re.firstMatch(in: metaString, options: [], range: range) { + if let m = re1.firstMatch(in: metaString, options: [], range: range) { // force unwrap key & value since matches regex above let key = metaString[Range(m.range(at: 1), in: metaString)!] let value = metaString[Range(m.range(at: 2), in: metaString)!] @@ -1003,6 +1001,9 @@ func stringToRegex(_ stringPattern: String) -> NSRegularExpression? { return regex } +// RE for parse matchPattern +let re3 = try! NSRegularExpression(pattern: #"^(http:|https:|\*:)\/\/((?:\*\.)?(?:[a-z0-9-]+\.)+(?:[a-z0-9]+)|\*\.[a-z]+|\*|[a-z0-9]+)(\/[^\s]*)$"#, options: .caseInsensitive) + func match(_ url: String, _ matchPattern: String) -> Bool { guard let parts = jsLikeURL(url), @@ -1028,10 +1029,8 @@ func match(_ url: String, _ matchPattern: String) -> Bool { if (ptcl != "http:" && ptcl != "https:") { return false } - let partsPattern = #"^(http:|https:|\*:)\/\/((?:\*\.)?(?:[a-z0-9-]+\.)+(?:[a-z0-9]+)|\*\.[a-z]+|\*|[a-z0-9]+)(\/[^\s]*)$"# - let partsPatternReg = try! NSRegularExpression(pattern: partsPattern, options: .caseInsensitive) let range = NSMakeRange(0, matchPattern.utf16.count) - guard let parts = partsPatternReg.firstMatch(in: matchPattern, options: [], range: range) else { + guard let parts = re3.firstMatch(in: matchPattern, options: [], range: range) else { logger?.error("\(#function, privacy: .public) - malformed regex match pattern - \(matchPattern, privacy: .public)") return false }