mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
@@ -1,5 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import localforage from 'localforage'
|
import localforage from 'localforage'
|
||||||
|
import {
|
||||||
|
isUseCloudPfpEnabled,
|
||||||
|
notifyProfilePictureChanged,
|
||||||
|
syncLocalProfilePictureToCloud,
|
||||||
|
} from '@/seqta/utils/cloudPfpSync'
|
||||||
let value = $state<string | undefined>(undefined)
|
let value = $state<string | undefined>(undefined)
|
||||||
let fileInput = $state<HTMLInputElement | undefined>(undefined)
|
let fileInput = $state<HTMLInputElement | undefined>(undefined)
|
||||||
let dragging = $state(false)
|
let dragging = $state(false)
|
||||||
@@ -25,6 +30,14 @@
|
|||||||
|
|
||||||
load()
|
load()
|
||||||
|
|
||||||
|
async function afterProfilePictureChange() {
|
||||||
|
window.dispatchEvent(new Event('profile-picture-updated'))
|
||||||
|
if (await isUseCloudPfpEnabled()) {
|
||||||
|
await syncLocalProfilePictureToCloud()
|
||||||
|
}
|
||||||
|
await notifyProfilePictureChanged()
|
||||||
|
}
|
||||||
|
|
||||||
function triggerSelect() {
|
function triggerSelect() {
|
||||||
fileInput?.click()
|
fileInput?.click()
|
||||||
}
|
}
|
||||||
@@ -43,7 +56,7 @@
|
|||||||
const newBlobUrl = URL.createObjectURL(file)
|
const newBlobUrl = URL.createObjectURL(file)
|
||||||
value = newBlobUrl
|
value = newBlobUrl
|
||||||
blobUrl = newBlobUrl
|
blobUrl = newBlobUrl
|
||||||
window.dispatchEvent(new Event('profile-picture-updated'))
|
await afterProfilePictureChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFileChange() {
|
function onFileChange() {
|
||||||
@@ -63,7 +76,7 @@
|
|||||||
}
|
}
|
||||||
value = undefined
|
value = undefined
|
||||||
await store.removeItem('profile-picture')
|
await store.removeItem('profile-picture')
|
||||||
window.dispatchEvent(new Event('profile-picture-updated'))
|
await afterProfilePictureChange()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
} from "@/plugins/core/settingsHelpers";
|
} from "@/plugins/core/settingsHelpers";
|
||||||
import ProfilePictureSetting from "./ProfilePictureSetting.svelte";
|
import ProfilePictureSetting from "./ProfilePictureSetting.svelte";
|
||||||
import { waitForElm } from "@/seqta/utils/waitForElm";
|
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||||
|
import browser from "webextension-polyfill";
|
||||||
import { cloudAuth } from "@/seqta/utils/CloudAuth";
|
import { cloudAuth } from "@/seqta/utils/CloudAuth";
|
||||||
import styles from "./styles.css?inline";
|
import styles from "./styles.css?inline";
|
||||||
import localforage from "localforage";
|
import localforage from "localforage";
|
||||||
@@ -67,7 +68,8 @@ const profilePicturePlugin: Plugin<typeof settings> = {
|
|||||||
if (useCloud && pfpUrl) {
|
if (useCloud && pfpUrl) {
|
||||||
img = document.createElement("img");
|
img = document.createElement("img");
|
||||||
img.className = "userInfoImg";
|
img.className = "userInfoImg";
|
||||||
img.src = pfpUrl;
|
const base = pfpUrl.split("?")[0]!;
|
||||||
|
img.src = `${base}?v=${Date.now()}`;
|
||||||
if (svg) svg.style.display = "none";
|
if (svg) svg.style.display = "none";
|
||||||
container.appendChild(img);
|
container.appendChild(img);
|
||||||
return;
|
return;
|
||||||
@@ -93,11 +95,26 @@ const profilePicturePlugin: Plugin<typeof settings> = {
|
|||||||
};
|
};
|
||||||
window.addEventListener("profile-picture-updated", onLocalPictureUpdated);
|
window.addEventListener("profile-picture-updated", onLocalPictureUpdated);
|
||||||
|
|
||||||
|
const onStorageRevision = (
|
||||||
|
changes: Record<string, browser.Storage.StorageChange>,
|
||||||
|
areaName: string,
|
||||||
|
) => {
|
||||||
|
if (areaName === "local" && changes.profile_picture_revision) {
|
||||||
|
void applyProfileImage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
browser.storage.onChanged.addListener(onStorageRevision);
|
||||||
|
|
||||||
const cloudUnsub = cloudAuth.subscribe(() => {
|
const cloudUnsub = cloudAuth.subscribe(() => {
|
||||||
void applyProfileImage();
|
void applyProfileImage();
|
||||||
});
|
});
|
||||||
|
|
||||||
const useCloudUnreg = api.settings.onChange("useCloudPfp", () => {
|
const useCloudUnreg = api.settings.onChange("useCloudPfp", (enabled: boolean) => {
|
||||||
|
if (enabled) {
|
||||||
|
void import("@/seqta/utils/cloudPfpSync").then(({ syncLocalProfilePictureToCloud }) =>
|
||||||
|
syncLocalProfilePictureToCloud(),
|
||||||
|
);
|
||||||
|
}
|
||||||
void applyProfileImage();
|
void applyProfileImage();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -105,6 +122,7 @@ const profilePicturePlugin: Plugin<typeof settings> = {
|
|||||||
useCloudUnreg.unregister();
|
useCloudUnreg.unregister();
|
||||||
cloudUnsub();
|
cloudUnsub();
|
||||||
window.removeEventListener("profile-picture-updated", onLocalPictureUpdated);
|
window.removeEventListener("profile-picture-updated", onLocalPictureUpdated);
|
||||||
|
browser.storage.onChanged.removeListener(onStorageRevision);
|
||||||
if (img) img.remove();
|
if (img) img.remove();
|
||||||
if (svg) svg.style.display = "";
|
if (svg) svg.style.display = "";
|
||||||
if (currentBlobUrl) URL.revokeObjectURL(currentBlobUrl);
|
if (currentBlobUrl) URL.revokeObjectURL(currentBlobUrl);
|
||||||
|
|||||||
@@ -107,6 +107,17 @@ class CloudAuthService {
|
|||||||
return (result[STORAGE_KEYS.accessToken] as string) ?? null;
|
return (result[STORAGE_KEYS.accessToken] as string) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Persist an updated user object (e.g. after cloud profile picture sync). */
|
||||||
|
public async setUser(user: CloudUser | null): Promise<void> {
|
||||||
|
(settingsState as any).setKey(STORAGE_KEYS.user, user);
|
||||||
|
await browser.storage.local.set({ [STORAGE_KEYS.user]: user });
|
||||||
|
this._state = {
|
||||||
|
isLoggedIn: this._state.isLoggedIn,
|
||||||
|
user,
|
||||||
|
};
|
||||||
|
this.notify();
|
||||||
|
}
|
||||||
|
|
||||||
private async getClientId(): Promise<string> {
|
private async getClientId(): Promise<string> {
|
||||||
let clientId = (settingsState as any)[STORAGE_KEYS.clientId] as string | undefined;
|
let clientId = (settingsState as any)[STORAGE_KEYS.clientId] as string | undefined;
|
||||||
if (!clientId) {
|
if (!clientId) {
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import browser from "webextension-polyfill";
|
||||||
|
import localforage from "localforage";
|
||||||
|
import { cloudAuth } from "@/seqta/utils/CloudAuth";
|
||||||
|
|
||||||
|
const ACCOUNTS_BASE = "https://accounts.betterseqta.org";
|
||||||
|
const PLUGIN_SETTINGS_KEY = "plugin.profile-picture.settings";
|
||||||
|
|
||||||
|
const profileStore = localforage.createInstance({
|
||||||
|
name: "profile-picture-store",
|
||||||
|
storeName: "profilePicture",
|
||||||
|
});
|
||||||
|
|
||||||
|
function cacheBustPfpUrl(url: string): string {
|
||||||
|
const base = url.split("?")[0]!;
|
||||||
|
return `${base}?v=${Date.now()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isUseCloudPfpEnabled(): Promise<boolean> {
|
||||||
|
const stored = await browser.storage.local.get(PLUGIN_SETTINGS_KEY);
|
||||||
|
const settings = stored[PLUGIN_SETTINGS_KEY] as { useCloudPfp?: boolean } | undefined;
|
||||||
|
return !!settings?.useCloudPfp;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function parseJsonResponse(r: Response): Promise<Record<string, unknown>> {
|
||||||
|
const text = await r.text();
|
||||||
|
try {
|
||||||
|
return text ? (JSON.parse(text) as Record<string, unknown>) : {};
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncLocalProfilePictureToCloud(): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
if (!(await isUseCloudPfpEnabled()) || !cloudAuth.state.isLoggedIn) {
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await cloudAuth.getStoredToken();
|
||||||
|
if (!token) return { success: false, error: "Not logged in" };
|
||||||
|
|
||||||
|
const blob = await profileStore.getItem<Blob>("profile-picture");
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!blob || !(blob instanceof Blob)) {
|
||||||
|
const res = await fetch(`${ACCOUNTS_BASE}/api/user/pfp/clear`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const data = await parseJsonResponse(res);
|
||||||
|
if (!res.ok) {
|
||||||
|
return { success: false, error: (data.error as string) ?? `Clear failed (${res.status})` };
|
||||||
|
}
|
||||||
|
const user = cloudAuth.state.user;
|
||||||
|
if (user) {
|
||||||
|
await cloudAuth.setUser({ ...user, pfpUrl: undefined });
|
||||||
|
}
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!blob.type.startsWith("image/")) {
|
||||||
|
return { success: false, error: "Invalid file type" };
|
||||||
|
}
|
||||||
|
if (blob.size > 5 * 1024 * 1024) {
|
||||||
|
return { success: false, error: "File too large (max 5MB)" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", blob, "profile-picture");
|
||||||
|
|
||||||
|
const res = await fetch(`${ACCOUNTS_BASE}/api/user/pfp`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
const data = await parseJsonResponse(res);
|
||||||
|
if (!res.ok) {
|
||||||
|
return { success: false, error: (data.error as string) ?? `Upload failed (${res.status})` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const pfpUrl = data.pfpUrl as string | undefined;
|
||||||
|
const user = cloudAuth.state.user;
|
||||||
|
if (user && pfpUrl) {
|
||||||
|
await cloudAuth.setUser({ ...user, pfpUrl: cacheBustPfpUrl(pfpUrl) });
|
||||||
|
}
|
||||||
|
return { success: true };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: err instanceof Error ? err.message : "Cloud profile picture sync failed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Notify SEQTA content scripts to refresh the in-page profile image. */
|
||||||
|
export async function notifyProfilePictureChanged(): Promise<void> {
|
||||||
|
const revision = Date.now();
|
||||||
|
await browser.storage.local.set({ profile_picture_revision: revision });
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user