Repository: web-scrobbler/web-scrobbler
Pull request #3725 (2023-05-15 09:24:33)

Make popup responsive
This is kinda patchworky as some major redesigning had to be done, but from my testing it seems to work without issue, without affecting desktop.

Note that this adds _a lot_ of new items to locale, as existing entries text are too long to be practical for the buttons.


Patch:
diff --git a/package-lock.json b/package-lock.json
index 3e44cb68..97b14a8c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,10 +22,10 @@
         "@types/fs-extra": "11.0.1",
         "@types/imagemin": "8.0.1",
         "@types/imagemin-jpegtran": "5.0.1",
-        "@types/node": "20.1.0",
+        "@types/node": "20.1.4",
         "@types/react": "18.2.6",
         "@types/webextension-polyfill": "0.10.0",
-        "@typescript-eslint/eslint-plugin": "5.59.2",
+        "@typescript-eslint/eslint-plugin": "5.59.5",
         "archiver": "5.3.1",
         "canvas": "^2.11.2",
         "chokidar": "3.5.3",
@@ -36,7 +36,7 @@
         "eslint-plugin-solid": "0.12.1",
         "eslint-plugin-tsdoc": "0.2.17",
         "fs-extra": "11.1.1",
-        "glob": "10.2.2",
+        "glob": "10.2.3",
         "imagemin": "8.0.1",
         "imagemin-jpegtran": "7.0.0",
         "imagemin-pngquant": "9.0.2",
@@ -51,7 +51,7 @@
         "tsx": "3.12.7",
         "typescript": "5.0.4",
         "vite-plugin-solid": "2.7.0",
-        "vite-plugin-static-copy": "0.14.0",
+        "vite-plugin-static-copy": "0.15.0",
         "vitest": "0.31.0"
       },
       "engines": {
@@ -1898,9 +1898,9 @@
       "dev": true
     },
     "node_modules/@types/node": {
-      "version": "20.1.0",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.0.tgz",
-      "integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A==",
+      "version": "20.1.4",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.4.tgz",
+      "integrity": "sha512-At4pvmIOki8yuwLtd7BNHl3CiWNbtclUbNtScGx4OHfBd4/oWoJC8KRCIxXwkdndzhxOsPXihrsOoydxBjlE9Q==",
       "devOptional": true
     },
     "node_modules/@types/normalize-package-data": {
@@ -1975,15 +1975,15 @@
       "dev": true
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "5.59.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.2.tgz",
-      "integrity": "sha512-yVrXupeHjRxLDcPKL10sGQ/QlVrA8J5IYOEWVqk0lJaSZP7X5DfnP7Ns3cc74/blmbipQ1htFNVGsHX6wsYm0A==",
+      "version": "5.59.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.5.tgz",
+      "integrity": "sha512-feA9xbVRWJZor+AnLNAr7A8JRWeZqHUf4T9tlP+TN04b05pFVhO5eN7/O93Y/1OUlLMHKbnJisgDURs/qvtqdg==",
       "dev": true,
       "dependencies": {
         "@eslint-community/regexpp": "^4.4.0",
-        "@typescript-eslint/scope-manager": "5.59.2",
-        "@typescript-eslint/type-utils": "5.59.2",
-        "@typescript-eslint/utils": "5.59.2",
+        "@typescript-eslint/scope-manager": "5.59.5",
+        "@typescript-eslint/type-utils": "5.59.5",
+        "@typescript-eslint/utils": "5.59.5",
         "debug": "^4.3.4",
         "grapheme-splitter": "^1.0.4",
         "ignore": "^5.2.0",
@@ -2073,13 +2073,13 @@
       }
     },
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "5.59.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz",
-      "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==",
+      "version": "5.59.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.5.tgz",
+      "integrity": "sha512-jVecWwnkX6ZgutF+DovbBJirZcAxgxC0EOHYt/niMROf8p4PwxxG32Qdhj/iIQQIuOflLjNkxoXyArkcIP7C3A==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "5.59.2",
-        "@typescript-eslint/visitor-keys": "5.59.2"
+        "@typescript-eslint/types": "5.59.5",
+        "@typescript-eslint/visitor-keys": "5.59.5"
       },
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -2090,9 +2090,9 @@
       }
     },
     "node_modules/@typescript-eslint/scope-manager/node_modules/@typescript-eslint/types": {
-      "version": "5.59.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz",
-      "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==",
+      "version": "5.59.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.5.tgz",
+      "integrity": "sha512-xkfRPHbqSH4Ggx4eHRIO/eGL8XL4Ysb4woL8c87YuAo8Md7AUjyWKa9YMwTL519SyDPrfEgKdewjkxNCVeJW7w==",
       "dev": true,
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -2103,13 +2103,13 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "5.59.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.2.tgz",
-      "integrity": "sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ==",
+      "version": "5.59.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.5.tgz",
+      "integrity": "sha512-4eyhS7oGym67/pSxA2mmNq7X164oqDYNnZCUayBwJZIRVvKpBCMBzFnFxjeoDeShjtO6RQBHBuwybuX3POnDqg==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "5.59.2",
-        "@typescript-eslint/utils": "5.59.2",
+        "@typescript-eslint/typescript-estree": "5.59.5",
+        "@typescript-eslint/utils": "5.59.5",
         "debug": "^4.3.4",
         "tsutils": "^3.21.0"
       },
@@ -2130,9 +2130,9 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": {
-      "version": "5.59.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz",
-      "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==",
+      "version": "5.59.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.5.tgz",
+      "integrity": "sha512-xkfRPHbqSH4Ggx4eHRIO/eGL8XL4Ysb4woL8c87YuAo8Md7AUjyWKa9YMwTL519SyDPrfEgKdewjkxNCVeJW7w==",
       "dev": true,
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -2143,13 +2143,13 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": {
-      "version": "5.59.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz",
-      "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==",
+      "version": "5.59.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.5.tgz",
+      "integrity": "sha512-+XXdLN2CZLZcD/mO7mQtJMvCkzRfmODbeSKuMY/yXbGkzvA9rJyDY5qDYNoiz2kP/dmyAxXquL2BvLQLJFPQIg==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "5.59.2",
-        "@typescript-eslint/visitor-keys": "5.59.2",
+        "@typescript-eslint/types": "5.59.5",
+        "@typescript-eslint/visitor-keys": "5.59.5",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
@@ -2230,17 +2230,17 @@
       }
     },
     "node_modules/@typescript-eslint/utils": {
-      "version": "5.59.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.2.tgz",
-      "integrity": "sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==",
+      "version": "5.59.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.5.tgz",
+      "integrity": "sha512-sCEHOiw+RbyTii9c3/qN74hYDPNORb8yWCoPLmB7BIflhplJ65u2PBpdRla12e3SSTJ2erRkPjz7ngLHhUegxA==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.2.0",
         "@types/json-schema": "^7.0.9",
         "@types/semver": "^7.3.12",
-        "@typescript-eslint/scope-manager": "5.59.2",
-        "@typescript-eslint/types": "5.59.2",
-        "@typescript-eslint/typescript-estree": "5.59.2",
+        "@typescript-eslint/scope-manager": "5.59.5",
+        "@typescript-eslint/types": "5.59.5",
+        "@typescript-eslint/typescript-estree": "5.59.5",
         "eslint-scope": "^5.1.1",
         "semver": "^7.3.7"
       },
