mirror of
				https://github.com/smartfrigde/armcord.git
				synced 2024-08-14 23:56:58 +00:00 
			
		
		
		
	Add mobile mode
This commit is contained in:
		
							parent
							
								
									1a7af5168d
								
							
						
					
					
						commit
						d6cbbcba7d
					
				
					 15 changed files with 105 additions and 19 deletions
				
			
		
							
								
								
									
										5
									
								
								.idea/.gitignore
									
										
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.idea/.gitignore
									
										
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | # Default ignored files | ||||||
|  | /shelf/ | ||||||
|  | /workspace.xml | ||||||
|  | # Editor-based HTTP Client requests | ||||||
|  | /httpRequests/ | ||||||
							
								
								
									
										12
									
								
								.idea/ArmCord.iml
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.idea/ArmCord.iml
									
										
									
										generated
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <module type="WEB_MODULE" version="4"> | ||||||
|  |   <component name="NewModuleRootManager"> | ||||||
|  |     <content url="file://$MODULE_DIR$"> | ||||||
|  |       <excludeFolder url="file://$MODULE_DIR$/temp" /> | ||||||
|  |       <excludeFolder url="file://$MODULE_DIR$/.tmp" /> | ||||||
|  |       <excludeFolder url="file://$MODULE_DIR$/tmp" /> | ||||||
|  |     </content> | ||||||
|  |     <orderEntry type="inheritedJdk" /> | ||||||
|  |     <orderEntry type="sourceFolder" forTests="false" /> | ||||||
|  |   </component> | ||||||
|  | </module> | ||||||
							
								
								
									
										8
									
								
								.idea/modules.xml
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/modules.xml
									
										
									
										generated
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | ||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="ProjectModuleManager"> | ||||||
|  |     <modules> | ||||||
|  |       <module fileurl="file://$PROJECT_DIR$/.idea/ArmCord.iml" filepath="$PROJECT_DIR$/.idea/ArmCord.iml" /> | ||||||
|  |     </modules> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										6
									
								
								.idea/vcs.xml
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/vcs.xml
									
										
									
										generated
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="VcsDirectoryMappings"> | ||||||
|  |     <mapping directory="$PROJECT_DIR$" vcs="Git" /> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
|  | @ -18,6 +18,7 @@ | ||||||
|     "settings-theme-native": "Native", |     "settings-theme-native": "Native", | ||||||
|     "settings-tray": "Minimize to tray", |     "settings-tray": "Minimize to tray", | ||||||
|     "settings-patches": "Automatic Patches", |     "settings-patches": "Automatic Patches", | ||||||
|  |     "settings-mobileMode": "Mobile mode", | ||||||
|     "settings-channel": "Discord channel:", |     "settings-channel": "Discord channel:", | ||||||
|     "settings-invitewebsocket": "Invite Websocket", |     "settings-invitewebsocket": "Invite Websocket", | ||||||
|     "settings-mod": "Client mod:", |     "settings-mod": "Client mod:", | ||||||
|  |  | ||||||
							
								
								
									
										6
									
								
								src/content/css/mobile.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/content/css/mobile.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | [aria-label~="Mute"] { | ||||||
|  |     display: none; | ||||||
|  | } | ||||||
|  | [aria-label~="Deafen"] { | ||||||
|  |     display: none; | ||||||
|  | } | ||||||
|  | @ -31,7 +31,6 @@ | ||||||
|                             <option value="stable">Stable</option> |                             <option value="stable">Stable</option> | ||||||
|                             <option value="canary">Canary</option> |                             <option value="canary">Canary</option> | ||||||
|                             <option value="ptb">PTB</option> |                             <option value="ptb">PTB</option> | ||||||
|                             <option value="foss">Fosscord</option> |  | ||||||
|                         </select> |                         </select> | ||||||
|                     </div> |                     </div> | ||||||
|                     <p class="text-center setup-ask" id="setup_question3"> |                     <p class="text-center setup-ask" id="setup_question3"> | ||||||
|  | @ -121,6 +120,7 @@ | ||||||
|                     automaticPatches: false, |                     automaticPatches: false, | ||||||
|                     mods: "cumcord", |                     mods: "cumcord", | ||||||
|                     inviteWebsocket: true, |                     inviteWebsocket: true, | ||||||
|  |                     mobileMode: false, | ||||||
|                     trayIcon: "ac_plug_colored", |                     trayIcon: "ac_plug_colored", | ||||||
|                     performanceMode: "none" |                     performanceMode: "none" | ||||||
|                 }); |                 }); | ||||||
|  | @ -148,8 +148,8 @@ | ||||||
|                             windowStyle: "default", |                             windowStyle: "default", | ||||||
|                             channel: options.channel, |                             channel: options.channel, | ||||||
|                             armcordCSP: true, |                             armcordCSP: true, | ||||||
|                             autoLaunch: true, |  | ||||||
|                             minimizeToTray: true, |                             minimizeToTray: true, | ||||||
|  |                             mobileMode: false, | ||||||
|                             automaticPatches: false, |                             automaticPatches: false, | ||||||
|                             performanceMode: "none", |                             performanceMode: "none", | ||||||
|                             trayIcon: "ac_plug_colored", |                             trayIcon: "ac_plug_colored", | ||||||
|  | @ -165,7 +165,7 @@ | ||||||
|                         armcordCSP: true, |                         armcordCSP: true, | ||||||
|                         minimizeToTray: true, |                         minimizeToTray: true, | ||||||
|                         automaticPatches: false, |                         automaticPatches: false, | ||||||
|                         autoLaunch: true, |                         mobileMode: false, | ||||||
|                         mods: "none", |                         mods: "none", | ||||||
|                         performanceMode: "none", |                         performanceMode: "none", | ||||||
|                         trayIcon: "ac_plug_colored", |                         trayIcon: "ac_plug_colored", | ||||||
|  |  | ||||||
|  | @ -58,9 +58,6 @@ | ||||||
|                         case "ptb": |                         case "ptb": | ||||||
|                             window.location.replace("https://ptb.discord.com/app"); |                             window.location.replace("https://ptb.discord.com/app"); | ||||||
|                             break; |                             break; | ||||||
|                         case "foss": |  | ||||||
|                             window.location.replace("https://dev.fosscord.com/app"); |  | ||||||
|                             break; |  | ||||||
|                         case undefined: |                         case undefined: | ||||||
|                             window.location.replace("https://discord.com/app"); |                             window.location.replace("https://discord.com/app"); | ||||||
|                             break; |                             break; | ||||||
|  |  | ||||||
|  | @ -83,6 +83,9 @@ export function registerIpc() { | ||||||
|     ipcMain.on("titlebar", (event, arg) => { |     ipcMain.on("titlebar", (event, arg) => { | ||||||
|         event.returnValue = customTitlebar; |         event.returnValue = customTitlebar; | ||||||
|     }); |     }); | ||||||
|  |     ipcMain.on("mobileMode", async (event, arg) => { | ||||||
|  |         event.returnValue = await getConfig("mobileMode"); | ||||||
|  |     }); | ||||||
|     ipcMain.on("shouldPatch", async (event, arg) => { |     ipcMain.on("shouldPatch", async (event, arg) => { | ||||||
|         event.returnValue = await getConfig("automaticPatches"); |         event.returnValue = await getConfig("automaticPatches"); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
							
								
								
									
										19
									
								
								src/preload/mobile.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/preload/mobile.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | ||||||
|  | import {ipcRenderer} from "electron"; | ||||||
|  | import {addStyle} from "../utils"; | ||||||
|  | import * as fs from "fs"; | ||||||
|  | import * as path from "path"; | ||||||
|  | export function injectMobileStuff() { | ||||||
|  |     document.addEventListener("DOMContentLoaded", function (event) { | ||||||
|  |         const mobileCSS = path.join(__dirname, "../", "/content/css/mobile.css"); | ||||||
|  |         addStyle(fs.readFileSync(mobileCSS, "utf8")); | ||||||
|  | 
 | ||||||
|  |         var logo = document.getElementById("window-title"); | ||||||
|  |         logo!.addEventListener("click", () => { | ||||||
|  |             if (ipcRenderer.sendSync("minimizeToTray") === true) { | ||||||
|  |                 ipcRenderer.send("win-hide"); | ||||||
|  |             } else if (ipcRenderer.sendSync("minimizeToTray") === false) { | ||||||
|  |                 ipcRenderer.send("win-quit"); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | @ -6,6 +6,7 @@ import * as path from "path"; | ||||||
| import {injectTitlebar} from "./titlebar"; | import {injectTitlebar} from "./titlebar"; | ||||||
| import {sleep, addStyle, injectJS, addScript} from "../utils"; | import {sleep, addStyle, injectJS, addScript} from "../utils"; | ||||||
| import {ipcRenderer} from "electron"; | import {ipcRenderer} from "electron"; | ||||||
|  | import {injectMobileStuff} from "./mobile"; | ||||||
| var version = ipcRenderer.sendSync("get-app-version", "app-version"); | var version = ipcRenderer.sendSync("get-app-version", "app-version"); | ||||||
| async function updateLang() { | async function updateLang() { | ||||||
|     if (window.location.href.indexOf("setup.html") > -1) { |     if (window.location.href.indexOf("setup.html") > -1) { | ||||||
|  | @ -37,6 +38,9 @@ if (window.location.href.indexOf("splash.html") > -1) { | ||||||
|     if (ipcRenderer.sendSync("titlebar")) { |     if (ipcRenderer.sendSync("titlebar")) { | ||||||
|         injectTitlebar(); |         injectTitlebar(); | ||||||
|     } |     } | ||||||
|  |     if (ipcRenderer.sendSync("mobileMode")) { | ||||||
|  |         injectMobileStuff(); | ||||||
|  |     } | ||||||
|     sleep(5000).then(async () => { |     sleep(5000).then(async () => { | ||||||
|         const cssPath = path.join(__dirname, "../", "/content/css/discord.css"); |         const cssPath = path.join(__dirname, "../", "/content/css/discord.css"); | ||||||
|         addStyle(fs.readFileSync(cssPath, "utf8")); |         addStyle(fs.readFileSync(cssPath, "utf8")); | ||||||
|  |  | ||||||
|  | @ -40,12 +40,17 @@ | ||||||
|             <input class="tgl tgl-light left" id="websocket" type="checkbox" /> |             <input class="tgl tgl-light left" id="websocket" type="checkbox" /> | ||||||
|             <label class="tgl-btn left" for="websocket"></label> |             <label class="tgl-btn left" for="websocket"></label> | ||||||
|         </div> |         </div> | ||||||
|  |         <br /> | ||||||
|  |         <div class="switch"> | ||||||
|  |             <label class="header" id="settings-mobileMode">Mobile mode</label> | ||||||
|  |             <input class="tgl tgl-light left" id="mobile" type="checkbox" /> | ||||||
|  |             <label class="tgl-btn left" for="mobile"></label> | ||||||
|  |         </div> | ||||||
|         <div class="switch"> |         <div class="switch"> | ||||||
|             <select name="channel" id="channel" class="left"> |             <select name="channel" id="channel" class="left"> | ||||||
|                 <option value="stable">Stable</option> |                 <option value="stable">Stable</option> | ||||||
|                 <option value="canary">Canary</option> |                 <option value="canary">Canary</option> | ||||||
|                 <option value="ptb">PTB</option> |                 <option value="ptb">PTB</option> | ||||||
|                 <option value="foss">Fosscord</option> |  | ||||||
|             </select> |             </select> | ||||||
|             <p class="header" id="settings-channel">Discord channel:</p> |             <p class="header" id="settings-channel">Discord channel:</p> | ||||||
|         </div> |         </div> | ||||||
|  | @ -89,6 +94,7 @@ | ||||||
|             ); |             ); | ||||||
|             document.getElementById("settings-patches").innerHTML = await settings.getLang("settings-patches"); |             document.getElementById("settings-patches").innerHTML = await settings.getLang("settings-patches"); | ||||||
|             document.getElementById("settings-tray").innerHTML = await settings.getLang("settings-tray"); |             document.getElementById("settings-tray").innerHTML = await settings.getLang("settings-tray"); | ||||||
|  |             document.getElementById("settings-mobileMode").innerHTML = await settings.getLang("settings-mobileMode"); | ||||||
|             document.getElementById("settings-theme").innerHTML = await settings.getLang("settings-theme"); |             document.getElementById("settings-theme").innerHTML = await settings.getLang("settings-theme"); | ||||||
|             //select stuff |             //select stuff | ||||||
|             document.getElementById("mod").options[3].text = await settings.getLang("settings-none"); |             document.getElementById("mod").options[3].text = await settings.getLang("settings-none"); | ||||||
|  | @ -107,6 +113,7 @@ | ||||||
|             document.getElementById("csp").checked = await settings.get("armcordCSP"); |             document.getElementById("csp").checked = await settings.get("armcordCSP"); | ||||||
|             document.getElementById("tray").checked = await settings.get("minimizeToTray"); |             document.getElementById("tray").checked = await settings.get("minimizeToTray"); | ||||||
|             document.getElementById("websocket").checked = await settings.get("inviteWebsocket"); |             document.getElementById("websocket").checked = await settings.get("inviteWebsocket"); | ||||||
|  |             document.getElementById("mobile").checked = await settings.get("mobileMode"); | ||||||
|             document.getElementById("patches").value = await settings.get("automaticPatches"); |             document.getElementById("patches").value = await settings.get("automaticPatches"); | ||||||
|             document.getElementById("mod").value = await settings.get("mods"); |             document.getElementById("mod").value = await settings.get("mods"); | ||||||
|             document.getElementById("channel").value = await settings.get("channel"); |             document.getElementById("channel").value = await settings.get("channel"); | ||||||
|  | @ -123,6 +130,7 @@ | ||||||
|                 minimizeToTray: document.getElementById("tray").checked, |                 minimizeToTray: document.getElementById("tray").checked, | ||||||
|                 automaticPatches: document.getElementById("patches").checked, |                 automaticPatches: document.getElementById("patches").checked, | ||||||
|                 mods: document.getElementById("mod").value, |                 mods: document.getElementById("mod").value, | ||||||
|  |                 mobileMode: document.getElementById("mobile").checked, | ||||||
|                 inviteWebsocket: document.getElementById("websocket").checked, |                 inviteWebsocket: document.getElementById("websocket").checked, | ||||||
|                 performanceMode: document.getElementById("prfmMode").value, |                 performanceMode: document.getElementById("prfmMode").value, | ||||||
|                 trayIcon: document.getElementById("trayIcon").value, |                 trayIcon: document.getElementById("trayIcon").value, | ||||||
|  |  | ||||||
							
								
								
									
										12
									
								
								src/tray.ts
									
										
									
									
									
								
							
							
						
						
									
										12
									
								
								src/tray.ts
									
										
									
									
									
								
							|  | @ -1,5 +1,5 @@ | ||||||
| import * as fs from "fs"; | import * as fs from "fs"; | ||||||
| import { app, Menu, Tray } from "electron"; | import { app, Menu, Tray, nativeImage} from "electron"; | ||||||
| import { mainWindow } from "./window"; | import { mainWindow } from "./window"; | ||||||
| import { getConfig, getConfigLocation, setWindowState } from "./utils"; | import { getConfig, getConfigLocation, setWindowState } from "./utils"; | ||||||
| import * as path from "path"; | import * as path from "path"; | ||||||
|  | @ -7,10 +7,13 @@ import { createSettingsWindow } from "./settings/main"; | ||||||
| let tray: any = null; | let tray: any = null; | ||||||
| app.whenReady().then(async () => { | app.whenReady().then(async () => { | ||||||
|     let finishedSetup = (await getConfig("doneSetup")); |     let finishedSetup = (await getConfig("doneSetup")); | ||||||
|  |     var trayIcon = (await getConfig("trayIcon")) ?? "ac_plug_colored"; | ||||||
|  |     let trayPath = nativeImage.createFromPath(path.join(__dirname, "../", `/assets/${trayIcon}.png`)); | ||||||
|  |     if(process.platform === "darwin" && trayPath.getSize().height > 22) | ||||||
|  |         trayPath = trayIcon.resize({height: 22}); | ||||||
|     if ((await getConfig("windowStyle")) == "basic") { |     if ((await getConfig("windowStyle")) == "basic") { | ||||||
|         var clientName = (await getConfig("clientName")) ?? "ArmCord"; |         var clientName = (await getConfig("clientName")) ?? "ArmCord"; | ||||||
|         var trayIcon = (await getConfig("trayIcon")) ?? "ac_plug_colored"; |         tray = new Tray(trayPath); | ||||||
|         tray = new Tray(path.join(__dirname, "../", `/assets/${trayIcon}.png`)); |  | ||||||
|         const contextMenu = function () { |         const contextMenu = function () { | ||||||
|             if (finishedSetup == false) { |             if (finishedSetup == false) { | ||||||
|                 return Menu.buildFromTemplate([ |                 return Menu.buildFromTemplate([ | ||||||
|  | @ -57,8 +60,7 @@ app.whenReady().then(async () => { | ||||||
|         tray.setContextMenu(contextMenu); |         tray.setContextMenu(contextMenu); | ||||||
|     } else { |     } else { | ||||||
|         var clientName = (await getConfig("clientName")) ?? "ArmCord"; |         var clientName = (await getConfig("clientName")) ?? "ArmCord"; | ||||||
|         var trayIcon = (await getConfig("trayIcon")) ?? "ac_plug_colored"; |         tray = new Tray(trayPath); | ||||||
|         tray = new Tray(path.join(__dirname, "../", `/assets/${trayIcon}.png`)); |  | ||||||
|         if (finishedSetup == false) { |         if (finishedSetup == false) { | ||||||
|             const contextMenu = Menu.buildFromTemplate([ |             const contextMenu = Menu.buildFromTemplate([ | ||||||
|                 { |                 { | ||||||
|  |  | ||||||
							
								
								
									
										15
									
								
								src/utils.ts
									
										
									
									
									
								
							
							
						
						
									
										15
									
								
								src/utils.ts
									
										
									
									
									
								
							|  | @ -4,7 +4,7 @@ import path from "path"; | ||||||
| export var firstRun: boolean; | export var firstRun: boolean; | ||||||
| export var isSetup: boolean; | export var isSetup: boolean; | ||||||
| export var contentPath: string; | export var contentPath: string; | ||||||
| //utillity functions that are used all over the codebase or just too obscure to be put in the file used in
 | //utility functions that are used all over the codebase or just too obscure to be put in the file used in
 | ||||||
| export function addStyle(styleString: string) { | export function addStyle(styleString: string) { | ||||||
|     const style = document.createElement("style"); |     const style = document.createElement("style"); | ||||||
|     style.textContent = styleString; |     style.textContent = styleString; | ||||||
|  | @ -43,6 +43,7 @@ export function setup() { | ||||||
|         mods: "cumcord", |         mods: "cumcord", | ||||||
|         performanceMode: "none", |         performanceMode: "none", | ||||||
|         inviteWebsocket: true, |         inviteWebsocket: true, | ||||||
|  |         mobileMode: false, | ||||||
|         trayIcon: "ac_plug_colored", |         trayIcon: "ac_plug_colored", | ||||||
|         doneSetup: false |         doneSetup: false | ||||||
|     }; |     }; | ||||||
|  | @ -140,7 +141,16 @@ export async function getLang(object: string) { | ||||||
|     } |     } | ||||||
|     let rawdata = fs.readFileSync(langPath, "utf-8"); |     let rawdata = fs.readFileSync(langPath, "utf-8"); | ||||||
|     let parsed = JSON.parse(rawdata); |     let parsed = JSON.parse(rawdata); | ||||||
|     return parsed[object]; |     if (parsed[object] == undefined) { | ||||||
|  |         console.log(object + " is undefined in " + language) | ||||||
|  |         langPath = path.join(__dirname, "../", "/assets/lang/en-US.json"); | ||||||
|  |         rawdata = fs.readFileSync(langPath, "utf-8"); | ||||||
|  |         parsed = JSON.parse(rawdata); | ||||||
|  |         return parsed[object] | ||||||
|  |     } else { | ||||||
|  |         return parsed[object]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| //ArmCord Window State manager
 | //ArmCord Window State manager
 | ||||||
|  | @ -178,6 +188,7 @@ export interface Settings { | ||||||
|     minimizeToTray: boolean; |     minimizeToTray: boolean; | ||||||
|     automaticPatches: boolean; |     automaticPatches: boolean; | ||||||
|     mods: string; |     mods: string; | ||||||
|  |     mobileMode: boolean, | ||||||
|     performanceMode: string; |     performanceMode: string; | ||||||
|     inviteWebsocket: boolean; |     inviteWebsocket: boolean; | ||||||
|     trayIcon: string; |     trayIcon: string; | ||||||
|  |  | ||||||
|  | @ -25,12 +25,16 @@ async function doAfterDefiningTheWindow() { | ||||||
|     var ignoreProtocolWarning = await getConfig("ignoreProtocolWarning"); |     var ignoreProtocolWarning = await getConfig("ignoreProtocolWarning"); | ||||||
|     checkIfConfigIsBroken(); |     checkIfConfigIsBroken(); | ||||||
|     registerIpc(); |     registerIpc(); | ||||||
| 
 |     if (await getConfig("mobileMode")) { | ||||||
|     // A little sloppy but it works :p
 |         mainWindow.webContents.userAgent = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.149 Mobile Safari/537.36" | ||||||
|     if (osType == 'Windows_NT') { |     } else { | ||||||
|         osType = "Windows " + os.release().split('.')[0] + " (" + os.release() + ")"; |         // A little sloppy but it works :p
 | ||||||
|  |         if (osType == 'Windows_NT') { | ||||||
|  |             osType = "Windows " + os.release().split('.')[0] + " (" + os.release() + ")"; | ||||||
|  |         } | ||||||
|  |         mainWindow.webContents.userAgent = `Mozilla/5.0 (X11; ${osType} ${os.arch()}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36`; //fake useragent for screenshare to work
 | ||||||
|     } |     } | ||||||
|     mainWindow.webContents.userAgent = `Mozilla/5.0 (X11; ${osType} ${os.arch()}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36`; //fake useragent for screenshare to work
 | 
 | ||||||
|     mainWindow.webContents.setWindowOpenHandler(({ url }) => { |     mainWindow.webContents.setWindowOpenHandler(({ url }) => { | ||||||
|         if (url.startsWith("https:" || url.startsWith("http:") || url.startsWith("mailto:"))) { |         if (url.startsWith("https:" || url.startsWith("http:") || url.startsWith("mailto:"))) { | ||||||
|             shell.openExternal(url); |             shell.openExternal(url); | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue