From 6c14589e98e8ee51b9704aba99dfcba038f932b8 Mon Sep 17 00:00:00 2001 From: thorsten Date: Thu, 27 Aug 2026 21:03:45 +0200 Subject: [PATCH 1/3] Configurable action pages in the settings --- src-tauri/src/commands/site.rs | 53 ++++++++++++++++++++++++-- src-tauri/src/settings.rs | 6 +++ src/lib/bridge/client.test.ts | 1 + src/lib/bridge/contract.ts | 8 +++- src/lib/bridge/mock.ts | 1 + src/lib/i18n/ar.ts | 4 ++ src/lib/i18n/de.ts | 4 ++ src/lib/i18n/en.ts | 4 ++ src/lib/i18n/es.ts | 4 ++ src/lib/i18n/fr.ts | 4 ++ src/lib/i18n/hi.ts | 4 ++ src/lib/i18n/it.ts | 4 ++ src/lib/i18n/ja.ts | 4 ++ src/lib/i18n/pt.ts | 4 ++ src/lib/i18n/ru.ts | 4 ++ src/lib/i18n/zh.ts | 4 ++ src/lib/site-urls.ts | 15 ++++++++ src/lib/stores/settings.svelte.ts | 1 + src/lib/views/settings-view.svelte | 59 +++++++++++++++++++++++++++++ src/lib/views/settings-view.test.ts | 2 + src/lib/views/x-view.test.ts | 1 + src/lib/views/youtube-view.test.ts | 1 + 22 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 src/lib/site-urls.ts diff --git a/src-tauri/src/commands/site.rs b/src-tauri/src/commands/site.rs index 5772032..1f08e04 100644 --- a/src-tauri/src/commands/site.rs +++ b/src-tauri/src/commands/site.rs @@ -41,6 +41,30 @@ fn target_url(platform: &str, action: &str, user_name: &str) -> Option { Some(url) } +/// The built-in page, unless the settings carry an override under `platform.group` — which +/// is how a platform moving a page again gets fixed from Settings rather than waited out. +fn resolve_target_url( + overrides: &std::collections::HashMap, + platform: &str, + action: &str, + user_name: &str, +) -> Option { + let key = format!("{platform}.{}", subject(action)); + if let Some(template) = overrides + .get(&key) + .map(|t| t.trim()) + .filter(|t| !t.is_empty()) + { + return Some(template.replace("{user}", &urlencoding_minimal(user_name))); + } + target_url(platform, action, user_name) +} + +fn effective_target_url(app: &AppHandle, platform: &str, action: &str) -> Option { + let overrides = app.state::().settings.get().site_urls; + resolve_target_url(&overrides, platform, action, &read_user_name(app)) +} + /// X handles are `[A-Za-z0-9_]`, so percent-encoding only has to defend against a handle /// that never should have got this far rather than implement general URL encoding. fn urlencoding_minimal(input: &str) -> String { @@ -95,8 +119,7 @@ pub fn navigate(app: &AppHandle, params: &Value) -> Result { .and_then(Value::as_str) .ok_or(Error::MissingParam("action"))?; - let user = read_user_name(app); - let url = target_url(platform, action, &user).ok_or_else(|| Error::NoTarget { + let url = effective_target_url(app, platform, action).ok_or_else(|| Error::NoTarget { platform: platform.to_string(), action: action.to_string(), })?; @@ -416,7 +439,7 @@ pub async fn run_action(app: AppHandle, params: &Value) -> Result { // same outcome, with the same empty log. // Scoped so the webview handle is dropped before the wait below: a `Webview` is not // `Send`, and holding one across an `await` makes the whole command un-spawnable. - if let Some(url) = target_url(platform, action, &read_user_name(&app)) { + if let Some(url) = effective_target_url(&app, platform, action) { { let site = app .get_webview(crate::site_webview_label(platform)) @@ -675,6 +698,30 @@ mod tests { ); } + #[test] + fn an_override_beats_the_built_in_page() { + let mut overrides = std::collections::HashMap::new(); + overrides.insert( + "x.reposts".to_string(), + "https://x.com/{user}/rt".to_string(), + ); + overrides.insert("x.likes".to_string(), " ".to_string()); + assert_eq!( + resolve_target_url(&overrides, "x", "deleteReposts", "someuser").unwrap(), + "https://x.com/someuser/rt" + ); + // Blank overrides read as "not set", and an untouched action keeps its built-in page. + assert_eq!( + resolve_target_url(&overrides, "x", "deleteLikes", "someuser").unwrap(), + "https://x.com/someuser/likes" + ); + assert_eq!( + resolve_target_url(&overrides, "x", "showReplies", "someuser").unwrap(), + "https://x.com/someuser/with_replies" + ); + assert!(resolve_target_url(&overrides, "x", "whatever", "someuser").is_none()); + } + /// `show*` and `delete*` land on the same page; deleting happens where the items are /// listed, so a divergence here would send a delete run to a page with nothing on it. #[test] diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index d1432c2..eda2262 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -94,6 +94,11 @@ pub struct AppSettings { #[serde(default)] pub custom_actions: Vec, pub timeouts: TimeoutSettings, + /// Overrides for the pages the actions run on, keyed `platform.group` (`x.reposts`), + /// with `{user}` standing for the handle. Only overrides live here — an absent key means + /// the built-in page in `commands::site::target_url`. + #[serde(default)] + pub site_urls: std::collections::HashMap, } impl Default for AppSettings { @@ -120,6 +125,7 @@ impl Default for AppSettings { assistant_effort: medium(), custom_actions: Vec::new(), timeouts: TimeoutSettings::default(), + site_urls: Default::default(), } } } diff --git a/src/lib/bridge/client.test.ts b/src/lib/bridge/client.test.ts index 7be31c3..4706467 100644 --- a/src/lib/bridge/client.test.ts +++ b/src/lib/bridge/client.test.ts @@ -27,6 +27,7 @@ describe('BridgeClient', () => { assistantModel: '', assistantEffort: 'medium' as const, customActions: [], + siteUrls: {}, timeouts: { waitAfterDelete: 100, waitBetweenRetryDeleteAttempts: 200, diff --git a/src/lib/bridge/contract.ts b/src/lib/bridge/contract.ts index fec1ba2..c8b8094 100644 --- a/src/lib/bridge/contract.ts +++ b/src/lib/bridge/contract.ts @@ -227,7 +227,13 @@ export const AppSettingsSchema = z.object({ assistantEffort: AssistantEffortSchema, /** Scripts the assistant wrote that the user kept. Shown in each platform's action panel. */ customActions: z.array(CustomActionSchema), - timeouts: TimeoutSettingsSchema + timeouts: TimeoutSettingsSchema, + /** + * Overrides for the pages the actions run on, keyed `platform.group` (`x.reposts`), with + * `{user}` standing for the handle. Only overrides live here — an absent key means the + * built-in page, so resetting is deleting. See `$lib/site-urls.ts` for the defaults. + */ + siteUrls: z.record(z.string(), z.string()).default({}) }); export type AppSettings = z.infer; diff --git a/src/lib/bridge/mock.ts b/src/lib/bridge/mock.ts index df3cffe..4abb3bd 100644 --- a/src/lib/bridge/mock.ts +++ b/src/lib/bridge/mock.ts @@ -91,6 +91,7 @@ function mockSettings(): AppSettings { assistantModel: '', assistantEffort: 'medium', customActions: [], + siteUrls: {}, timeouts: { waitAfterDelete: 500, waitBetweenRetryDeleteAttempts: 500, diff --git a/src/lib/i18n/ar.ts b/src/lib/i18n/ar.ts index c1588c6..cc7cdaf 100644 --- a/src/lib/i18n/ar.ts +++ b/src/lib/i18n/ar.ts @@ -329,6 +329,10 @@ export const ar: Record = { 'header.url': 'أنت هنا', 'header.reload': 'إعادة تحميل الصفحة', + 'settings.urls': 'الصفحات', + 'settings.urls.description': + 'أين يعمل كل إجراء. {user} يمثل اسم المستخدم المسجّل؛ إفراغ الحقل يعيده إلى الصفحة المدمجة.', + 'settings.urls.reset': 'إعادة تعيين الصفحات', 'log.column.time': 'الوقت', 'log.column.level': 'المستوى', diff --git a/src/lib/i18n/de.ts b/src/lib/i18n/de.ts index 4dd9c72..31cc618 100644 --- a/src/lib/i18n/de.ts +++ b/src/lib/i18n/de.ts @@ -346,6 +346,10 @@ export const de: Record = { 'header.url': 'Du bist hier', 'header.reload': 'Seite neu laden', + 'settings.urls': 'Seiten', + 'settings.urls.description': + 'Wo jede Aktion läuft. {user} steht für den angemeldeten Handle; ein geleertes Feld kehrt zur eingebauten Seite zurück.', + 'settings.urls.reset': 'Seiten zurücksetzen', 'log.column.time': 'Zeit', 'log.column.level': 'Stufe', diff --git a/src/lib/i18n/en.ts b/src/lib/i18n/en.ts index f872d02..28bfbad 100644 --- a/src/lib/i18n/en.ts +++ b/src/lib/i18n/en.ts @@ -347,6 +347,10 @@ export const en = { 'header.url': 'You are here', 'header.reload': 'Reload page', + 'settings.urls': 'Pages', + 'settings.urls.description': + 'Where each action runs. {user} stands for the signed-in handle; clearing a field returns it to the built-in page.', + 'settings.urls.reset': 'Reset pages', 'log.column.time': 'Time', 'log.column.level': 'Level', diff --git a/src/lib/i18n/es.ts b/src/lib/i18n/es.ts index 0af4294..684d675 100644 --- a/src/lib/i18n/es.ts +++ b/src/lib/i18n/es.ts @@ -343,6 +343,10 @@ export const es: Record = { 'header.url': 'Estás aquí', 'header.reload': 'Recargar la página', + 'settings.urls': 'Páginas', + 'settings.urls.description': + 'Dónde se ejecuta cada acción. {user} representa el nombre de usuario conectado; vaciar un campo lo devuelve a la página integrada.', + 'settings.urls.reset': 'Restablecer páginas', 'log.column.time': 'Hora', 'log.column.level': 'Nivel', diff --git a/src/lib/i18n/fr.ts b/src/lib/i18n/fr.ts index bc6c09c..1217169 100644 --- a/src/lib/i18n/fr.ts +++ b/src/lib/i18n/fr.ts @@ -345,6 +345,10 @@ export const fr: Record = { 'header.url': 'Tu es ici', 'header.reload': 'Recharger la page', + 'settings.urls': 'Pages', + 'settings.urls.description': + 'Où chaque action s’exécute. {user} représente le pseudo connecté ; vider un champ le ramène à la page intégrée.', + 'settings.urls.reset': 'Réinitialiser les pages', 'log.column.time': 'Heure', 'log.column.level': 'Niveau', diff --git a/src/lib/i18n/hi.ts b/src/lib/i18n/hi.ts index b909441..fc9d8f5 100644 --- a/src/lib/i18n/hi.ts +++ b/src/lib/i18n/hi.ts @@ -336,6 +336,10 @@ export const hi: Record = { 'header.url': 'आप यहाँ हैं', 'header.reload': 'पेज फिर से लोड करें', + 'settings.urls': 'पेज', + 'settings.urls.description': + 'हर क्रिया कहाँ चलती है। {user} साइन-इन हैंडल के लिए है; खाली किया गया फ़ील्ड बिल्ट-इन पेज पर लौट जाता है।', + 'settings.urls.reset': 'पेज रीसेट करें', 'log.column.time': 'समय', 'log.column.level': 'स्तर', diff --git a/src/lib/i18n/it.ts b/src/lib/i18n/it.ts index d4f3f1f..8fe1e2a 100644 --- a/src/lib/i18n/it.ts +++ b/src/lib/i18n/it.ts @@ -343,6 +343,10 @@ export const it: Record = { 'header.url': 'Sei qui', 'header.reload': 'Ricarica la pagina', + 'settings.urls': 'Pagine', + 'settings.urls.description': + 'Dove viene eseguita ogni azione. {user} rappresenta l’handle connesso; svuotare un campo lo riporta alla pagina integrata.', + 'settings.urls.reset': 'Ripristina le pagine', 'log.column.time': 'Ora', 'log.column.level': 'Livello', diff --git a/src/lib/i18n/ja.ts b/src/lib/i18n/ja.ts index 342edc7..13e86af 100644 --- a/src/lib/i18n/ja.ts +++ b/src/lib/i18n/ja.ts @@ -334,6 +334,10 @@ export const ja: Record = { 'header.url': '現在の場所', 'header.reload': 'ページを再読み込み', + 'settings.urls': 'ページ', + 'settings.urls.description': + '各アクションが実行されるページ。{user} はサインイン中のハンドルを表します。空にした欄は組み込みのページに戻ります。', + 'settings.urls.reset': 'ページをリセット', 'log.column.time': '時刻', 'log.column.level': 'レベル', diff --git a/src/lib/i18n/pt.ts b/src/lib/i18n/pt.ts index 24f7efd..798250e 100644 --- a/src/lib/i18n/pt.ts +++ b/src/lib/i18n/pt.ts @@ -342,6 +342,10 @@ export const pt: Record = { 'header.url': 'Estás aqui', 'header.reload': 'Recarregar a página', + 'settings.urls': 'Páginas', + 'settings.urls.description': + 'Onde cada ação é executada. {user} representa o utilizador com sessão iniciada; esvaziar um campo devolve-o à página integrada.', + 'settings.urls.reset': 'Repor páginas', 'log.column.time': 'Hora', 'log.column.level': 'Nível', diff --git a/src/lib/i18n/ru.ts b/src/lib/i18n/ru.ts index 5aa5967..8d47f0d 100644 --- a/src/lib/i18n/ru.ts +++ b/src/lib/i18n/ru.ts @@ -338,6 +338,10 @@ export const ru: Record = { 'header.url': 'Вы здесь', 'header.reload': 'Перезагрузить страницу', + 'settings.urls': 'Страницы', + 'settings.urls.description': + 'Где выполняется каждое действие. {user} — имя вошедшего пользователя; очищенное поле возвращается к встроенной странице.', + 'settings.urls.reset': 'Сбросить страницы', 'log.column.time': 'Время', 'log.column.level': 'Уровень', diff --git a/src/lib/i18n/zh.ts b/src/lib/i18n/zh.ts index 9f3ace3..46e1eb3 100644 --- a/src/lib/i18n/zh.ts +++ b/src/lib/i18n/zh.ts @@ -321,6 +321,10 @@ export const zh: Record = { 'header.url': '你在这里', 'header.reload': '重新加载页面', + 'settings.urls': '页面', + 'settings.urls.description': + '每个操作运行的页面。{user} 代表已登录的用户名;清空字段即恢复内置页面。', + 'settings.urls.reset': '重置页面', 'log.column.time': '时间', 'log.column.level': '级别', diff --git a/src/lib/site-urls.ts b/src/lib/site-urls.ts new file mode 100644 index 0000000..9daba30 --- /dev/null +++ b/src/lib/site-urls.ts @@ -0,0 +1,15 @@ +/** + * The pages each action runs on, as the settings show and override them. `{user}` stands for + * the signed-in handle. Kept in step by hand with `target_url` in + * `src-tauri/src/commands/site.rs`, the same way the extension's copy is — the host decides, + * this is what the settings page can print beside the field. + */ +export const SITE_URL_DEFAULTS: Record = { + 'x.posts': 'https://x.com/search?q=from%3A{user}&src=typed_query', + 'x.replies': 'https://x.com/{user}/with_replies', + 'x.reposts': 'https://x.com/{user}/reposts', + 'x.likes': 'https://x.com/{user}/likes', + 'x.following': 'https://x.com/{user}/following', + 'youtube.comments': 'https://myactivity.google.com/page?page=youtube_comments', + 'youtube.likes': 'https://myactivity.google.com/page?page=youtube_likes' +}; diff --git a/src/lib/stores/settings.svelte.ts b/src/lib/stores/settings.svelte.ts index ee4aa5c..4158ea1 100644 --- a/src/lib/stores/settings.svelte.ts +++ b/src/lib/stores/settings.svelte.ts @@ -22,6 +22,7 @@ const FALLBACK_SETTINGS: AppSettings = { assistantModel: '', assistantEffort: 'medium', customActions: [], + siteUrls: {}, timeouts: { waitAfterDelete: 500, waitBetweenRetryDeleteAttempts: 500, diff --git a/src/lib/views/settings-view.svelte b/src/lib/views/settings-view.svelte index 4913f28..eabe8be 100644 --- a/src/lib/views/settings-view.svelte +++ b/src/lib/views/settings-view.svelte @@ -10,6 +10,8 @@ type Language } from '$lib/bridge/contract'; import { THEME_PRESETS } from '$lib/theme/preset'; + import { SITE_URL_DEFAULTS } from '$lib/site-urls'; + import { X_GROUPS, YOUTUBE_GROUPS } from '$lib/actions'; import { LANGUAGES, i18n, t } from '$lib/i18n/index.svelte'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; @@ -29,6 +31,7 @@ import { cn } from '$lib/utils'; import PaletteIcon from '@lucide/svelte/icons/palette'; import SlidersIcon from '@lucide/svelte/icons/sliders-horizontal'; + import GlobeIcon from '@lucide/svelte/icons/globe'; import LayoutGridIcon from '@lucide/svelte/icons/layout-grid'; import CodeIcon from '@lucide/svelte/icons/code'; import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw'; @@ -130,6 +133,25 @@ } ] as const; + /** One row per action, in the order the panels list them. The keys mirror the Rust side. */ + const urlFields = [ + ...X_GROUPS.map((group) => ({ key: `x.${group.key}`, platform: 'X', label: group.label })), + ...YOUTUBE_GROUPS.map((group) => ({ + key: `youtube.${group.key}`, + platform: 'YouTube', + label: group.label + })) + ]; + + /** Only overrides are stored: a field left empty or put back to the default is deleted. */ + function commitSiteUrl(key: string, value: string): void { + const next = { ...settingsStore.settings.siteUrls }; + const trimmed = value.trim(); + if (trimmed === '' || trimmed === SITE_URL_DEFAULTS[key]) delete next[key]; + else next[key] = trimmed; + void commit({ siteUrls: next }); + } + async function commit(next: Partial): Promise { const merged = { ...settingsStore.settings, ...next }; const parsed = AppSettingsSchema.safeParse(merged); @@ -645,6 +667,43 @@ + + + {@render cardTitle(t('settings.urls'), GlobeIcon)} + {t('settings.urls.description')} + + + {#each urlFields as field (field.key)} + + {#snippet control()} + + commitSiteUrl(field.key, e.currentTarget.value)} + /> + {/snippet} + + {/each} + +
+ +
+
+
+ diff --git a/src/lib/views/settings-view.test.ts b/src/lib/views/settings-view.test.ts index 504c679..442ec63 100644 --- a/src/lib/views/settings-view.test.ts +++ b/src/lib/views/settings-view.test.ts @@ -36,6 +36,7 @@ function setup(overrides: MockHandlers = {}) { assistantModel: '', assistantEffort: 'medium' as const, customActions: [], + siteUrls: {}, timeouts: { waitAfterDelete: 500, waitBetweenRetryDeleteAttempts: 500, @@ -143,6 +144,7 @@ describe('SettingsView', () => { assistantModel: '', assistantEffort: 'medium' as const, customActions: [], + siteUrls: {}, timeouts: { waitAfterDelete: 500, waitBetweenRetryDeleteAttempts: 500, diff --git a/src/lib/views/x-view.test.ts b/src/lib/views/x-view.test.ts index 1a68b01..5356591 100644 --- a/src/lib/views/x-view.test.ts +++ b/src/lib/views/x-view.test.ts @@ -59,6 +59,7 @@ function setup( assistantModel: '', assistantEffort: 'medium' as const, customActions, + siteUrls: {}, timeouts: { waitAfterDelete: 1, waitBetweenRetryDeleteAttempts: 1, waitAfterDocumentLoad: 1 } }), 'site.navigate': navigate, diff --git a/src/lib/views/youtube-view.test.ts b/src/lib/views/youtube-view.test.ts index e84704f..8ab93f7 100644 --- a/src/lib/views/youtube-view.test.ts +++ b/src/lib/views/youtube-view.test.ts @@ -33,6 +33,7 @@ function setup(confirmDeletion: boolean) { assistantModel: '', assistantEffort: 'medium' as const, customActions: [], + siteUrls: {}, timeouts: { waitAfterDelete: 1, waitBetweenRetryDeleteAttempts: 1, waitAfterDocumentLoad: 1 } }), 'site.navigate': navigate, From 438ed82f2dcad6642b3c9720e33ef24863371db4 Mon Sep 17 00:00:00 2001 From: thorsten Date: Thu, 27 Aug 2026 21:07:18 +0200 Subject: [PATCH 2/3] Version 3.6.0 --- release-notes/v3.6.0.md | 12 ++++++++++++ src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 release-notes/v3.6.0.md diff --git a/release-notes/v3.6.0.md b/release-notes/v3.6.0.md new file mode 100644 index 0000000..dd7e801 --- /dev/null +++ b/release-notes/v3.6.0.md @@ -0,0 +1,12 @@ +### What's Changed + +**The pages are yours to point** + +- New: **Settings → Pages** lists the page every action runs on — X posts, replies, + reposts, likes and following, YouTube comments and likes — and lets you change any of + them. `{user}` stands for the signed-in handle. Clearing a field brings back the + built-in page, and one button resets them all. +- The point of it: when a platform moves a page again — the way X just moved reposts — + the fix is a settings field, not a wait for the next release. + +The extension is unchanged and stays at v1.0.1. diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index e15a499..773b5b5 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -518,7 +518,7 @@ dependencies = [ [[package]] name = "cleanmyposts" -version = "3.5.1" +version = "3.6.0" dependencies = [ "dirs", "keyring", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0131812..ba39fdb 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cleanmyposts" -version = "3.5.1" +version = "3.6.0" description = "Bulk-delete your posts, reposts, replies, likes and comments on X and YouTube" authors = ["thorstenalpers"] repository = "https://github.com/thorstenalpers/CleanMyPosts" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 2c1e204..83d060f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "CleanMyPosts", - "version": "3.5.1", + "version": "3.6.0", "identifier": "com.thorstenalpers.cleanmyposts", "build": { "frontendDist": "../build", From c039f02cc8f50fc98a752d4d3a1056c24d924b18 Mon Sep 17 00:00:00 2001 From: thorsten Date: Thu, 27 Aug 2026 21:23:40 +0200 Subject: [PATCH 3/3] Page overrides in the extension popup, docs for both --- .agents/docs/09-feature-settings.md | 10 +++++- extension/manifest.json | 2 +- extension/src/background.ts | 18 +++++++++-- extension/src/popup/Popup.svelte | 49 +++++++++++++++++++++++++++++ extension/src/protocol.ts | 9 +++++- release-notes/v3.6.0.md | 5 ++- src/lib/assistant-context.ts | 4 +++ 7 files changed, 91 insertions(+), 6 deletions(-) diff --git a/.agents/docs/09-feature-settings.md b/.agents/docs/09-feature-settings.md index 44da0f4..eed6363 100644 --- a/.agents/docs/09-feature-settings.md +++ b/.agents/docs/09-feature-settings.md @@ -38,6 +38,7 @@ type AppSettings = { assistantSource: string; // 'claude-code' for the local binary, else a provider id assistantCliPath: string; // empty: look where Claude Code installs itself engineScript: string; // the user's own patch for the delete engine; empty = built-in + siteUrls: Record; // page overrides, keyed `platform.group`; absent = built-in timeouts: { waitAfterDelete: number; // ms — pause between individual delete actions waitBetweenRetryDeleteAttempts: number; @@ -52,7 +53,7 @@ A form in the chrome UI at `/settings`. It calls `settings.get` on load and `set on every change. The host pushes a `settingsChanged` event whenever settings change so other views stay in sync. -Four cards — **Appearance, General, Assistant, Automation** — each with a `CardDescription` +Five cards — **Appearance, General, Assistant, Automation, Pages** — each with a `CardDescription` saying what the group is for, and `SettingRow`s inside. - **Appearance**: the mode switch (System / Light / Dark), the colour preset picker and the @@ -71,6 +72,13 @@ saying what the group is for, and `SettingRow`s inside. provider with a button into the API-keys dialog. - **Automation**: everything that decides how a run behaves — the confirmation, the cookie banners, the three waits, and the engine script. +- **Pages**: the address every action runs on, one row per action. Only overrides are stored + (`siteUrls`, keyed `platform.group`, `{user}` for the handle); clearing a field or matching + the default deletes the key, and "Reset pages" drops them all. The Rust host resolves the + override in `resolve_target_url` (`src-tauri/src/commands/site.rs`); the defaults the view + prints beside the fields live in `src/lib/site-urls.ts`, kept in step by hand. The + extension mirrors the card in its popup (`siteUrls` in `PopupSettings`), resolved in its + `background.ts` the same way. `notifications` and `telemetry` are two switches that could be mistaken for each other and are not. A toast is a courtesy; the log is the record, and the diagnostics switch is enforced diff --git a/extension/manifest.json b/extension/manifest.json index fbe713f..733162e 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "CleanMyPosts", - "version": "1.0.1", + "version": "1.1.0", "description": "Bulk-delete your posts, replies, reposts, likes and followings on X, and your comments and liked videos on YouTube.", "icons": { "32": "icons/32x32.png", diff --git a/extension/src/background.ts b/extension/src/background.ts index 61f98e8..ccf4f76 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -81,6 +81,20 @@ async function getTimeouts(): Promise { return (stored[SETTINGS_KEY] as PopupSettings | undefined)?.timeouts ?? DEFAULT_SETTINGS.timeouts; } +/** The built-in page, unless the popup's settings carry an override under `platform.group`. */ +async function resolveTargetUrl( + platform: Platform, + action: Action, + userName: string +): Promise { + const stored = await browser.storage.local.get(SETTINGS_KEY); + const overrides = (stored[SETTINGS_KEY] as PopupSettings | undefined)?.siteUrls ?? {}; + const group = action.replace(/^delete/, '').toLowerCase(); + const template = overrides[`${platform}.${group}`]?.trim(); + if (template) return template.replace('{user}', userName.replace(/[^A-Za-z0-9_]/g, '')); + return targetUrl(platform, action, userName); +} + function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -226,7 +240,7 @@ async function runNext(): Promise { return; } - const url = targetUrl(state.platform, action, state.userName ?? ''); + const url = await resolveTargetUrl(state.platform, action, state.userName ?? ''); if (!url) throw new Error(`unknown action "${state.platform}:${action}"`); const timeouts = await getTimeouts(); @@ -285,7 +299,7 @@ async function show(platform: Platform, action: Action): Promise { userName = known ?? ''; } - const url = targetUrl(platform, action, userName); + const url = await resolveTargetUrl(platform, action, userName); if (!url) throw new Error(`unknown action "${platform}:${action}"`); await navigate(tabId, url); diff --git a/extension/src/popup/Popup.svelte b/extension/src/popup/Popup.svelte index 335ff9c..d750074 100644 --- a/extension/src/popup/Popup.svelte +++ b/extension/src/popup/Popup.svelte @@ -2,6 +2,7 @@ import type { Component } from 'svelte'; import type { Platform } from '$lib/engine/protocol'; import { X_GROUPS, YOUTUBE_GROUPS, type ActionGroupDef } from '$lib/actions'; + import { SITE_URL_DEFAULTS } from '$lib/site-urls'; import ActionRow from '$lib/components/action-row.svelte'; import { ConfirmDialog } from '$lib/components/ui/alert-dialog'; import XIcon from '$lib/components/icons/x-icon.svelte'; @@ -107,6 +108,24 @@ save({ ...settings, timeouts: { ...settings.timeouts, [key]: Math.round(ms) } }); } + /** One row per action, mirroring the app's Settings → Pages. */ + const URL_FIELDS = PLATFORMS.flatMap((p) => + p.groups.map((group) => ({ + key: `${p.id}.${group.key}`, + platform: p.label, + label: group.label + })) + ); + + /** Only overrides are stored: a field left empty or put back to the default is deleted. */ + function setSiteUrl(key: string, value: string): void { + const next = { ...settings.siteUrls }; + const trimmed = value.trim(); + if (trimmed === '' || trimmed === SITE_URL_DEFAULTS[key]) delete next[key]; + else next[key] = trimmed; + save({ ...settings, siteUrls: next }); + } + // Two states, not three: `Default` is what it starts as, and the first press is a choice // away from whatever it happens to be showing. const isDark = $derived( @@ -305,6 +324,36 @@ {/each} + +
+

+ {t('settings.urls.description')} +

+ {#each URL_FIELDS as field (field.key)} + + {/each} + +
{/if} diff --git a/extension/src/protocol.ts b/extension/src/protocol.ts index c9c0135..feca1da 100644 --- a/extension/src/protocol.ts +++ b/extension/src/protocol.ts @@ -112,6 +112,12 @@ export interface PopupSettings { theme: 'Default' | 'Light' | 'Dark'; /** A `Language` from `$lib/i18n`; `System` reads `navigator.language`. */ language: string; + /** + * Overrides for the pages the actions run on, keyed `platform.group` (`x.reposts`), with + * `{user}` standing for the handle — the same scheme as the app's Settings → Pages. Only + * overrides live here; an absent key means the built-in page. + */ + siteUrls: Record; } export const SETTINGS_KEY = 'popupSettings'; @@ -126,7 +132,8 @@ export const DEFAULT_SETTINGS: PopupSettings = { }, welcomed: false, theme: 'Default', - language: 'System' + language: 'System', + siteUrls: {} }; /** Popup -> background. One action or all of them is the same request with a longer list. */ diff --git a/release-notes/v3.6.0.md b/release-notes/v3.6.0.md index dd7e801..1ca11ab 100644 --- a/release-notes/v3.6.0.md +++ b/release-notes/v3.6.0.md @@ -8,5 +8,8 @@ built-in page, and one button resets them all. - The point of it: when a platform moves a page again — the way X just moved reposts — the fix is a settings field, not a wait for the next release. +- The extension gets the same fields in its popup's settings panel, stored beside its + waits, as v1.1.0. -The extension is unchanged and stays at v1.0.1. +The assistant's troubleshooting notes know about the new card, so asking it about an +action that lands on an empty page now points at the settings field. diff --git a/src/lib/assistant-context.ts b/src/lib/assistant-context.ts index 905faf7..6bc73f6 100644 --- a/src/lib/assistant-context.ts +++ b/src/lib/assistant-context.ts @@ -108,6 +108,10 @@ function describeTroubleshooting(): string { '- "Deletion failed." on every item: the platform changed its markup and the engine’s', ' buttons moved. Update to the latest release; if it persists it is a bug worth', ' reporting with the log.', + '- An action opens a page that no longer lists its items: the platform moved the page.', + ' Settings → Pages holds the address every action runs on and can point it somewhere', + ' else; `{user}` stands for the signed-in handle, clearing a field brings back the', + ' built-in page, and "Reset pages" clears every override at once.', '- Some likes or followings survive: not everything is reachable from the list', ' (protected accounts, items behind "Show more"). Running the action again clears', ' most of it.',