@@ -2256,9 +2256,9 @@
       }
     },
     "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": {
-      "version": "5.59.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz",
-      "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==",
+      "version": "5.59.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.5.tgz",
+      "integrity": "sha512-xkfRPHbqSH4Ggx4eHRIO/eGL8XL4Ysb4woL8c87YuAo8Md7AUjyWKa9YMwTL519SyDPrfEgKdewjkxNCVeJW7w==",
       "dev": true,
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -2269,13 +2269,13 @@
       }
     },
     "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": {
-      "version": "5.59.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz",
-      "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==",
+      "version": "5.59.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.5.tgz",
+      "integrity": "sha512-+XXdLN2CZLZcD/mO7mQtJMvCkzRfmODbeSKuMY/yXbGkzvA9rJyDY5qDYNoiz2kP/dmyAxXquL2BvLQLJFPQIg==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "5.59.2",
-        "@typescript-eslint/visitor-keys": "5.59.2",
+        "@typescript-eslint/types": "5.59.5",
+        "@typescript-eslint/visitor-keys": "5.59.5",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
@@ -2296,12 +2296,12 @@
       }
     },
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "5.59.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz",
-      "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==",
+      "version": "5.59.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.5.tgz",
+      "integrity": "sha512-qL+Oz+dbeBRTeyJTIy0eniD3uvqU7x+y1QceBismZ41hd4aBSRh8UAw4pZP0+XzLuPZmx4raNMq/I+59W2lXKA==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "5.59.2",
+        "@typescript-eslint/types": "5.59.5",
         "eslint-visitor-keys": "^3.3.0"
       },
       "engines": {
@@ -2313,9 +2313,9 @@
       }
     },
     "node_modules/@typescript-eslint/visitor-keys/node_modules/@typescript-eslint/types": {
-      "version": "5.59.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz",
-      "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==",
+      "version": "5.59.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.5.tgz",
+      "integrity": "sha512-xkfRPHbqSH4Ggx4eHRIO/eGL8XL4Ysb4woL8c87YuAo8Md7AUjyWKa9YMwTL519SyDPrfEgKdewjkxNCVeJW7w==",
       "dev": true,
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -5524,9 +5524,9 @@
       }
     },
     "node_modules/glob": {
-      "version": "10.2.2",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.2.tgz",
-      "integrity": "sha512-Xsa0BcxIC6th9UwNjZkhrMtNo/MnyRL8jGCP+uEwhA5oFOCY1f2s1/oNKY47xQ0Bg5nkjsfAEIej1VeH62bDDQ==",
+      "version": "10.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.3.tgz",
+      "integrity": "sha512-Kb4rfmBVE3eQTAimgmeqc2LwSnN0wIOkkUL6HmxEFxNJ4fHghYHVbFba/HcGcRjE6s9KoMNK3rSOwkL4PioZjg==",
       "dev": true,
       "dependencies": {
         "foreground-child": "^3.1.0",
@@ -12214,9 +12214,9 @@
       }
     },
     "node_modules/vite-plugin-static-copy": {
-      "version": "0.14.0",
-      "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-0.14.0.tgz",
-      "integrity": "sha512-RMFmb4czomcrsbQBiUZs9HcDGN3kxGvF+OrtkfTVocp12CuoUCuJQhcY26RK35A6KS4WasGzEwcYZqHMjkAvVw==",
+      "version": "0.15.0",
+      "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-0.15.0.tgz",
+      "integrity": "sha512-Ww+/Ug9guV45oIfIi/lA2z8v3K+lLHV9zCJqTVO4FTdqrJoZBj68VgGBSH1fi0N4q/EHW32RsL3ympi4Wlsq5w==",
       "dev": true,
       "dependencies": {
         "chokidar": "^3.5.3",
diff --git a/package.json b/package.json
index 03672cc5..6dbf23d8 100644
--- a/package.json
+++ b/package.json
@@ -39,10 +39,10 @@
     "@types/fs-extra": "11.0.1",
     "@types/imagemin": "8.0.1",
     "@types/imagemin-jpegtran": "5.0.1",
-    "@types/node": "20.1.0",
+    "@types/node": "20.1.4",
     "@types/react": "18.2.6",
     "@types/webextension-polyfill": "0.10.0",
-    "@typescript-eslint/eslint-plugin": "5.59.2",
+    "@typescript-eslint/eslint-plugin": "5.59.5",
     "archiver": "5.3.1",
     "canvas": "^2.11.2",
     "chokidar": "3.5.3",
@@ -53,7 +53,7 @@
     "eslint-plugin-solid": "0.12.1",
     "eslint-plugin-tsdoc": "0.2.17",
     "fs-extra": "11.1.1",
-    "glob": "10.2.2",
+    "glob": "10.2.3",
     "imagemin": "8.0.1",
     "imagemin-jpegtran": "7.0.0",
     "imagemin-pngquant": "9.0.2",
@@ -68,7 +68,7 @@
     "tsx": "3.12.7",
     "typescript": "5.0.4",
     "vite-plugin-solid": "2.7.0",
-    "vite-plugin-static-copy": "0.14.0",
+    "vite-plugin-static-copy": "0.15.0",
     "vitest": "0.31.0"
   },
   "engines": {
diff --git a/scripts/util.ts b/scripts/util.ts
index 765d62f7..e7b4fb0c 100644
--- a/scripts/util.ts
+++ b/scripts/util.ts
@@ -1,3 +1,6 @@
+import { resolve } from 'path';
+import { normalizePath } from 'vite';
+
 /**
  * Folder names corresponding to build targets
  */
@@ -37,5 +40,14 @@ export function getBrowser() {
 	return browserName;
 }
 
+/**
+ * Resolve vite path in a cross-platform way
+ *
+ * @param paths - paths to resolve
+ */
+export function resolvePath(...paths: string[]) {
+	return normalizePath(resolve(...paths));
+}
+
 export const releaseTarget = process.argv.at(-1);
 export const releaseType = process.argv.at(-2);
diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json
index efc4f7d4..34dcbe8f 100644
--- a/src/_locales/en/messages.json
+++ b/src/_locales/en/messages.json
@@ -511,6 +511,14 @@
         "message": "You cannot edit this track",
         "description": "Title of edit button"
     },
+    "infoEditTitleShort": {
+        "message": "Edit",
+        "description": "Title of edit button, should be short for mobile button label"
+    },
+    "infoEditUnableTitleShort": {
+        "message": "Can't Edit",
+        "description": "Title of edit button, should be short for mobile button label"
+    },
     "infoSwapTitle": {
         "message": "Swap title and artist",
         "description": "Title of swap button"
@@ -519,10 +527,22 @@
         "message": "You cannot swap empty track info",
         "description": "Title of swap button"
     },
+    "infoSwapTitleShort": {
+        "message": "Swap title/artist",
+        "description": "Title of swap button, should be short for mobile button label"
+    },
+    "infoSwapUnableTitleShort": {
+        "message": "Can't swap info",
+        "description": "Title of swap button, should be short for mobile button label"
+    },
     "infoRegexTitle": {
         "message": "Regex/Bulk edit mode",
         "description": "Title of regex/bulk edit button"
     },
+    "infoRegexTitleShort": {
+        "message": "Regex/Bulk edit",
+        "description": "Title of regex/bulk edit button, should be short for mobile button label"
+    },
     "infoEditedWarning": {
         "message": "This track has been edited. Regex edits are not applied to edited tracks, and the preview you see here may be inaccurate.",
         "description": "Warning message to user if bulk editing on a previously edited track"
@@ -535,6 +555,14 @@
         "message": "You cannot submit empty track info",
         "description": "Title of submit button"
     },
+    "infoSubmitTitleShort": {
+        "message": "Submit",
+        "description": "Title of submit button, should be short for mobile button label"
+    },
+    "infoSubmitUnableTitleShort": {
+        "message": "Can't submit",
+        "description": "Title of submit button, should be short for mobile button label"
+    },
     "infoRevertTitle": {
         "message": "Revert changes",
         "description": "Title of revert button"
@@ -543,6 +571,14 @@
         "message": "You cannot revert this track",
         "description": "Title of revert button"
     },
+    "infoRevertTitleShort": {
+      "message": "Revert",
+      "description": "Title of revert button, should be short for mobile button label"
+    },
+    "infoRevertUnableTitleShort": {
+      "message": "Can't Revert",
+      "description": "Title of revert button, should be short for mobile button label"
+    },
     "infoSkipTitle": {
         "message": "Don't scrobble current track",
         "description": "Title of skip button"
@@ -555,6 +591,18 @@
         "message": "You skipped this track",
         "description": "Title of unskip button"
     },
+    "infoSkipTitleShort": {
+      "message": "Skip",
+      "description": "Title of skip button, should be short for mobile button label"
+    },
+    "infoSkipUnableTitleShort": {
+      "message": "Can't Skip",
+      "description": "Title of skip button, should be short for mobile button label"
+    },
+    "infoSkippedTitleShort": {
+      "message": "Skipped",
+      "description": "Title of unskip button, should be short for mobile button label"
+    },
     "infoOpenAlbumArt": {
         "message": "Open album art in new tab",
         "description": "Title of album image"
diff --git a/src/connectors/youtube.ts b/src/connectors/youtube.ts
index 0548100c..ac62daf0 100644
--- a/src/connectors/youtube.ts
+++ b/src/connectors/youtube.ts
@@ -21,9 +21,18 @@ export {};
 const videoSelector = '.html5-main-video';
 
 const chapterNameSelector = '.html5-video-player .ytp-chapter-title-content';
-const videoTitleSelector = '.html5-video-player .ytp-title-link';
-const channelNameSelector = '#top-row .ytd-channel-name a';
-const videoDescriptionSelector = '#meta-contents #description';
+const videoTitleSelector = [
+	'.html5-video-player .ytp-title-link',
+	'.slim-video-information-title .yt-core-attributed-string',
+];
+const channelNameSelector = [
+	'#top-row .ytd-channel-name a',
+	'.slim-owner-channel-name .yt-core-attributed-string',
+];
+const videoDescriptionSelector = [
+	'#meta-contents #description',
+	'.crawler-full-description',
+];
 
 // Dummy category indicates an actual category is being fetched
 const categoryPending = 'YT_DUMMY_CATEGORY_PENDING';
@@ -68,7 +77,7 @@ const trackInfoGetters = [
 readConnectorOptions();
 setupEventListener();
 
-Connector.playerSelector = '#content';
+Connector.playerSelector = ['#content', '#player'];
 
 Connector.getTrackInfo = () => {
 	const trackInfo: TrackInfoWithAlbum = {};
@@ -217,7 +226,17 @@ function getVideoId() {
 		return Util.getYtVideoIdFromUrl(miniPlayerVideoUrl);
 	}
 
-	return Util.getAttrFromSelectors('ytd-watch-flexy', 'video-id');
+	const videoIDDesktop = Util.getAttrFromSelectors(
+		'ytd-watch-flexy',
+		'video-id'
+	);
+	if (videoIDDesktop) {
+		return videoIDDesktop;
+	}
+
+	// as a fallback on mobile, try to get the video ID from the URL
+	const videoIDMobile = new URLSearchParams(window.location.search).get('v');
+	return videoIDMobile;
 }
 
 function getVideoCategory() {
diff --git a/src/core/background/main.ts b/src/core/background/main.ts
index a19fef68..20bad3b5 100644
--- a/src/core/background/main.ts
+++ b/src/core/background/main.ts
@@ -17,6 +17,9 @@ import {
 	getCurrentTab,
 	setState,
 	unlockState,
+	enableConnector,
+	disableConnector,
+	disableUntilClosed,
 } from './util';
 import { ControllerModeStr } from '@/core/object/controller/controller';
 import { CloneableSong } from '@/core/object/song';
@@ -331,24 +334,15 @@ async function contextMenuHandler(info: browser.Menus.OnClickData) {
 
 	switch (info.menuItemId) {
 		case contextMenus.ENABLE_CONNECTOR: {
-			sendBackgroundMessage(tab.tabId, {
-				type: 'setConnectorState',
-				payload: true,
-			});
+			enableConnector(tab.tabId);
 			break;
 		}
 		case contextMenus.DISABLE_CONNECTOR: {
-			sendBackgroundMessage(tab.tabId, {
-				type: 'setConnectorState',
-				payload: false,
-			});
+			disableConnector(tab.tabId);
 			break;
 		}
 		case contextMenus.DISABLE_UNTIL_CLOSED: {
-			sendBackgroundMessage(tab.tabId, {
-				type: 'disableConnectorUntilTabIsClosed',
-				payload: undefined,
-			});
+			disableUntilClosed(tab.tabId);
 			break;
 		}
 	}
diff --git a/src/core/background/util.ts b/src/core/background/util.ts
index 3675b934..1d91a273 100644
--- a/src/core/background/util.ts
+++ b/src/core/background/util.ts
@@ -5,6 +5,7 @@ import * as ControllerMode from '@/core/object/controller/controller-mode';
 import * as BrowserStorage from '@/core/storage/browser-storage';
 import { controllerModePriority } from '@/core/object/controller/controller';
 import { performUpdateAction } from './action';
+import { sendBackgroundMessage } from '@/util/communication';
 
 const state = BrowserStorage.getStorage(BrowserStorage.STATE_MANAGEMENT);
 
@@ -134,3 +135,39 @@ export const DEFAULT_STATE: StateManagement = {
 	activeTabs: [],
 	browserPreferredTheme: 'light',
 };
+
+/**
+ * Disables connector of a tab
+ *
+ * @param tabId - tab id of tab to disable connector of
+ */
+export function disableConnector(tabId: number) {
+	sendBackgroundMessage(tabId, {
+		type: 'setConnectorState',
+		payload: false,
+	});
+}
+
+/**
+ * Enables connector of a tab
+ *
+ * @param tabId - tab id of tab to enable connector of
+ */
+export function enableConnector(tabId: number) {
+	sendBackgroundMessage(tabId, {
+		type: 'setConnectorState',
+		payload: true,
+	});
+}
+
+/**
+ * Disables connector of a tab until the tab is closed
+ *
+ * @param tabId - tab id of tab to disable connector of
+ */
+export function disableUntilClosed(tabId: number) {
+	sendBackgroundMessage(tabId, {
+		type: 'disableConnectorUntilTabIsClosed',
+		payload: undefined,
+	});
+}
diff --git a/src/core/connectors.ts b/src/core/connectors.ts
index c7fe79f3..4f50d6c4 100644
--- a/src/core/connectors.ts
+++ b/src/core/connectors.ts
@@ -9,7 +9,7 @@ export interface ConnectorMeta {
 export default <ConnectorMeta[]>[
 	{
 		label: 'YouTube',
-		matches: ['*://www.youtube.com/*'],
+		matches: ['*://www.youtube.com/*', '*://m.youtube.com/*'],
 		js: 'youtube.js',
 		id: 'youtube',
 	},
diff --git a/src/ui/components/context-menu/context-menu.module.scss b/src/ui/components/context-menu/context-menu.module.scss
index 02d36719..fdd6e4b2 100644
--- a/src/ui/components/context-menu/context-menu.module.scss
+++ b/src/ui/components/context-menu/context-menu.module.scss
@@ -1,6 +1,5 @@
 .contextMenu {
 	background-color: var(--last-fm-bg);
-	bottom: 0;
 	color: var(--text);
 	display: flex;
 
@@ -9,6 +8,13 @@
 		padding: 0.5em 0;
 		position: relative;
 
+		@supports (-webkit-touch-callout: none) {
+			&.activeDialogButton {
+				background-color: var(--always-last-fm-hover);
+				color: var(--always-light);
+			}
+		}
+
 		&.activeDialogButton::after {
 			animation: 0.1s ease-in-out forwards showTip;
 			border-left: 20px solid transparent;
@@ -40,7 +46,18 @@
 	position: absolute;
 	transform: translateY(calc(-100%)) scale(0);
 	transform-origin: bottom center;
-	z-index: 0;
+
+	&.leftDialog {
+		margin-left: 20px;
+		margin-right: auto;
+		transform-origin: bottom left;
+	}
+
+	&.rightDialog {
+		margin-left: auto;
+		margin-right: 20px;
+		transform-origin: bottom right;
+	}
 
 
 	// stylelint-disable-next-line no-duplicate-selectors
@@ -70,6 +87,8 @@
 
 .contextMenu,
 .contextMenuDialog {
+	z-index: 1000000;
+
 	// stylelint-disable-next-line no-duplicate-selectors
 	& {
 		/* avoid flickering on ios */
@@ -93,9 +112,11 @@
 			margin: 0 auto;
 		}
 
-		&:hover {
-			background-color: var(--always-last-fm-hover);
-			color: var(--always-light);
+		@supports not (-webkit-touch-callout: none) {
+			&:hover {
+				background-color: var(--always-last-fm-hover);
+				color: var(--always-light);
+			}
 		}
 	}
 }
diff --git a/src/ui/components/context-menu/context-menu.tsx b/src/ui/components/context-menu/context-menu.tsx
index 67aa2db3..42c37e7c 100644
--- a/src/ui/components/context-menu/context-menu.tsx
+++ b/src/ui/components/context-menu/context-menu.tsx
@@ -6,11 +6,29 @@ import {
 	itemIsSingular,
 	triggerNavigationButton,
 } from '@/ui/options/components/navigator';
-import { For, Setter, Show, createSignal, onMount } from 'solid-js';
+import {
+	Accessor,
+	For,
+	Setter,
+	Show,
+	createMemo,
+	createSignal,
+	onMount,
+} from 'solid-js';
 import styles from './context-menu.module.scss';
 import { t } from '@/util/i18n';
 
 function closeDialogs(e: MouseEvent) {
+	if (!(e.target instanceof Element)) return;
+
+	const dialogButton = e.target.closest(`.${styles.contextMenuItem}`);
+	const dialog = dialogButton?.nextElementSibling;
+
+	let currentlyOpen = true;
+	if (dialog instanceof HTMLDialogElement) {
+		currentlyOpen = dialog.open;
+	}
+
 	const dialogs = document.querySelectorAll('dialog');
 	dialogs.forEach((dialog) => {
 		dialog.close();
@@ -22,12 +40,7 @@ function closeDialogs(e: MouseEvent) {
 		dialogButton.classList.remove(styles.activeDialogButton);
 	});
 
-	if (!(e.target instanceof Element)) return;
-
-	const dialogButton = e.target.closest(`.${styles.contextMenuItem}`);
-	const dialog = dialogButton?.nextElementSibling;
-
-	if (!(dialog instanceof HTMLDialogElement)) return;
+	if (!(dialog instanceof HTMLDialogElement) || currentlyOpen) return;
 
 	dialog.show();
 	dialogButton?.classList.add(styles.activeDialogButton);
@@ -35,7 +48,7 @@ function closeDialogs(e: MouseEvent) {
 
 export default function ContextMenu(props: {
 	items: Navigator;
-	setActiveSetting: Setter<NavigatorNavigationButton>;
+	setActiveSetting?: Setter<NavigatorNavigationButton>;
 }) {
 	onMount(() => {
 		window.addEventListener('click', closeDialogs);
@@ -54,12 +67,14 @@ export default function ContextMenu(props: {
 
 function ContextMenuItems(props: {
 	items: Navigator;
-	setActiveSetting: Setter<NavigatorNavigationButton>;
+	setActiveSetting?: Setter<NavigatorNavigationButton>;
 }) {
 	return (
 		<For each={props.items}>
-			{(item) => (
+			{(item, index) => (
 				<ContextMenuItem
+					index={index}
+					length={props.items.length}
 					item={item}
 					setActiveSetting={props.setActiveSetting}
 				/>
@@ -69,10 +84,25 @@ function ContextMenuItems(props: {
 }
 
 function ContextMenuItem(props: {
+	index: Accessor<number>;
+	length: number;
 	item: NavigatorButtonGroup | NavigatorButton;
-	setActiveSetting: Setter<NavigatorNavigationButton>;
+	setActiveSetting?: Setter<NavigatorNavigationButton>;
 }) {
 	const [mounted, setMounted] = createSignal(false);
+	const locationClass = createMemo(() => {
+		if (props.length === 1) {
+			return styles.centerDialog;
+		}
+		if (props.index() === 0) {
+			return styles.leftDialog;
+		}
+		if (props.index() === props.length - 1) {
+			return styles.rightDialog;
+		}
+		return styles.centerDialog;
+	});
+
 	// hacky way to ensure animation doesnt play on load. If its not loaded it just fails to display an animation, no big deal.
 	onMount(() => {
 		setTimeout(() => {
@@ -83,16 +113,24 @@ function ContextMenuItem(props: {
 		<>
 			<button
 				class={styles.contextMenuItem}
-				onClick={() =>
-					triggerNavigationButton(props.item, props.setActiveSetting)
-				}
+				onClick={() => {
+					if (props.setActiveSetting && 'element' in props.item) {
+						triggerNavigationButton(
+							props.item,
+							props.setActiveSetting
+						);
+					}
+					if ('action' in props.item) {
+						props.item.action();
+					}
+				}}
 			>
 				<props.item.icon />
 				<label>{t(props.item.namei18n)}</label>
 			</button>
 			<Show when={!itemIsSingular(props.item)}>
 				<dialog
-					class={`${styles.contextMenuDialog} ${
+					class={`${styles.contextMenuDialog} ${locationClass()} ${
 						mounted() && `${styles.mountedAnimation}`
 					}`}
 				>
diff --git a/src/ui/components/util.tsx b/src/ui/components/util.tsx
new file mode 100644
index 00000000..e5251ed7
--- /dev/null
+++ b/src/ui/components/util.tsx
@@ -0,0 +1,13 @@
+export function Squircle(props: { id: string }) {
+	return (
+		<svg width="0" height="0">
+			<clipPath id={props.id} clipPathUnits="objectBoundingBox">
+				<path d="M 0, 0.5 C 0, 0.03 0.03, 0 0.5, 0 S 1, 0.03 1, 0.5 0.97, 1 0.5, 1 0, 0.97 0, 0.5" />
+			</clipPath>
+		</svg>
+	);
+}
+
+export function isIos() {
+	return CSS.supports('-webkit-touch-callout', 'none');
+}
diff --git a/src/ui/options/components/edit-options/edited-tracks.tsx b/src/ui/options/components/edit-options/edited-tracks.tsx
index 11b59080..66ad7e9c 100644
--- a/src/ui/options/components/edit-options/edited-tracks.tsx
+++ b/src/ui/options/components/edit-options/edited-tracks.tsx
@@ -75,7 +75,8 @@ function TrackInfo(props: {
 		<li class={styles.deleteListing}>
 			<button
 				class={styles.deleteEditButton}
-				onClick={() => {
+				onClick={(event) => {
+					event.stopPropagation();
 					props.mutate((e) => {
 						if (!e) return e;
 						delete e[props.key];
diff --git a/src/ui/options/components/edit-options/regex-edits.tsx b/src/ui/options/components/edit-options/regex-edits.tsx
index 881ce1b7..a3a5e117 100644
--- a/src/ui/options/components/edit-options/regex-edits.tsx
+++ b/src/ui/options/components/edit-options/regex-edits.tsx
@@ -73,7 +73,8 @@ function EditInfo(props: {
 		<li class={styles.deleteListing}>
 			<button
 				class={styles.deleteEditButton}
-				onClick={() => {
+				onClick={(event) => {
+					event.stopPropagation();
 					props.mutate((e) => {
 						if (!e) return e;
 						const o = e.filter((_, i) => i !== props.index);
diff --git a/src/ui/options/components/navigator.ts b/src/ui/options/components/navigator.ts
index 83675a8c..bf6983ee 100644
--- a/src/ui/options/components/navigator.ts
+++ b/src/ui/options/components/navigator.ts
@@ -12,9 +12,21 @@ import FAQ from '@/ui/options/components/faq';
 import ContactComponent from '@/ui/options/components/contact';
 import OptionsComponent from '@/ui/options/components/options/options';
 import Accounts from '@/ui/options/components/accounts';
+import ToggleOn from '@suid/icons-material/ToggleOnOutlined';
+import ToggleOff from '@suid/icons-material/ToggleOffOutlined';
+import Timer from '@suid/icons-material/TimerOutlined';
 import ConnectorOverrideOptions from '@/ui/options/components/connector-override';
 import EditOptions from '@/ui/options/components/edit-options/edit-options';
 import Favorite from '@suid/icons-material/FavoriteOutlined';
+import browser from 'webextension-polyfill';
+import {
+	disableConnector,
+	disableUntilClosed,
+	enableConnector,
+	getCurrentTab,
+} from '@/core/background/util';
+import { getConnectorByUrl } from '@/util/util-connector';
+import * as ControllerMode from '@/core/object/controller/controller-mode';
 
 /**
  * Type indicating possible states for modal
@@ -96,6 +108,68 @@ export const settings: Navigator = [
 	{ namei18n: 'showSomeLoveTitle', icon: Favorite, element: ShowSomeLove },
 ];
 
+async function getToggleNavigators(): Promise<NavigatorActionButton[]> {
+	const tab = await getCurrentTab();
+
+	if (tab.mode === ControllerMode.Unsupported) {
+		return [];
+	}
+
+	if (tab.mode === ControllerMode.Disabled) {
+		return [
+			{
+				namei18n: 'menuEnableConnector',
+				icon: ToggleOn,
+				action: () => {
+					enableConnector(tab.tabId);
+				},
+			},
+		];
+	}
+
+	return [
+		{
+			namei18n: 'menuDisableConnector',
+			icon: ToggleOff,
+			action: () => {
+				disableConnector(tab.tabId);
+			},
+		},
+		{
+			namei18n: 'menuDisableUntilTabClosed',
+			icon: Timer,
+			action: () => {
+				disableUntilClosed(tab.tabId);
+			},
+		},
+	];
+}
+
+export async function getMobileNavigatorGroup(): Promise<NavigatorButtonGroup> {
+	const group: NavigatorButtonGroup = {
+		namei18n: 'optionsOptions',
+		icon: Settings,
+		group: [
+			{
+				namei18n: 'optionsOptions',
+				icon: Settings,
+				action: () => {
+					browser.tabs.create({
+						url: browser.runtime.getURL(
+							'src/ui/options/index.html'
+						),
+					});
+				},
+			},
+		],
+	};
+
+	const toggleNavigators = await getToggleNavigators();
+	group.group.push(...toggleNavigators);
+
+	return group;
+}
+
 export function triggerNavigationButton(
 	button: NavigatorButton | NavigatorButtonGroup,
 	setActiveSetting: Setter<NavigatorNavigationButton>
@@ -120,5 +194,5 @@ export function triggerNavigationButton(
 export function itemIsSingular(
 	item: NavigatorButton | NavigatorButtonGroup
 ): item is NavigatorButton {
-	return 'element' in item;
+	return !('group' in item);
 }
diff --git a/src/ui/options/main.tsx b/src/ui/options/main.tsx
index 19aeb1be..1a860d07 100644
--- a/src/ui/options/main.tsx
+++ b/src/ui/options/main.tsx
@@ -41,12 +41,16 @@ function Options() {
 
 	const onclick = (e: MouseEvent) => {
 		const target = e.target;
-		if (!(target instanceof HTMLElement)) return;
+		if (!(target instanceof HTMLElement)) {
+			return;
+		}
 		if (
 			target.closest(`.${styles.modal}`) &&
 			!target.classList.contains(styles.modal)
-		)
+		) {
+			e.stopPropagation();
 			return;
+		}
 		modal?.close();
 	};
 	document.addEventListener('click', onclick);
diff --git a/src/ui/options/settings.module.scss b/src/ui/options/settings.module.scss
index 07e0833d..dc2bec2e 100644
--- a/src/ui/options/settings.module.scss
+++ b/src/ui/options/settings.module.scss
@@ -95,12 +95,23 @@ a {
 	position: relative;
 	width: 80vw;
 
+	@supports (-webkit-touch-callout: none) {
+		border-radius: 0;
+		// stylelint-disable-next-line unit-no-unknown
+		height: 100dvh;
+		margin: 0;
+		// stylelint-disable-next-line unit-no-unknown
+		max-height: 100dvh;
+		max-width: 100vw;
+		width: 100vw;
+	}
+
 	.modalContent {
 		box-sizing: border-box;
-		height: 80vh;
+		height: 100%;
 		overflow: auto;
 		padding: 1em;
-		width: 80vw;
+		width: 100%;
 	}
 
 	.modalClose {
@@ -113,6 +124,10 @@ a {
 		right: 0;
 		top: 0;
 
+		@supports (-webkit-touch-callout: none) {
+			padding: 0.75em 1em 0.5em;
+		}
+
 		&:hover {
 			background-color: var(--always-last-fm-hover);
 		}
diff --git a/src/ui/popup/edit.tsx b/src/ui/popup/edit.tsx
index 128aa40f..de96e930 100644
--- a/src/ui/popup/edit.tsx
+++ b/src/ui/popup/edit.tsx
@@ -1,9 +1,14 @@
 import { t } from '@/util/i18n';
 import styles from './popup.module.scss';
 import {
+	Accessor,
 	Match,
 	Resource,
+	Setter,
+	Show,
 	Switch,
+	createMemo,
+	createResource,
 	createSignal,
 	onCleanup,
 	onMount,
@@ -16,7 +21,13 @@ import Code from '@suid/icons-material/CodeOutlined';
 import PublishedWithChanges from '@suid/icons-material/PublishedWithChangesOutlined';
 import { sendBackgroundMessage } from '@/util/communication';
 import savedEdits from '@/core/storage/saved-edits';
-import Regex from './regex';
+import Regex, { RegexEditContextMenu } from './regex';
+import { Squircle, isIos } from '../components/util';
+import {
+	Navigator,
+	getMobileNavigatorGroup,
+} from '../options/components/navigator';
+import ContextMenu from '../components/context-menu/context-menu';
 
 /**
  * Component that allows the user to edit the currently playing track
@@ -74,127 +85,212 @@ export default function Edit(props: { tab: Resource<ManagerTab> }) {
 	});
 
 	return (
-		<div class={styles.nowPlayingPopup} ref={nowplaying}>
-			<Switch>
-				<Match when={isRegex()}>
-					<Regex clonedSong={clonedSong} tab={tab} />
-				</Match>
-				<Match when={!isRegex()}>
-					<a
-						class={styles.coverArtWrapper}
-						href={
-							clonedSong.getTrackArt() ??
-							browser.runtime.getURL('img/cover_art_default.png')
-						}
-						target="_blank"
-						rel="noopener noreferrer"
-						title={t('infoOpenAlbumArt')}
-					>
-						<img
-							class={styles.coverArt}
-							src={
+		<>
+			<Show when={isIos()}>
+				<Switch>
+					<Match when={isRegex()}>
+						<RegexEditContextMenu tab={tab} />
+					</Match>
+					<Match when={!isRegex()}>
+						<EditContextMenu
+							tab={tab}
+							clonedSong={clonedSong}
+							setIsRegex={setIsRegex}
+							track={track}
+							artist={artist}
+							album={album}
+							albumArtist={albumArtist}
+						/>
+					</Match>
+				</Switch>
+			</Show>
+			<div class={styles.nowPlayingPopup} ref={nowplaying}>
+				<Switch>
+					<Match when={isRegex()}>
+						<Regex clonedSong={clonedSong} tab={tab} />
+					</Match>
+					<Match when={!isRegex()}>
+						<a
+							class={styles.coverArtWrapper}
+							href={
 								clonedSong.getTrackArt() ??
 								browser.runtime.getURL(
 									'img/cover_art_default.png'
 								)
 							}
-						/>
-					</a>
-					<div class={styles.songDetails}>
-						<input
-							class={styles.editField}
-							type="text"
-							value={clonedSong.getTrack() ?? ''}
-							title={t('infoTrackPlaceholder')}
-							placeholder={t('infoTrackPlaceholder')}
-							onInput={(e) => {
-								setTrack(e.currentTarget.value);
-							}}
-						/>
-						<input
-							class={styles.editField}
-							type="text"
-							value={clonedSong.getArtist() ?? ''}
-							title={t('infoArtistPlaceholder')}
-							placeholder={t('infoArtistPlaceholder')}
-							onInput={(e) => {
-								setArtist(e.currentTarget.value);
-							}}
-						/>
-						<input
-							class={styles.editField}
-							type="text"
-							value={clonedSong.getAlbum() ?? ''}
-							title={t('infoAlbumPlaceholder')}
-							placeholder={t('infoAlbumPlaceholder')}
-							onInput={(e) => {
-								setAlbum(e.currentTarget.value);
-							}}
-						/>
-						<input
-							class={styles.editField}
-							type="text"
-							value={clonedSong.getAlbumArtist() ?? ''}
-							title={t('infoAlbumArtistPlaceholder')}
-							placeholder={t('infoAlbumArtistPlaceholder')}
-							onInput={(e) => {
-								setAlbumArtist(e.currentTarget.value);
-							}}
-						/>
-						<div class={styles.controlButtons}>
-							<button
-								class={styles.controlButton}
-								disabled={!track() || !artist()}
-								title={
-									!track() || !artist()
-										? t('infoSubmitUnableTitle')
-										: t('infoSubmitTitle')
+							target="_blank"
+							rel="noopener noreferrer"
+							title={t('infoOpenAlbumArt')}
+						>
+							<img
+								class={styles.coverArt}
+								src={
+									clonedSong.getTrackArt() ??
+									browser.runtime.getURL(
+										'img/cover_art_default.png'
+									)
 								}
-								onClick={() => {
-									saveEdit(tab, clonedSong, {
-										artist: artist(),
-										track: track(),
-										album: album() || null,
-										albumArtist: albumArtist() || null,
-									});
+							/>
+							<Squircle id="coverArtClip" />
+						</a>
+						<div class={styles.songDetails}>
+							<input
+								class={styles.editField}
+								type="text"
+								value={clonedSong.getTrack() ?? ''}
+								title={t('infoTrackPlaceholder')}
+								placeholder={t('infoTrackPlaceholder')}
+								onInput={(e) => {
+									setTrack(e.currentTarget.value);
 								}}
-							>
-								<Check />
-							</button>
-							<button
-								class={styles.controlButton}
-								disabled={!track() || !artist()}
-								title={
-									!track() || !artist()
-										? t('infoSwapUnableTitle')
-										: t('infoSwapTitle')
-								}
-								onClick={() => {
-									saveEdit(tab, clonedSong, {
-										artist: track(),
-										track: artist(),
-										album: album() || null,
-										albumArtist: albumArtist() || null,
-									});
+							/>
+							<input
+								class={styles.editField}
+								type="text"
+								value={clonedSong.getArtist() ?? ''}
+								title={t('infoArtistPlaceholder')}
+								placeholder={t('infoArtistPlaceholder')}
+								onInput={(e) => {
+									setArtist(e.currentTarget.value);
 								}}
-							>
-								<PublishedWithChanges />
-							</button>
-							<button
-								class={styles.controlButton}
-								title={t('infoRegexTitle')}
-								onClick={() => setIsRegex(true)}
-							>
-								<Code />
-							</button>
+							/>
+							<input
+								class={styles.editField}
+								type="text"
+								value={clonedSong.getAlbum() ?? ''}
+								title={t('infoAlbumPlaceholder')}
+								placeholder={t('infoAlbumPlaceholder')}
+								onInput={(e) => {
+									setAlbum(e.currentTarget.value);
+								}}
+							/>
+							<input
+								class={styles.editField}
+								type="text"
+								value={clonedSong.getAlbumArtist() ?? ''}
+								title={t('infoAlbumArtistPlaceholder')}
+								placeholder={t('infoAlbumArtistPlaceholder')}
+								onInput={(e) => {
+									setAlbumArtist(e.currentTarget.value);
+								}}
+							/>
+							<Show when={!isIos()}>
+								<div class={styles.controlButtons}>
+									<button
+										class={styles.controlButton}
+										disabled={!track() || !artist()}
+										title={
+											!track() || !artist()
+												? t('infoSubmitUnableTitle')
+												: t('infoSubmitTitle')
+										}
+										onClick={() => {
+											saveEdit(tab, clonedSong, {
+												artist: artist(),
+												track: track(),
+												album: album() || null,
+												albumArtist:
+													albumArtist() || null,
+											});
+										}}
+									>
+										<Check />
+									</button>
+									<button
+										class={styles.controlButton}
+										disabled={!track() || !artist()}
+										title={
+											!track() || !artist()
+												? t('infoSwapUnableTitle')
+												: t('infoSwapTitle')
+										}
+										onClick={() => {
+											saveEdit(tab, clonedSong, {
+												artist: track(),
+												track: artist(),
+												album: album() || null,
+												albumArtist:
+													albumArtist() || null,
+											});
+										}}
+									>
+										<PublishedWithChanges />
+									</button>
+									<button
+										class={styles.controlButton}
+										title={t('infoRegexTitle')}
+										onClick={() => setIsRegex(true)}
+									>
+										<Code />
+									</button>
+								</div>
+							</Show>
 						</div>
-					</div>
-				</Match>
-			</Switch>
-		</div>
+					</Match>
+				</Switch>
+			</div>
+		</>
 	);
 }
 
+function EditContextMenu(props: {
+	tab: Resource<ManagerTab>;
+	clonedSong: ClonedSong;
+	setIsRegex: Setter<boolean>;
+	track: Accessor<string>;
+	artist: Accessor<string>;
+	album: Accessor<string>;
+	albumArtist: Accessor<string>;
+}) {
+	const [navigatorResource] = createResource(getMobileNavigatorGroup);
+	const items = createMemo(() => {
+		const items: Navigator = [
+			{
+				namei18n:
+					!props.track() || !props.artist()
+						? 'infoSubmitUnableTitleShort'
+						: 'infoSubmitTitleShort',
+				icon: Check,
+				action: () =>
+					saveEdit(props.tab, props.clonedSong, {
+						artist: props.artist(),
+						track: props.track(),
+						album: props.album() || null,
+						albumArtist: props.albumArtist() || null,
+					}),
+			},
+			{
+				namei18n:
+					!props.track() || !props.artist()
+						? 'infoSwapUnableTitleShort'
+						: 'infoSwapTitleShort',
+				icon: PublishedWithChanges,
+				action: () =>
+					saveEdit(props.tab, props.clonedSong, {
+						artist: props.track(),
+						track: props.artist(),
+						album: props.album() || null,
+						albumArtist: props.albumArtist() || null,
+					}),
+			},
+			{
+				namei18n: 'infoRegexTitleShort',
+				icon: Code,
+				action: () => props.setIsRegex(true),
+			},
+		];
+		if (!navigatorResource.loading) {
+			const navigatorGroup = navigatorResource();
+			if (navigatorGroup) {
+				items.push(navigatorGroup);
+			}
+		}
+		return items;
+	});
+
+	return <ContextMenu items={items()} />;
+}
+
 /**
  * Save edit to storage
  * @param tab - tab data of the tab whose data is being edited
diff --git a/src/ui/popup/index.html b/src/ui/popup/index.html
index fed80a17..97ca6a8e 100644
--- a/src/ui/popup/index.html
+++ b/src/ui/popup/index.html
@@ -8,6 +8,5 @@
 	<script src="main.tsx" type="module"></script>
 </head>
 <body>
-	<div id="root"></div>
 </body>
 </html>
diff --git a/src/ui/popup/main.tsx b/src/ui/popup/main.tsx
index 284cbd32..5de79ea7 100644
--- a/src/ui/popup/main.tsx
+++ b/src/ui/popup/main.tsx
@@ -4,7 +4,15 @@ import Unsupported from './unsupported';
 import * as ControllerMode from '@/core/object/controller/controller-mode';
 import { initializeThemes } from '@/theme/themes';
 import '@/theme/themes.scss';
-import { Match, Switch, createResource, Show } from 'solid-js';
+import {
+	Match,
+	Switch,
+	createResource,
+	Show,
+	createMemo,
+	Accessor,
+	createEffect,
+} from 'solid-js';
 import { popupListener, setupPopupListeners } from '@/util/communication';
 import Base from './base';
 import { getCurrentTab } from '@/core/background/util';
@@ -16,6 +24,12 @@ import Settings from '@suid/icons-material/SettingsOutlined';
 import browser from 'webextension-polyfill';
 import styles from './popup.module.scss';
 import { t } from '@/util/i18n';
+import { isIos } from '../components/util';
+import ContextMenu from '../components/context-menu/context-menu';
+import {
+	Navigator,
+	getMobileNavigatorGroup,
+} from '../options/components/navigator';
 
 /**
  * List of modes that have a settings button in the popup content, dont show supplementary settings icon.
@@ -27,11 +41,40 @@ const settingModes = [
 	ControllerMode.Loading,
 ];
 
+/**
+ * List of modes that already have context menu handling in the popup content, dont show supplementary context menu on ios.
+ */
+const contextMenuModes = [
+	ControllerMode.Playing,
+	ControllerMode.Skipped,
+	ControllerMode.Scrobbled,
+	ControllerMode.Loading,
+	ControllerMode.Unknown,
+];
+
 /**
  * Component that determines what popup to show, and then shows it
  */
 function Popup() {
 	const [tab, setTab] = createResource(getCurrentTab);
+	const [navigatorResource, { refetch }] = createResource(
+		getMobileNavigatorGroup
+	);
+
+	createEffect(() => {
+		tab(); // does nothing, but causes the effect to re-run when the tab changes
+		refetch();
+	});
+
+	const items: Accessor<Navigator> = createMemo(() => {
+		if (!navigatorResource.loading) {
+			const navigator = navigatorResource();
+			if (navigator) {
+				return [navigator];
+			}
+		}
+		return [];
+	});
 
 	setupPopupListeners(
 		popupListener({
@@ -44,6 +87,11 @@ function Popup() {
 
 	return (
 		<>
+			<Show
+				when={!contextMenuModes.includes(tab()?.mode ?? '') && isIos()}
+			>
+				<ContextMenu items={items()} />
+			</Show>
 			<Switch fallback={<></>}>
 				<Match when={tab()?.mode === ControllerMode.Base}>
 					<Base />
@@ -71,7 +119,7 @@ function Popup() {
 				</Match>
 			</Switch>
 
-			<Show when={!settingModes.includes(tab()?.mode ?? '')}>
+			<Show when={!settingModes.includes(tab()?.mode ?? '') && !isIos()}>
 				<a
 					href={browser.runtime.getURL('src/ui/options/index.html')}
 					class={styles.settingsIcon}
@@ -87,9 +135,10 @@ function Popup() {
 }
 
 //render the popup
-const root = document.getElementById('root');
-if (!root) {
-	throw new Error('Root element not found');
-}
+const body = document.body;
+const root = document.createElement('div');
+root.id = 'root';
+root.classList.add(styles.root);
+body.appendChild(root);
 render(Popup, root);
 void initializeThemes();
diff --git a/src/ui/popup/nowplaying.tsx b/src/ui/popup/nowplaying.tsx
index dc426b23..cb0fde83 100644
--- a/src/ui/popup/nowplaying.tsx
+++ b/src/ui/popup/nowplaying.tsx
@@ -10,6 +10,7 @@ import {
 	Switch,
 	createEffect,
 	createMemo,
+	createResource,
 	createSignal,
 	onCleanup,
 	onMount,
@@ -35,6 +36,12 @@ import {
 } from '@/util/util';
 import scrobbleService from '@/core/object/scrobble-service';
 import { SessionData } from '@/core/scrobbler/base-scrobbler';
+import { Squircle, isIos } from '../components/util';
+import ContextMenu from '../components/context-menu/context-menu';
+import {
+	Navigator,
+	getMobileNavigatorGroup,
+} from '../options/components/navigator';
 
 /**
  * Component showing info for currently playing song if there is one
@@ -96,6 +103,13 @@ export default function NowPlaying(props: { tab: Resource<ManagerTab> }) {
 				<EditComponent tab={tab} />
 			</Match>
 			<Match when={song()}>
+				<Show when={isIos()}>
+					<NowPlayingContextMenu
+						song={song}
+						tab={tab}
+						setIsEditing={setIsEditing}
+					/>
+				</Show>
 				<div class={styles.nowPlayingPopup} ref={nowplaying}>
 					<PopupLink
 						class={styles.coverArtWrapper}
@@ -114,6 +128,7 @@ export default function NowPlaying(props: { tab: Resource<ManagerTab> }) {
 								)
 							}
 						/>
+						<Squircle id="coverArtClip" />
 					</PopupLink>
 					<SongDetails
 						song={song}
@@ -126,6 +141,53 @@ export default function NowPlaying(props: { tab: Resource<ManagerTab> }) {
 	);
 }
 
+function NowPlayingContextMenu(props: {
+	song: Accessor<ClonedSong | null>;
+	tab: Resource<ManagerTab>;
+	setIsEditing: Setter<boolean>;
+}) {
+	const [navigatorResource] = createResource(getMobileNavigatorGroup);
+	const items = createMemo(() => {
+		const items: Navigator = [
+			{
+				namei18n:
+					props.tab()?.mode === ControllerMode.Playing
+						? 'infoEditTitleShort'
+						: 'infoEditUnableTitleShort',
+				icon: Edit,
+				action: () => props.setIsEditing(true),
+			},
+		];
+		if (props.song()?.flags.isCorrectedByUser) {
+			items.push({
+				namei18n:
+					props.tab()?.mode === ControllerMode.Playing
+						? 'infoRevertTitleShort'
+						: 'infoRevertUnableTitleShort',
+				icon: RestartAlt,
+				action: () => actionResetSongData(props.tab),
+			});
+		}
+		items.push({
+			namei18n: getSkipLabel(props.tab, true),
+			icon: Block,
+			action: () => actionSkipCurrentSong(props.tab),
+		});
+		if (!navigatorResource.loading) {
+			const navigatorGroup = navigatorResource();
+			if (navigatorGroup) {
+				items.push(navigatorGroup);
+			}
+		}
+		return items;
+	});
+
+	return <ContextMenu items={items()} />;
+}
+
+/**
+ * Component containing the metadata for the currently playing song
+ */
 function SongDetails(props: {
 	song: Accessor<ClonedSong | null>;
 	tab: Resource<ManagerTab>;
@@ -135,16 +197,42 @@ function SongDetails(props: {
 	return (
 		<div class={styles.songDetails}>
 			<TrackData song={song} />
+			<Show when={isIos()}>
+				<IOSLoveTrack song={song} tab={tab} />
+			</Show>
 			<TrackMetadata song={song} />
-			<TrackControls
-				song={song}
-				tab={tab}
-				setIsEditing={props.setIsEditing}
-			/>
+			<Show when={!isIos()}>
+				<TrackControls
+					song={song}
+					tab={tab}
+					setIsEditing={props.setIsEditing}
+				/>
+			</Show>
 		</div>
 	);
 }
 
+/**
+ * Component containing the button for loving track on iOS
+ */
+function IOSLoveTrack(props: {
+	tab: Resource<ManagerTab>;
+	song: Accessor<ClonedSong | null>;
+}) {
+	const { tab, song } = props;
+	return (
+		<button
+			class={`${styles.iosLoveButton}${
+				song()?.metadata.userloved ? ` ${styles.active}` : ''
+			}`}
+			onClick={() => toggleLove(tab, song)}
+			title={song()?.metadata.userloved ? t('infoUnlove') : t('infoLove')}
+		>
+			<Favorite />
+		</button>
+	);
+}
+
 /**
  * The component showing the track data.
  */
@@ -237,9 +325,7 @@ function TrackControls(props: {
 						? t('infoEditTitle')
 						: t('infoEditUnableTitle')
 				}
-				onClick={() => {
-					props.setIsEditing(true);
-				}}
+				onClick={() => props.setIsEditing(true)}
 			>
 				<Edit />
 			</button>
@@ -252,12 +338,7 @@ function TrackControls(props: {
 							? t('infoRevertTitle')
 							: t('infoRevertUnableTitle')
 					}
-					onClick={() => {
-						sendBackgroundMessage(tab()?.tabId ?? -1, {
-							type: 'resetData',
-							payload: undefined,
-						});
-					}}
+					onClick={() => actionResetSongData(tab)}
 				>
 					<RestartAlt />
 				</button>
@@ -273,13 +354,8 @@ function TrackControls(props: {
 						: ''
 				}`}
 				disabled={tab()?.mode !== ControllerMode.Playing}
-				onClick={() => {
-					sendBackgroundMessage(tab()?.tabId || -1, {
-						type: 'skipCurrentSong',
-						payload: undefined,
-					});
-				}}
-				title={t(getSkipLabel(tab))}
+				onClick={() => actionSkipCurrentSong(tab)}
+				title={t(getSkipLabel(tab, false))}
 			>
 				<Block />
 			</button>
@@ -287,14 +363,7 @@ function TrackControls(props: {
 				class={`${styles.controlButton}${
 					song()?.metadata.userloved ? ` ${styles.active}` : ''
 				}`}
-				onClick={() => {
-					sendBackgroundMessage(tab()?.tabId ?? -1, {
-						type: 'toggleLove',
-						payload: {
-							isLoved: !song()?.metadata.userloved,
-						},
-					});
-				}}
+				onClick={() => toggleLove(tab, song)}
 				title={
 					song()?.metadata.userloved ? t('infoUnlove') : t('infoLove')
 				}
@@ -341,16 +410,62 @@ function PopupLink(props: {
  * Get the correct label for the skip label based on current controller mode
  *
  * @param tab - currently active tab
+ * @param isShort - if the label should be short
  * @returns label for skip button
  */
-function getSkipLabel(tab: Resource<ManagerTab>): string {
+function getSkipLabel(tab: Resource<ManagerTab>, isShort: boolean): string {
+	let res = 'infoSkipUnableTitle';
 	switch (tab()?.mode) {
 		case ControllerMode.Playing:
-			return 'infoSkipTitle';
+			res = 'infoSkipTitle';
+			break;
 		case ControllerMode.Skipped:
-			return 'infoSkippedTitle';
+			res = 'infoSkippedTitle';
+			break;
 		case ControllerMode.Scrobbled:
-			return 'infoSkipUnableTitle';
+			res = 'infoSkipUnableTitle';
+			break;
 	}
-	return 'infoSkipUnableTitle';
+	return isShort ? `${res}Short` : res;
+}
+
+/**
+ * Skip current song
+ *
+ * @param tab - currently active tab
+ */
+function actionSkipCurrentSong(tab: Resource<ManagerTab>) {
+	sendBackgroundMessage(tab()?.tabId || -1, {
+		type: 'skipCurrentSong',
+		payload: undefined,
+	});
+}
+
+/**
+ * Reset song data
+ *
+ * @param tab - currently active tab
+ */
+function actionResetSongData(tab: Resource<ManagerTab>) {
+	sendBackgroundMessage(tab()?.tabId ?? -1, {
+		type: 'resetData',
+		payload: undefined,
+	});
+}
+
+/**
+ * Love current song
+ * @param tab - currently active tab
+ * @param song - currently playing song
+ */
+function toggleLove(
+	tab: Resource<ManagerTab>,
+	song: Accessor<ClonedSong | null>
+) {
+	sendBackgroundMessage(tab()?.tabId ?? -1, {
+		type: 'toggleLove',
+		payload: {
+			isLoved: !song()?.metadata.userloved,
+		},
+	});
 }
diff --git a/src/ui/popup/popup.module.scss b/src/ui/popup/popup.module.scss
index 36e01b89..f558838b 100644
--- a/src/ui/popup/popup.module.scss
+++ b/src/ui/popup/popup.module.scss
@@ -1,3 +1,12 @@
+@mixin iOSTextInput {
+	appearance: none;
+	background-color: var(--last-fm-bg);
+	border: none;
+	box-sizing: border-box;
+	color: var(--text);
+	font-size: 1.2em;
+}
+
 .alertPopup {
 	padding: 15px;
 	text-align: center;
@@ -76,6 +85,12 @@ a {
 		min-width: 30ch;
 		padding: 0.5em 1em;
 		white-space: nowrap;
+
+		@supports (-webkit-touch-callout: none) {
+			.editField {
+				@include iOSTextInput;
+			}
+		}
 	}
 
 	.playDetails {
@@ -274,3 +289,115 @@ a {
 		grid-area: albumartistpreview;
 	}
 }
+
+@supports (-webkit-touch-callout: none) {
+
+	.root {
+		display: flex;
+		flex-direction: column-reverse;
+		// stylelint-disable-next-line unit-no-unknown
+		height: 100dvh;
+		width: 100vw;
+	}
+
+	.nowPlayingPopup,
+	.alertPopup {
+		align-items: center;
+		box-sizing: border-box;
+		flex: 1 1 auto;
+		flex-direction: column;
+		margin: 0 auto;
+		overflow: auto;
+		overflow-y: auto;
+		padding-bottom: 1.5em;
+		width: 100%;
+	}
+
+	.nowPlayingPopup {
+		.coverArtWrapper {
+			filter: drop-shadow(0 0.25em 0.5em var(--text));
+			flex: 0 0 auto;
+			height: min(30vh, 30vw);
+			margin: 1em 0;
+			position: relative;
+
+			&::after {
+				background-color: var(--background);
+				clip-path: url(#coverArtClip);
+				content: '';
+				inset: 0;
+				position: absolute;
+				z-index: -100;
+			}
+
+			.coverArt {
+				clip-path: url(#coverArtClip);
+				display: inline-block;
+				max-width: min(30vh, 30vw);
+			}
+		}
+
+		.songDetails {
+			align-items: center;
+		}
+
+		.iosLoveButton {
+			background-color: transparent;
+			border: none;
+			color: var(--text);
+			cursor: pointer;
+			padding: 0.25em 0.5em;
+
+			&.active {
+				color: var(--last-fm);
+			}
+
+			svg {
+				height: 2em;
+				width: 2em;
+			}
+		}
+	}
+
+	.regexContainer {
+		align-items: center;
+		display: flex;
+		flex-direction: column;
+		width: 100%;
+
+		.iosSearchLabel,
+		.iosReplaceLabel,
+		.iosPreviewLabel {
+			margin-top: 0.5em;
+		}
+
+		input {
+			@include iOSTextInput;
+			width: 100%;
+		}
+
+		.entryWrapper {
+			align-items: center;
+			border: 1px solid var(--last-fm-bg);
+			border-radius: 1em;
+			display: flex;
+			flex-direction: column;
+
+			.entryWrapperLegend {
+				padding: 0 1em;
+			}
+		}
+
+		.searchWrapper {
+			align-items: center;
+			display: flex;
+			flex-direction: column;
+			width: 100%;
+
+			.regexTest {
+				bottom: -0.3em; // I have no idea why this works - and I honestly don't want to know either
+				top: auto;
+			}
+		}
+	}
+}
diff --git a/src/ui/popup/regex.tsx b/src/ui/popup/regex.tsx
index a92470e6..0e21aa50 100644
--- a/src/ui/popup/regex.tsx
+++ b/src/ui/popup/regex.tsx
@@ -5,11 +5,14 @@ import Close from '@suid/icons-material/CloseOutlined';
 
 import { t } from '@/util/i18n';
 import {
+	JSXElement,
 	Match,
 	Resource,
 	Show,
 	Switch,
 	createEffect,
+	createMemo,
+	createResource,
 	createSignal,
 } from 'solid-js';
 import {
@@ -25,6 +28,12 @@ import {
 import { ManagerTab } from '@/core/storage/wrapper';
 import regexEdits from '@/core/storage/regex-edits';
 import { sendBackgroundMessage } from '@/util/communication';
+import ContextMenu from '../components/context-menu/context-menu';
+import {
+	Navigator,
+	getMobileNavigatorGroup,
+} from '../options/components/navigator';
+import { isIos } from '../components/util';
 
 const [searches, setSearches] = createSignal({
 	track: null,
@@ -54,9 +63,11 @@ export default function Regex(props: {
 }) {
 	return (
 		<div class={styles.regexContainer}>
-			<span class={styles.searchLabel}>{t('infoSearchLabel')}</span>
-			<span class={styles.replaceLabel}>{t('infoReplaceLabel')}</span>
-			<span class={styles.previewLabel}>{t('infoPreviewLabel')}</span>
+			<Show when={!isIos()}>
+				<span class={styles.searchLabel}>{t('infoSearchLabel')}</span>
+				<span class={styles.replaceLabel}>{t('infoReplaceLabel')}</span>
+				<span class={styles.previewLabel}>{t('infoPreviewLabel')}</span>
+			</Show>
 			<Entry clonedSong={props.clonedSong} type="track" />
 			<Entry clonedSong={props.clonedSong} type="artist" />
 			<Entry clonedSong={props.clonedSong} type="album" />
@@ -87,23 +98,32 @@ function Entry(props: { clonedSong: ClonedSong; type: FieldType }) {
 	});
 
 	return (
-		<>
-			<span class={`${styles[`${type}Label`]} ${styles.entryLabel}`}>
-				{t(`info${pascalCaseField(type)}Label`)}
-			</span>
+		<EntryWrapper type={type}>
+			<Show when={!isIos()}>
+				<span class={`${styles[`${type}Label`]} ${styles.entryLabel}`}>
+					{t(`info${pascalCaseField(type)}Label`)}
+				</span>
+			</Show>
 			<SearchField type={type} clonedSong={clonedSong} />
-			<input
-				type="text"
-				class={styles[`${type}Replace`]}
-				onInput={(e) =>
-					setReplaces((prev) => ({
-						...prev,
-						[type]: e.currentTarget.value || null,
-					}))
-				}
-			/>
-			<span class={styles[`${type}Preview`]}>{previews()[type]}</span>
-		</>
+			<ReplaceField type={type} />
+			<PreviewOutput type={type} />
+		</EntryWrapper>
+	);
+}
+
+/**
+ * Wrapper for an entry that shows a fieldset on iOS and just the inputs on other platforms.
+ */
+function EntryWrapper(props: { type: FieldType; children: JSXElement }) {
+	return (
+		<Show when={isIos()} fallback={props.children}>
+			<fieldset class={styles.entryWrapper}>
+				<legend class={styles.entryWrapperLegend}>
+					{t(`info${pascalCaseField(props.type)}Label`)}
+				</legend>
+				{props.children}
+			</fieldset>
+		</Show>
 	);
 }
 
@@ -114,7 +134,13 @@ function SearchField(props: { type: FieldType; clonedSong: ClonedSong }) {
 	const { type, clonedSong } = props;
 	return (
 		<div class={`${styles[`${type}Search`]} ${styles.searchWrapper}`}>
+			<Show when={isIos()}>
+				<label for={`${type}SearchInput`} class={styles.iosSearchLabel}>
+					{t('infoSearchLabel')}
+				</label>
+			</Show>
 			<input
+				id={`${type}SearchInput`}
 				type="text"
 				class={styles.searchField}
 				onInput={(e) =>
@@ -149,6 +175,61 @@ function SearchField(props: { type: FieldType; clonedSong: ClonedSong }) {
 	);
 }
 
+/**
+ * A single replace input for a single song property.
+ */
+function ReplaceField(props: { type: FieldType }) {
+	const { type } = props;
+	return (
+		<>
+			<Show when={isIos()}>
+				<label
+					for={`${type}ReplaceInput`}
+					class={styles.iosReplaceLabel}
+				>
+					{t('infoReplaceLabel')}
+				</label>
+			</Show>
+			<input
+				id={`${type}ReplaceInput`}
+				type="text"
+				class={styles[`${type}Replace`]}
+				onInput={(e) =>
+					setReplaces((prev) => ({
+						...prev,
+						[type]: e.currentTarget.value || null,
+					}))
+				}
+			/>
+		</>
+	);
+}
+
+/**
+ * Preview of a single song property.
+ */
+function PreviewOutput(props: { type: FieldType }) {
+	const { type } = props;
+	return (
+		<>
+			<Show when={isIos()}>
+				<label
+					for={`${type}PreviewOutput`}
+					class={styles.iosPreviewLabel}
+				>
+					{t('infoPreviewLabel')}
+				</label>
+			</Show>
+			<output
+				id={`${type}PreviewOutput`}
+				class={styles[`${type}Preview`]}
+			>
+				{previews()[type]}
+			</output>
+		</>
+	);
+}
+
 /**
  * Footer of regex edit popup, allows the user to submit, and displays a warning if edit exists already.
  */
@@ -158,21 +239,48 @@ function Footer(props: { tab: Resource<ManagerTab>; clonedSong: ClonedSong }) {
 			<Show when={props.clonedSong.flags.isCorrectedByUser}>
 				<span class={styles.editWarning}>{t('infoEditedWarning')}</span>
 			</Show>
-			<div class={styles.controlButtons}>
-				<button
-					class={styles.controlButton}
-					title={t('infoSubmitTitle')}
-					onClick={() => {
-						saveEdit(props.tab);
-					}}
-				>
-					<Check />
-				</button>
-			</div>
+			<Show when={!isIos()}>
+				<div class={styles.controlButtons}>
+					<button
+						class={styles.controlButton}
+						title={t('infoSubmitTitle')}
+						onClick={() => {
+							saveEdit(props.tab);
+						}}
+					>
+						<Check />
+					</button>
+				</div>
+			</Show>
 		</div>
 	);
 }
 
+/**
+ * Component for the context menu of regex edit
+ */
+export function RegexEditContextMenu(props: { tab: Resource<ManagerTab> }) {
+	const [navigatorResource] = createResource(getMobileNavigatorGroup);
+	const items = createMemo(() => {
+		const items: Navigator = [
+			{
+				namei18n: 'infoSubmitTitleShort',
+				icon: Check,
+				action: () => saveEdit(props.tab),
+			},
+		];
+		if (!navigatorResource.loading) {
+			const navigatorGroup = navigatorResource();
+			if (navigatorGroup) {
+				items.push(navigatorGroup);
+			}
+		}
+		return items;
+	});
+
+	return <ContextMenu items={items()} />;
+}
+
 /**
  * Save edit to storage
  * @param tab - tab data of the tab whose data is being edited
diff --git a/vite.configs.ts b/vite.configs.ts
index 80ed7133..92687527 100644
--- a/vite.configs.ts
+++ b/vite.configs.ts
@@ -1,21 +1,20 @@
-import { UserConfig, normalizePath } from 'vite';
-import { resolve } from 'path';
+import { UserConfig } from 'vite';
 import * as manifests from './manifest.config';
 import solid from 'vite-plugin-solid';
 import { viteStaticCopy } from 'vite-plugin-static-copy';
 import compileConnectors from './scripts/compile-connectors';
 import makeManifest from './scripts/make-manifest';
-import { isDev, releaseTarget } from 'scripts/util';
+import { isDev, releaseTarget, resolvePath } from 'scripts/util';
 import minifyImages from 'scripts/minify-images';
 import generateIcons from 'scripts/generate-icons';
 
-const dist = resolve(import.meta.url.slice(5), '..', 'build');
-const root = resolve(import.meta.url.slice(5), '..', 'src');
+const dist = resolvePath('build');
+const root = resolvePath('src');
 const builds = {
-	chrome: resolve(dist, 'chrome'),
-	firefox: resolve(dist, 'firefox'),
-	safari: resolve(dist, 'safariraw'),
-	error: resolve(dist, 'error'),
+	chrome: resolvePath(dist, 'chrome'),
+	firefox: resolvePath(dist, 'firefox'),
+	safari: resolvePath(dist, 'safariraw'),
+	error: resolvePath(dist, 'error'),
 };
 
 const common: UserConfig = {
@@ -39,11 +38,11 @@ export const buildBackground: UserConfig = {
 	...common,
 	build: {
 		minify: !isDev(),
-		outDir: resolve(distRoot(), 'background'),
+		outDir: resolvePath(distRoot(), 'background'),
 		emptyOutDir: true,
 		watch: isDev() ? {} : null,
 		lib: {
-			entry: resolve(root, 'core', 'background', 'main.ts'),
+			entry: resolvePath(root, 'core', 'background', 'main.ts'),
 			name: manifests.common.name,
 			formats: ['iife'],
 		},
@@ -60,11 +59,11 @@ export const buildContent: UserConfig = {
 	...common,
 	build: {
 		minify: !isDev(),
-		outDir: resolve(distRoot(), 'content'),
+		outDir: resolvePath(distRoot(), 'content'),
 		emptyOutDir: true,
 		watch: isDev() ? {} : null,
 		lib: {
-			entry: resolve(root, 'core', 'content', 'main.ts'),
+			entry: resolvePath(root, 'core', 'content', 'main.ts'),
 			name: manifests.common.name,
 			formats: ['iife'],
 		},
@@ -87,8 +86,8 @@ export const buildStart: UserConfig = {
 		sourcemap: isDev(),
 		rollupOptions: {
 			input: {
-				popup: resolve(root, 'ui', 'popup', 'index.html'),
-				options: resolve(root, 'ui', 'options', 'index.html'),
+				popup: resolvePath(root, 'ui', 'popup', 'index.html'),
+				options: resolvePath(root, 'ui', 'options', 'index.html'),
 			},
 			output: {},
 		},
@@ -100,7 +99,7 @@ export const buildStart: UserConfig = {
 		viteStaticCopy({
 			targets: [
 				{
-					src: normalizePath(resolve(root, '_locales')),
+					src: resolvePath(root, '_locales'),
 					dest: '',
 				},
 			],


Test patch:
diff --git a/vitest.config.ts b/vitest.config.ts
index b422c9bd..47ea7eff 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -1,9 +1,9 @@
-import path from 'path';
+import { resolvePath } from './scripts/util';
 export default {
 	resolve: {
 		alias: {
-			'@': path.resolve(__dirname, './src'),
-			'#': path.resolve(__dirname, './tests'),
+			'@': resolvePath(__dirname, './src'),
+			'#': resolvePath(__dirname, './tests'),
 		},
 	},
 };