mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 17:21:05 +00:00
feat: extension feedback built in
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { titleBarState } from "./titleBarState.svelte";
|
||||
</script>
|
||||
|
||||
<div id="bsplus-title-root">
|
||||
<span data-testid="page-title">{titleBarState.pageTitle}</span>
|
||||
{#if titleBarState.showSearch}
|
||||
<div class="search-trigger-wrapper">
|
||||
<div class="search-trigger-anchor">
|
||||
<div class="search-trigger">
|
||||
<span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<p>Quick search...</p>
|
||||
<span
|
||||
class="search-trigger-hotkey"
|
||||
style="margin-left: auto; display: flex; align-items: center; color: rgb(119, 119, 119); font-size: 12px;"
|
||||
>{titleBarState.searchHotkeyLabel}</span>
|
||||
</div>
|
||||
<div class="search-progress-bar-wrapper">
|
||||
<div class="search-progress-track">
|
||||
<div class="search-progress-bar" style="width: 0%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-progress-text" aria-live="polite"></div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
export {
|
||||
prepareCustomTitleBarEarly,
|
||||
mountCustomTitleBar,
|
||||
unmountCustomTitleBar,
|
||||
setCustomTitleBarText,
|
||||
waitForCustomTitleBarReady,
|
||||
} from "./mountCustomTitleBar";
|
||||
export { titleBarState } from "./titleBarState.svelte";
|
||||
@@ -0,0 +1,161 @@
|
||||
import { mount, unmount } from "svelte";
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
|
||||
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||
import TitleBar from "./TitleBar.svelte";
|
||||
import { titleBarState } from "./titleBarState.svelte";
|
||||
|
||||
const ROOT_ID = "bsplus-title-root";
|
||||
const TITLE_CLASS = "bsplus-custom-title";
|
||||
const PENDING_CLASS = "bsplus-custom-title-pending";
|
||||
|
||||
let app: ReturnType<typeof mount> | null = null;
|
||||
let titleEl: HTMLElement | null = null;
|
||||
let hostObserver: MutationObserver | null = null;
|
||||
let earlyPrepareStarted = false;
|
||||
let remountTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function nativePageTitleEl(host: HTMLElement) {
|
||||
return [...host.children].find(
|
||||
(el) =>
|
||||
el instanceof HTMLElement &&
|
||||
el.id !== ROOT_ID &&
|
||||
el.matches('span[data-testid="page-title"]'),
|
||||
) as HTMLElement | undefined;
|
||||
}
|
||||
|
||||
function syncPageTitle() {
|
||||
if (!titleEl) return;
|
||||
titleBarState.pageTitle = (nativePageTitleEl(titleEl)?.textContent ?? "").trim();
|
||||
}
|
||||
|
||||
function needsSearchChip() {
|
||||
const all = settingsState.getAll() as unknown as Record<string, unknown>;
|
||||
const plugin = all["plugin.global-search.settings"] as
|
||||
| { enabled?: boolean }
|
||||
| undefined;
|
||||
return plugin?.enabled === true || titleBarState.showSearch;
|
||||
}
|
||||
|
||||
function isReady() {
|
||||
const root = document.getElementById(ROOT_ID);
|
||||
if (!root || !titleEl?.classList.contains(TITLE_CLASS)) return false;
|
||||
if (!needsSearchChip()) return true;
|
||||
return Boolean(root.querySelector(".search-trigger-wrapper"));
|
||||
}
|
||||
|
||||
/** finishLoad waits here so the overlay stays until the title bar is ready. */
|
||||
export async function waitForCustomTitleBarReady(timeoutMs = 10000) {
|
||||
if (isSeqtaEngageExperience() || !settingsState.onoff) {
|
||||
document.documentElement.classList.remove(PENDING_CLASS);
|
||||
return;
|
||||
}
|
||||
|
||||
await mountCustomTitleBar();
|
||||
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
if (isReady()) break;
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
}
|
||||
document.documentElement.classList.remove(PENDING_CLASS);
|
||||
}
|
||||
|
||||
function observeHost(host: HTMLElement) {
|
||||
hostObserver?.disconnect();
|
||||
syncPageTitle();
|
||||
hostObserver = new MutationObserver(() => {
|
||||
if (!document.getElementById(ROOT_ID)) {
|
||||
if (remountTimer) clearTimeout(remountTimer);
|
||||
remountTimer = setTimeout(() => {
|
||||
remountTimer = null;
|
||||
if (!settingsState.onoff || isSeqtaEngageExperience()) return;
|
||||
app = null;
|
||||
void mountCustomTitleBar();
|
||||
}, 50);
|
||||
return;
|
||||
}
|
||||
syncPageTitle();
|
||||
});
|
||||
hostObserver.observe(host, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function prepareCustomTitleBarEarly() {
|
||||
if (isSeqtaEngageExperience() || !settingsState.onoff || earlyPrepareStarted) {
|
||||
return;
|
||||
}
|
||||
earlyPrepareStarted = true;
|
||||
document.documentElement.classList.add(PENDING_CLASS);
|
||||
void mountCustomTitleBar();
|
||||
}
|
||||
|
||||
export async function mountCustomTitleBar(): Promise<boolean> {
|
||||
if (isSeqtaEngageExperience() || !settingsState.onoff) return false;
|
||||
|
||||
if (app && titleEl && document.getElementById(ROOT_ID)) {
|
||||
observeHost(titleEl);
|
||||
return true;
|
||||
}
|
||||
|
||||
document.documentElement.classList.add(PENDING_CLASS);
|
||||
|
||||
let title: HTMLElement;
|
||||
try {
|
||||
title = (await waitForElm("#title", true, 50, 200)) as HTMLElement;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
titleEl = title;
|
||||
title.classList.add(TITLE_CLASS);
|
||||
syncPageTitle();
|
||||
|
||||
if (!document.getElementById(ROOT_ID)) {
|
||||
if (app) {
|
||||
try {
|
||||
unmount(app);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
app = null;
|
||||
}
|
||||
app = mount(TitleBar, { target: title });
|
||||
}
|
||||
|
||||
observeHost(title);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function unmountCustomTitleBar() {
|
||||
hostObserver?.disconnect();
|
||||
hostObserver = null;
|
||||
if (remountTimer) clearTimeout(remountTimer);
|
||||
remountTimer = null;
|
||||
|
||||
if (app) {
|
||||
try {
|
||||
unmount(app);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
app = null;
|
||||
}
|
||||
|
||||
document.getElementById(ROOT_ID)?.remove();
|
||||
titleEl?.classList.remove(TITLE_CLASS);
|
||||
titleEl = null;
|
||||
titleBarState.pageTitle = "";
|
||||
titleBarState.showSearch = false;
|
||||
earlyPrepareStarted = false;
|
||||
document.documentElement.classList.remove(PENDING_CLASS);
|
||||
}
|
||||
|
||||
export function setCustomTitleBarText(text: string) {
|
||||
titleBarState.pageTitle = text;
|
||||
const native = titleEl && nativePageTitleEl(titleEl);
|
||||
if (native) native.textContent = text;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const titleBarState = $state({
|
||||
pageTitle: "",
|
||||
showSearch: false,
|
||||
searchHotkeyLabel: "Ctrl+K",
|
||||
});
|
||||
Reference in New Issue
Block a user