├── src ├── content │ ├── styles │ │ ├── uno.css │ │ ├── index.css │ │ ├── firefox │ │ │ └── style-compat.css │ │ ├── style-polyfillForTwitter.css │ │ ├── features │ │ │ ├── style-discoverMore.css │ │ │ └── style-iconSettings.css │ │ └── style-tlui.css │ ├── modules │ │ ├── settings │ │ │ ├── safemode │ │ │ │ ├── isSafemode.ts │ │ │ │ └── safemode.ts │ │ │ └── display.ts │ │ ├── utils │ │ │ ├── getValues.ts │ │ │ ├── fontSize.ts │ │ │ ├── dateAndTime.ts │ │ │ ├── toastMessage.tsx │ │ │ └── color.ts │ │ ├── observer │ │ │ ├── functions │ │ │ │ ├── profile │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── followersListButton │ │ │ │ │ │ ├── buttons.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── initProfileTab.ts │ │ │ │ ├── fixTwittersBugs.ts │ │ │ │ ├── fixDM.ts │ │ │ │ ├── composeTweet.ts │ │ │ │ ├── tweetSettings │ │ │ │ │ └── _data.ts │ │ │ │ └── postingDialog.ts │ │ │ ├── functions.ts │ │ │ └── errorDialog.ts │ │ ├── i18n │ │ │ └── index.ts │ │ └── htmlClass │ │ │ └── classManager.ts │ ├── icons │ │ ├── logo │ │ │ ├── dog.png │ │ │ ├── empty.svg │ │ │ ├── x.svg │ │ │ └── twitter.svg │ │ ├── arrow │ │ │ ├── arrow_down.svg │ │ │ ├── arrow_left.svg │ │ │ ├── arrow_right.svg │ │ │ ├── arrow_up.svg │ │ │ └── reset.svg │ │ ├── brand │ │ │ ├── x.svg │ │ │ └── twitter.svg │ │ ├── common │ │ │ ├── arrow_right.svg │ │ │ ├── arrow_right_enabled.svg │ │ │ ├── github.svg │ │ │ ├── sidebar_enabled.svg │ │ │ ├── reveal.svg │ │ │ ├── pencil_enabled.svg │ │ │ ├── information.svg │ │ │ ├── more.svg │ │ │ ├── profile_enabled.svg │ │ │ ├── pencil.svg │ │ │ ├── sidebar.svg │ │ │ ├── dm_enabled.svg │ │ │ ├── home_enabled.svg │ │ │ ├── sparkles_enabled.svg │ │ │ ├── brush.svg │ │ │ ├── none.svg │ │ │ ├── tweet_enabled.svg │ │ │ ├── reset.svg │ │ │ ├── twitter.svg │ │ │ ├── dm.svg │ │ │ ├── home.svg │ │ │ ├── profile.svg │ │ │ ├── sparkles.svg │ │ │ ├── more_circle.svg │ │ │ ├── brush_enabled.svg │ │ │ ├── more_circle_enabled.svg │ │ │ ├── tweet.svg │ │ │ ├── verified.svg │ │ │ └── transparent.svg │ │ ├── arrow_cutter │ │ │ ├── arrow_down.svg │ │ │ ├── arrow_up.svg │ │ │ ├── arrow_toleft.svg │ │ │ └── arrow_toright.svg │ │ ├── home │ │ │ ├── home_twitter.svg │ │ │ ├── home_x.svg │ │ │ └── home_tuic.svg │ │ ├── branding │ │ │ ├── tuic_unilogo_gray.svg │ │ │ └── tuic_unilogo.svg │ │ └── figure │ │ │ └── import_append.svg │ └── printPref.tsx ├── shared │ ├── testError.ts │ ├── tlui │ │ ├── components │ │ │ ├── Component.ts │ │ │ ├── ContainerComponent.ts │ │ │ ├── DivBox.ts │ │ │ ├── ButtonComponent.ts │ │ │ └── TextboxComponent.ts │ │ └── observer.ts │ ├── settings │ │ ├── components │ │ │ ├── SectionTitle.vue │ │ │ ├── SectionTitle2.vue │ │ │ ├── settingSubtitle2.vue │ │ │ ├── settingSubTitleNomargin.vue │ │ │ ├── IconButton.vue │ │ │ ├── ColorResetButton.vue │ │ │ ├── RoundedColorPicker.vue │ │ │ ├── TransparentToggleButton.vue │ │ │ ├── detailsBox.vue │ │ │ └── defaultPrefButton.vue │ │ └── modules │ │ │ ├── settingTimeline.vue │ │ │ ├── settingDM.vue │ │ │ ├── settingUncategorized.vue │ │ │ ├── SettingsHeader.vue │ │ │ ├── settingProfile.vue │ │ │ ├── settingEffectText.vue │ │ │ ├── settingLogo.vue │ │ │ └── settingSidebar.vue │ ├── options │ │ ├── store.ts │ │ ├── components │ │ │ ├── RadioButtonList.vue │ │ │ ├── IconButton.vue │ │ │ ├── CheckBoxList.vue │ │ │ ├── textParts │ │ │ │ └── settingSubTitle.vue │ │ │ ├── IconRadioButtonList.vue │ │ │ ├── ColorsList.vue │ │ │ ├── ThreeColorSetting.vue │ │ │ ├── CheckBox.vue │ │ │ ├── RadioButton.vue │ │ │ ├── IconRadioButtonBase64Support.vue │ │ │ ├── IconRadioButton.vue │ │ │ └── UploadImageFile.vue │ │ ├── injectOption2Entry.ts │ │ ├── modules │ │ │ └── customCSS.vue │ │ ├── scripts │ │ │ └── changePrefScript.ts │ │ └── injectSafeMode.ts │ └── sourcemap │ │ └── index.ts ├── global.ts ├── options │ ├── options.ts │ └── tuic_logo_gray.svg └── popup │ └── tuic_logo_gray.svg ├── third-party └── sourcemap │ ├── .gitignore │ ├── dist │ ├── sourcemap_js_bg.wasm │ ├── sourcemap_js_bg.wasm.d.ts │ └── sourcemap_js.d.ts │ ├── README.md │ ├── src │ ├── main.rs │ └── lib.rs │ └── Cargo.toml ├── i18n ├── ig.json ├── th.json ├── ar.json ├── bg.json ├── bn.json ├── ca.json ├── cs.json ├── da.json ├── el.json ├── eu.json ├── fa.json ├── fi.json ├── fil.json ├── fr.json ├── ga.json ├── gl.json ├── gu.json ├── ha.json ├── he.json ├── hi.json ├── hr.json ├── hu.json ├── id.json ├── it.json ├── kn.json ├── mr.json ├── ms.json ├── nl.json ├── pl.json ├── pt.json ├── sk.json ├── sr.json ├── sv.json ├── ta.json ├── ur.json ├── vi.json ├── yo.json ├── ro.json ├── ar-x-fm.json ├── nb.json ├── en-ss.json ├── en-xx.json ├── en-GB.json ├── es.json ├── _langList.ts ├── tr.json ├── de.json └── _officialTwitterI18nConfig.ts ├── pnpm-workspace.yaml ├── public ├── icon │ ├── header.png │ ├── icon128.png │ ├── icon16.png │ ├── icon48.png │ ├── newIcon_TUIC_C_Blue.png │ └── newIcon_TUIC_C_Blue.svg ├── inject.js └── safemode.html ├── .gitignore ├── .prettierrc.js ├── crowdin.yml ├── tsconfig.json ├── tsconfig.node.json ├── .vscode ├── extensions.json └── settings.json ├── scripts ├── pwa-manifest │ └── generate-manifest.ts ├── generate-crx.ts ├── change-manifest.ts └── vite-plugin │ └── unocss.ts ├── _locales └── en-GB │ └── messages.json ├── tsconfig.main.json ├── .stylelintrc.js ├── docs ├── manifest_build.md ├── vite_build.md └── i18n.md ├── LICENSE ├── .env.local.example ├── .github └── workflows │ ├── i18nUpdate.yml │ └── lint.yml ├── uno.config.ts ├── CONTRIBUTING.md └── package.json /src/content/styles/uno.css: -------------------------------------------------------------------------------- 1 | @unocss; 2 | -------------------------------------------------------------------------------- /third-party/sourcemap/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /i18n/ig.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "イボ語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/th.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "タイ語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "アラビア語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/bg.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "ブルガリア語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/bn.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "ベンガル語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/ca.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "カタロニア語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/cs.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "チェコ語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/da.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "デンマーク語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/el.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "ギリシャ語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/eu.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "バスク語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/fa.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "ペルシア語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/fi.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "フィンランド語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/fil.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "フィリピノ語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "フランス語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/ga.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "アイルランド語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/gl.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "ガリシア語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/gu.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "グジャラート語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/ha.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "ハウサ語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/he.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "ヘブライ語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/hi.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "ヒンディー語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/hr.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "クロアチア語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/hu.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "ハンガリー語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/id.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "インドネシア語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "イタリア語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/kn.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "カンナダ語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/mr.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "マラーティー語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/ms.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "マレー語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "オランダ語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "ポーランド語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "ポルトガル語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/sk.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "スロバキア語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/sr.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "セルビア語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/sv.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "スウェーデン語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/ta.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "タミル語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/ur.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "ウルドゥー語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/vi.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "ベトナム語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/yo.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "ヨルバ語" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/ro.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "ルーマニア語・モルドバ語" 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | minimumReleaseAge: '10080' # in minutes (7 days) 2 | -------------------------------------------------------------------------------- /i18n/ar-x-fm.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "アラビア語(女性形)" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/nb.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "ノルウェー語[ブークモール]" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/en-ss.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "アメリカ英語 (翻訳キー確認用?)" 3 | } 4 | -------------------------------------------------------------------------------- /i18n/en-xx.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "アメリカ英語 (テスト用?)" 3 | } 4 | -------------------------------------------------------------------------------- /src/shared/testError.ts: -------------------------------------------------------------------------------- 1 | export function throwTestError() { 2 | throw new Error("Test"); 3 | } 4 | -------------------------------------------------------------------------------- /i18n/en-GB.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "イギリス英語", 3 | "profileSetting-profile": "Profile" 4 | } 5 | -------------------------------------------------------------------------------- /public/icon/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ablaze-MIRAI/Twitter-UI-Customizer/HEAD/public/icon/header.png -------------------------------------------------------------------------------- /public/icon/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ablaze-MIRAI/Twitter-UI-Customizer/HEAD/public/icon/icon128.png -------------------------------------------------------------------------------- /public/icon/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ablaze-MIRAI/Twitter-UI-Customizer/HEAD/public/icon/icon16.png -------------------------------------------------------------------------------- /public/icon/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ablaze-MIRAI/Twitter-UI-Customizer/HEAD/public/icon/icon48.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | *.xpi 3 | /manifest.json 4 | /node_modules/ 5 | /dist/ 6 | .env.local 7 | dependency-graph.svg 8 | -------------------------------------------------------------------------------- /src/content/modules/settings/safemode/isSafemode.ts: -------------------------------------------------------------------------------- 1 | export const isSafemode = location.pathname === "/tuic/safemode"; 2 | -------------------------------------------------------------------------------- /public/inject.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | const src = chrome.runtime.getURL("index.js"); 3 | await import(src); 4 | })(); 5 | -------------------------------------------------------------------------------- /src/content/icons/logo/dog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ablaze-MIRAI/Twitter-UI-Customizer/HEAD/src/content/icons/logo/dog.png -------------------------------------------------------------------------------- /public/icon/newIcon_TUIC_C_Blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ablaze-MIRAI/Twitter-UI-Customizer/HEAD/public/icon/newIcon_TUIC_C_Blue.png -------------------------------------------------------------------------------- /third-party/sourcemap/dist/sourcemap_js_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ablaze-MIRAI/Twitter-UI-Customizer/HEAD/third-party/sourcemap/dist/sourcemap_js_bg.wasm -------------------------------------------------------------------------------- /src/shared/tlui/components/Component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * コンポーネント 3 | */ 4 | export interface Component { 5 | /** 6 | * コンポーネントの実要素 7 | */ 8 | element: Element; 9 | } 10 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | export default { 3 | tabWidth: 4, 4 | semi: true, 5 | singleQuote: false, 6 | endOfLine: "auto", 7 | printWidth: 300, 8 | }; 9 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /i18n/ja.json 3 | translation: /i18n/%two_letters_code%.json 4 | - source: /_locales/ja/messages.json 5 | translation: /_locales/%locale%/messages.json 6 | -------------------------------------------------------------------------------- /src/content/icons/logo/empty.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/content/icons/arrow/arrow_down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/content/icons/arrow/arrow_left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/content/icons/arrow/arrow_right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/content/icons/arrow/arrow_up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /third-party/sourcemap/README.md: -------------------------------------------------------------------------------- 1 | # sourcemap-js 2 | 3 | how to build 4 | 5 | install wasm-pack 6 | run: 7 | wasm-pack build --target web 8 | 9 | and use pkg/ 10 | 11 | Please move pkg/ files to dist (without .gitignore in pkg/) to use in TUIC. 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "include": [], 4 | "references": [ 5 | { 6 | "path": "tsconfig.main.json", 7 | }, 8 | { 9 | "path": "tsconfig.node.json", 10 | }, 11 | ], 12 | } 13 | -------------------------------------------------------------------------------- /third-party/sourcemap/src/main.rs: -------------------------------------------------------------------------------- 1 | use sourcemap_js::NRSourceMap; 2 | 3 | fn main() { 4 | println!( 5 | "{:?}", 6 | NRSourceMap::new(&String::from_utf8(include_bytes!("../srcmap.js.map").to_vec()).unwrap()) 7 | .lookup(1, 710) 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/content/styles/index.css: -------------------------------------------------------------------------------- 1 | @import url("./style-polyfillForTwitter.css"); 2 | @import url("./style-tlui.css"); 3 | @import url("./style-tuicColor.css"); 4 | @import url("./style-tuicFeatures.css"); 5 | @import url("./style-tuicSettingPage.css"); 6 | @import url("./firefox/style-compat.css"); 7 | -------------------------------------------------------------------------------- /src/shared/settings/components/SectionTitle.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /src/shared/settings/components/SectionTitle2.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /src/shared/settings/components/settingSubtitle2.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /src/shared/settings/components/settingSubTitleNomargin.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /src/shared/options/store.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | 3 | // editingColorType : 指定する色のタイプがベース / ダーク / ライトどれで使用されるかを保持 4 | export const useStore = defineStore("optionMain", { 5 | state: () => 6 | ({ 7 | editingColorType: "buttonColor", 8 | }) as { 9 | editingColorType: "buttonColor" | "buttonColorLight" | "buttonColorDark"; 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /third-party/sourcemap/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sourcemap-js" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | #when testing with main.rs, commentout these two lines 9 | [lib] 10 | crate-type = ["cdylib", "rlib"] 11 | 12 | [dependencies] 13 | sourcemap = "8.0.1" 14 | wasm-bindgen = "0.2.92" 15 | -------------------------------------------------------------------------------- /public/safemode.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/shared/settings/modules/settingTimeline.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | -------------------------------------------------------------------------------- /src/shared/options/components/RadioButtonList.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/shared/options/components/IconButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/content/icons/arrow/reset.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/content/modules/utils/getValues.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 関数の場合は返り値、そうでない場合は引数そのままを返します。 3 | * 4 | * @param {unknwon} functionOrPrimitive 関数もしくはそうでない値 5 | * @return {unknwon} 返り値もしくはそのままの値 6 | */ 7 | export function getPrimitiveOrFunction(functionOrPrimitive: (() => T) | T): T { 8 | if (typeof functionOrPrimitive === "function") { 9 | return (functionOrPrimitive as () => T)(); 10 | } else { 11 | return functionOrPrimitive; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/shared/options/components/CheckBoxList.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/shared/options/components/textParts/settingSubTitle.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/shared/settings/components/IconButton.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["i18n", "scripts", "*.ts"], 3 | "compilerOptions": { 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "target": "ESNext", 7 | "lib": ["ESNext"], 8 | "noEmit": true, 9 | "skipLibCheck": true, 10 | "types": ["@types/node"], 11 | "allowImportingTsExtensions": true, 12 | "erasableSyntaxOnly": true, 13 | "verbatimModuleSyntax": true, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /src/content/icons/brand/x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/global.ts: -------------------------------------------------------------------------------- 1 | type I18n = Record; 2 | 3 | interface I18nAndAllContent { 4 | all: string[]; 5 | i18n: I18n; 6 | [key: string]: unknown; 7 | } 8 | 9 | interface ArticleInfomation { 10 | elements: { buttonBarBase: HTMLDivElement; articleBase: HTMLElement; statusButton: HTMLAnchorElement }; 11 | option: { 12 | isLockedAccount: boolean; 13 | cannotRT: boolean; 14 | cannotShare: boolean; 15 | isMe: boolean; 16 | isBigArticle: boolean; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /i18n/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "スペイン語(カスティーリャ語)", 3 | "settingUI-goBackButton": "Atrás", 4 | "settingUI-easySetting": "Es tu primera vez personalizando?", 5 | "settingUI-easySetting-detail": "¡Te preparamos unos ajustes sugeridos!", 6 | "settingUI-everythingSetting": "¡Vamos a personalizarlo a tu gusto!", 7 | "settingUI-restoreDefaultAll": "Restaurar valores predeterminados", 8 | "settingUI-restoreDefaultAll-confirm": "¿Seguro que deseas devolver la configuración a su estado original?" 9 | } 10 | -------------------------------------------------------------------------------- /src/content/styles/firefox/style-compat.css: -------------------------------------------------------------------------------- 1 | /** ダイレクトメッセージが使えなくなる問題を修正 **/ 2 | 3 | /* https://github.com/Floorp-Projects/Floorp-core/blob/4433faa42c63eb32fdcbaa94747cb31702a45bcd/browser/extensions/webextensions/floorp-system/shared/webcompat/bug-894-twitter-com.css 4 | * ライセンス変更含め使用許諾済み */ 5 | 6 | main div > span { 7 | line-height: normal !important; 8 | } 9 | 10 | /* こっから自分で書いたコード */ 11 | 12 | [data-testid="messageEntry"] > div > div ~ div:not(.TUICOriginalContent) > div:not([role="presentation"]) { 13 | margin-bottom: 0; 14 | } 15 | -------------------------------------------------------------------------------- /src/content/modules/settings/safemode/safemode.ts: -------------------------------------------------------------------------------- 1 | import { injectSafeMode } from "@shared/options/injectSafeMode"; 2 | 3 | export function runSafemode() { 4 | document.querySelector("#TUIC_safemode")?.remove(); 5 | document.querySelector(".twitter_ui_customizer_css")?.remove(); 6 | document.querySelector("#react-root").style.display = "none"; 7 | 8 | const entry = document.createElement("div"); 9 | entry.id = "TUICOptionSafemodeEntry"; 10 | document.body.appendChild(entry); 11 | 12 | injectSafeMode(); 13 | } 14 | -------------------------------------------------------------------------------- /src/content/icons/logo/x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/shared/options/injectOption2Entry.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import optionMain from "../settings/SettingMain.ce.vue"; 3 | import { createPinia } from "pinia"; 4 | 5 | export const injectOptionMain = () => { 6 | if (optionMain.styles !== undefined) { 7 | const style = document.createElement("style"); 8 | style.textContent = optionMain.styles; 9 | document.head.appendChild(style); 10 | } 11 | 12 | const app = createApp(optionMain); 13 | app.use(createPinia()); 14 | app.mount("#TUICOptionEntry"); 15 | }; 16 | -------------------------------------------------------------------------------- /src/content/modules/settings/display.ts: -------------------------------------------------------------------------------- 1 | import { injectOptionMain } from "@shared/options/injectOption2Entry"; 2 | 3 | export function displaySetting(rootElement: HTMLElement) { 4 | //document.querySelector("#TUICOptionMain"); 5 | if (!document.querySelector("#TUIC_setting")) { 6 | if (document.querySelector("#TUICOptionEntry") == null) { 7 | const div = document.createElement("div"); 8 | div.id = "TUICOptionEntry"; 9 | rootElement.appendChild(div); 10 | } 11 | injectOptionMain(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/shared/options/components/IconRadioButtonList.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/shared/settings/modules/settingDM.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": [ 7 | "dbaeumer.vscode-eslint", 8 | "esbenp.prettier-vscode", 9 | "stylelint.vscode-stylelint", 10 | "vue.volar", 11 | "antfu.unocss", 12 | ], 13 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 14 | "unwantedRecommendations": [ 15 | ], 16 | } 17 | -------------------------------------------------------------------------------- /src/shared/settings/components/ColorResetButton.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | -------------------------------------------------------------------------------- /src/shared/settings/modules/settingUncategorized.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /src/content/styles/style-polyfillForTwitter.css: -------------------------------------------------------------------------------- 1 | .r-icoktb { 2 | opacity: 0.5; 3 | } 4 | 5 | .r-zd98yo { 6 | margin-bottom: 32px; 7 | } 8 | 9 | .r-1yflyrw { 10 | margin-bottom: 30px; 11 | } 12 | 13 | .r-1vxqurs { 14 | margin-bottom: 29px; 15 | } 16 | 17 | .r-1v456y7 { 18 | margin-bottom: 35px; 19 | } 20 | 21 | .r-sr82au { 22 | margin-bottom: 38px; 23 | } 24 | 25 | .r-z2wwpe { 26 | border-radius: 4px; 27 | } 28 | 29 | .r-115tad6 { 30 | color: rgb(139 152 165); 31 | } 32 | 33 | /* Twitter Blue以外の長文のもっと見るを非表示にするCSS */ 34 | div[data-testid="tweetText"] { 35 | -webkit-line-clamp: initial !important; 36 | } 37 | /* 38 | header nav > a > div { 39 | padding-right: 0 !important; 40 | } 41 | */ 42 | -------------------------------------------------------------------------------- /src/shared/tlui/components/ContainerComponent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "./Component"; 2 | 3 | /** 4 | * コンテナ 5 | */ 6 | export class ContainerComponent implements Component { 7 | public element: Element; 8 | 9 | /** 10 | * @param elements 内包する要素 11 | */ 12 | constructor(elements: (Node | Component)[]) { 13 | this.element = document.createElement("div"); 14 | this.element.classList.add("tlui-container"); 15 | 16 | for (const element of elements) { 17 | if (element instanceof Node) { 18 | this.element.appendChild(element); 19 | } else { 20 | this.element.appendChild(element.element); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/content/icons/common/arrow_right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /scripts/pwa-manifest/generate-manifest.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | 3 | import { pwaManifest } from "./pwa-manifest.config.ts"; 4 | 5 | export const generatePWAManifest = async (locale: string, ti18n: Record) => { 6 | await fs.writeFile( 7 | `public/pwa-manifests/${locale}.json`, 8 | JSON.stringify(pwaManifest( 9 | ti18n["XtoTwitter-PostToTweet-pwaManifest-description"], 10 | ti18n["XtoTwitter-PostToTweet-pwaManifest-newTweet"], 11 | ti18n["XtoTwitter-PostToTweet-pwaManifest-explore"], 12 | ti18n["XtoTwitter-PostToTweet-pwaManifest-notifications"], 13 | ti18n["XtoTwitter-PostToTweet-pwaManifest-directMessages"], 14 | ), null, 2), 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /_locales/en-GB/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Twitter UI Customizer", 4 | "description": "拡張機能の名前" 5 | }, 6 | "extensionDescription": { 7 | "message": "Improve the customizability of your Twitter UI", 8 | "description": "拡張機能の説明" 9 | }, 10 | "notificationMessage": { 11 | "message": "A new version is now available. Please update.", 12 | "description": "アップデート通知", 13 | "placeholders": { 14 | "V1": { 15 | "content": "$1", 16 | "example": "x.x.x (Extension)" 17 | }, 18 | "V2": { 19 | "content": "$2", 20 | "example": "x.x.x (GitHub)" 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/content/icons/common/arrow_right_enabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /scripts/generate-crx.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | 4 | import ChromeExtenson from "crx"; 5 | 6 | async function generateCRX() { 7 | const crx = new ChromeExtenson({ 8 | codebase: `https://github.com/${process.env.GITHUB_REPO}/releases/latest/download/Twitter_UI_Customizer_Chromium.crx`, 9 | privateKey: process.env.CHROME_EXTENSION_KEY, 10 | }); 11 | 12 | await crx.load(path.resolve("dist")); 13 | const crxBuffer = await crx.pack(); 14 | fs.writeFileSync("crxupdate.xml", crx.generateUpdateXML()); 15 | fs.writeFileSync("Twitter_UI_Customizer_Chromium.crx", crxBuffer); 16 | console.log("\x1b[32m✓\x1b[0m CRX generated."); 17 | } 18 | 19 | if (process.argv[1] === import.meta.filename) { 20 | await generateCRX(); 21 | } 22 | -------------------------------------------------------------------------------- /src/content/modules/observer/functions/profile/index.ts: -------------------------------------------------------------------------------- 1 | import { getPref } from "@content/modules/pref"; 2 | import { followersList } from "./followersListButton"; 3 | import { profileInitialTab } from "./initProfileTab"; 4 | import { TUICI18N } from "@content/modules/i18n"; 5 | 6 | export function profileModify() { 7 | // フォロワー一覧のボタンについての処理 8 | followersList(); 9 | 10 | // プロフィール画面を最初に開いたときのタブについての処理 11 | profileInitialTab(); 12 | 13 | // 「返信」タブを「ツイートと返信」の名称に戻す 14 | if (getPref("profileSetting.tabs.changeNameReplies")) { 15 | const repliesTabElement = document.querySelector(`[role="navigation"] [href$="/with_replies"] span`); 16 | if (repliesTabElement) { 17 | repliesTabElement.textContent = TUICI18N.get("profileSetting-changeName-replies-oldName"); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/content/icons/common/github.svg: -------------------------------------------------------------------------------- 1 | GitHub -------------------------------------------------------------------------------- /src/shared/options/modules/customCSS.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | -------------------------------------------------------------------------------- /src/shared/settings/modules/SettingsHeader.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/content/icons/common/sidebar_enabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tsconfig.main.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["i18n/_langList.ts", "public", "src", "third-party"], 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "jsxImportSource": "solid-js", 6 | "module": "ESNext", 7 | "moduleResolution": "Bundler", 8 | "target": "ES2023", 9 | "lib": ["ES2023", "DOM", "DOM.Iterable"], 10 | "esModuleInterop": true, 11 | "isolatedModules": true, 12 | "skipLibCheck": true, 13 | "noEmit": true, 14 | "types": ["@types/chrome", "vite/client", "vite-svg-loader"], 15 | "paths": { 16 | "@content/*": ["./src/content/*"], 17 | "@shared/*": ["./src/shared/*"], 18 | "@modules/*": ["./src/content/modules/*"], 19 | "@i18nData/*": ["./i18n/*"], 20 | "@third-party/*": ["./third-party/*"], 21 | }, 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /src/content/icons/common/reveal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/content/styles/features/style-discoverMore.css: -------------------------------------------------------------------------------- 1 | :is([data-tuic-discover-more="open"], [data-tuic-discover-more="close"]) { 2 | padding-left: 20px; 3 | } 4 | 5 | :is([data-tuic-discover-more="open"], [data-tuic-discover-more="close"])::before { 6 | position: absolute; 7 | top: 50%; 8 | left: 20px; 9 | content: ""; 10 | } 11 | 12 | [data-tuic-discover-more="close"]::before { 13 | border: 5px solid transparent; 14 | border-left: 8px solid #555; 15 | transform: translateY(-50%); 16 | } 17 | 18 | [data-tuic-discover-more="open"]::before { 19 | border: 6px solid transparent; 20 | border-top: 7px solid #555; 21 | } 22 | [data-tuic-discover-more="invisible"] { 23 | display: none !important; 24 | } 25 | 26 | :is([data-tuic-discover-more="close"], [data-tuic-discover-more="invisible"]) ~ [data-tuic-discover-more-tweet] { 27 | display: none !important; 28 | } 29 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("stylelint").Config} */ 2 | export default { 3 | extends: [ 4 | "stylelint-config-standard", 5 | "stylelint-config-recommended-vue", 6 | "stylelint-config-recess-order", 7 | ], 8 | rules: { 9 | // https://stylelint.io/user-guide/rules 10 | "at-rule-no-unknown": null, 11 | "comment-empty-line-before": null, 12 | "property-no-vendor-prefix": null, 13 | "rule-empty-line-before": null, 14 | "no-descending-specificity": null, 15 | "declaration-empty-line-before": null, 16 | "no-empty-source": null, 17 | 18 | // kebab-case 19 | "custom-property-pattern": null, 20 | "selector-id-pattern": null, 21 | "selector-class-pattern": null, 22 | "keyframes-name-pattern": null, 23 | }, 24 | ignoreFiles: ["dist/**/*", "node_modules/**/*"], 25 | }; 26 | -------------------------------------------------------------------------------- /src/content/icons/common/pencil_enabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/content/icons/common/information.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/shared/options/components/ColorsList.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /i18n/_langList.ts: -------------------------------------------------------------------------------- 1 | export type Locale = string; 2 | export const langList: Locale[] = [ 3 | "ar-x-fm", 4 | "ar", 5 | "bg", 6 | "bn", 7 | "ca", 8 | "cs", 9 | "da", 10 | "de", 11 | "el", 12 | "en-GB", 13 | "en-ss", 14 | "en-xx", 15 | "en", 16 | "es", 17 | "eu", 18 | "fa", 19 | "fi", 20 | "fil", 21 | "fr", 22 | "ga", 23 | "gl", 24 | "gu", 25 | "ha", 26 | "he", 27 | "hi", 28 | "hr", 29 | "hu", 30 | "id", 31 | "ig", 32 | "it", 33 | "ja", 34 | "kn", 35 | "ko", 36 | "mr", 37 | "ms", 38 | "nb", 39 | "nl", 40 | "pl", 41 | "pt", 42 | "ro", 43 | "ru", 44 | "sk", 45 | "sr", 46 | "sv", 47 | "ta", 48 | "th", 49 | "tr", 50 | "uk", 51 | "ur", 52 | "vi", 53 | "yo", 54 | "zh-Hant", 55 | "zh", 56 | ]; 57 | 58 | export default langList; 59 | -------------------------------------------------------------------------------- /src/content/icons/arrow_cutter/arrow_down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/content/icons/arrow_cutter/arrow_up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/content/icons/arrow_cutter/arrow_toleft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/content/icons/arrow_cutter/arrow_toright.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/content/icons/common/more.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/content/icons/common/profile_enabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/shared/options/components/ThreeColorSetting.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/content/icons/common/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/content/modules/utils/fontSize.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Twitterの「フォントサイズ」に基づいて値を返します。 3 | * 4 | * @template {number | string} T 5 | * @param {T} x1 フォントサイズが一番小さいときの返り値(設定画面では左端) 6 | * @param {T} x2 フォントサイズが二番目に小さいときの返り値 7 | * @param {T} x3 フォントサイズが三番目に小さいときの返り値 8 | * @param {T} x4 フォントサイズが四番目に小さいときの返り値 9 | * @param {T} x5 フォントサイズが五番目に小さいときの返り値 10 | * @return {T} 返り値もしくはそのままの値 11 | */ 12 | export function fontSizeClass(x1: T, x2: T, x3: T, x4: T, x5: T) { 13 | const fontSize = document.querySelector("html").style.fontSize.toString(); 14 | switch (fontSize) { 15 | case "14px": 16 | return document.querySelector(`h1[role="heading"] > a[href="/home"]`)?.className.includes("r-116um31") ? x1 : x2; 17 | case "15px": 18 | return x3; 19 | case "17px": 20 | return x4; 21 | case "18px": 22 | return x5; 23 | default: 24 | return x3; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/content/icons/common/sidebar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/content/modules/observer/functions.ts: -------------------------------------------------------------------------------- 1 | import { sidebarButtons } from "./functions/sidebarBtn"; 2 | import { dmPage } from "./functions/fixDM"; 3 | import { replacePost } from "./functions/replacePostWithTweet"; 4 | import { fixTwittersBugs } from "./functions/fixTwittersBugs"; 5 | import { hideElements } from "./functions/hideElements"; 6 | import { updateStyles } from "./functions/updateStyles"; 7 | import { hideOsusumeTweets } from "./functions/hideOsusumeTweets"; 8 | import { changeIcon } from "./functions/changeIcon"; 9 | import { tweetSettings } from "./functions/tweetSettings"; 10 | import { profileModify } from "./functions/profile"; 11 | import { sortPostingDialogButtons } from "./functions/postingDialog"; 12 | import { composetweet } from "./functions/composeTweet"; 13 | 14 | export { tweetSettings, hideOsusumeTweets, replacePost, hideElements, updateStyles, sidebarButtons, dmPage, fixTwittersBugs, changeIcon, profileModify, sortPostingDialogButtons, composetweet }; 15 | -------------------------------------------------------------------------------- /src/content/icons/common/dm_enabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/shared/settings/components/RoundedColorPicker.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 30 | -------------------------------------------------------------------------------- /src/content/icons/common/home_enabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/content/icons/common/sparkles_enabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /third-party/sourcemap/dist/sourcemap_js_bg.wasm.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export const memory: WebAssembly.Memory; 4 | export function __wbg_nrsourcemap_free(a: number): void; 5 | export function __wbg_nrtoken_free(a: number): void; 6 | export function __wbg_get_nrtoken_line(a: number): number; 7 | export function __wbg_set_nrtoken_line(a: number, b: number): void; 8 | export function __wbg_get_nrtoken_col(a: number): number; 9 | export function __wbg_set_nrtoken_col(a: number, b: number): void; 10 | export function nrsourcemap_new(a: number, b: number): number; 11 | export function nrsourcemap_lookup(a: number, b: number, c: number): number; 12 | export function nrtoken_source(a: number, b: number): void; 13 | export function __wbindgen_malloc(a: number, b: number): number; 14 | export function __wbindgen_realloc(a: number, b: number, c: number, d: number): number; 15 | export function __wbindgen_add_to_stack_pointer(a: number): number; 16 | export function __wbindgen_free(a: number, b: number, c: number): void; 17 | -------------------------------------------------------------------------------- /src/content/icons/common/brush.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/shared/settings/modules/settingProfile.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 23 | -------------------------------------------------------------------------------- /src/content/icons/common/none.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/manifest_build.md: -------------------------------------------------------------------------------- 1 | # manifest.jsonについて 2 | 3 | このリポジトリ、拡張機能なのにmanifest.jsonが含まれていません 4 | なぜなら、chromiumとfirefoxではmanifest.jsonの中身が違うため、どちらかを拾ったらどちらかを捨てないといけないからです 5 | なので、manifest.jsonを召喚するコマンドがあります 6 | (Node.jsを使うので、Node.jsを宗教上の理由で使えないという方は`/manifest.config.ts`を見て自力で合成してください) 7 | 8 | ## manifest.json生成スクリプト 9 | 10 | `node ./scripts/manifestChange.ts <引数>` 11 | で使います 12 | 引数によってどのmanifest.jsonを使うかが変わります 13 | 以下のとおりです 14 | `chromium`:Chromium系ブラウザ向けのmanifest.jsonです 15 | `chromiumCRX`:GitHubのReleaseで公開しているCRX用のmanifest.jsonです 16 | `firefox`:firefox系ブラウザ向けのmanifest.jsonです 17 | 18 | ### しくみ 19 | 20 | `/manifest.config.ts`の中には、`common` `firefox` `chromium` `chromiumCRX`というkeyが存在しています 21 | `common`には必ず使うmanifest.jsonの要素、それ以外には、何向けかによって内容が変わるmanifest.jsonの要素になっています 22 | 第一引数によって、どのObjectを取得統合するかを変えています(`chromiumCRX`だけは特殊で`chromium`も取得統合している) 23 | 24 | ## manifest.jsonの加え方 25 | 26 | `.gitignore`にmanifest.jsonを入れているので、manifest.jsonの変更ではgitに適用できません 27 | ではどうするか。`/manifest.config.ts`を変更します 28 | やり方は...見たらわかると思います(おい) 29 | 上の「しくみ」も見ればわかりやすいと思います 30 | -------------------------------------------------------------------------------- /src/content/icons/common/tweet_enabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/shared/options/components/CheckBox.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/content/icons/brand/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/content/icons/common/reset.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/shared/options/scripts/changePrefScript.ts: -------------------------------------------------------------------------------- 1 | import { isSafemode } from "@modules/settings/safemode/isSafemode"; 2 | import { getPref, setPref, savePref, mergePref } from "@modules/pref"; 3 | import { titleObserverFunction } from "@modules/observer/titleObserver"; 4 | import { updateClasses } from "@modules/htmlClass/classManager"; 5 | 6 | export const XToTwitterRestoreIcon = () => { 7 | const importPref = { 8 | sidebarSetting: { buttonConfig: { birdGoBack: true } }, 9 | twitterIcon: { 10 | options: { 11 | faviconSet: true, 12 | }, 13 | icon: "twitter", 14 | }, 15 | }; 16 | setPref("", mergePref(getPref(""), importPref)); 17 | savePref(); 18 | updateClasses(); 19 | titleObserverFunction(); 20 | if (!isSafemode) { 21 | document.querySelector("#TUIC_setting").remove(); 22 | } 23 | if (!getPref("XToTwitter.XToTwitter") && document.title.endsWith(" / Twitter")) { 24 | document.title = document.title.replace(" / Twitter", " / X"); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/content/icons/common/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/shared/options/components/RadioButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 25 | -------------------------------------------------------------------------------- /src/shared/options/components/IconRadioButtonBase64Support.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 25 | -------------------------------------------------------------------------------- /src/shared/options/components/IconRadioButton.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 26 | -------------------------------------------------------------------------------- /src/shared/options/injectSafeMode.ts: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/questions/42800035/why-cant-you-create-custom-elements-in-content-scripts 2 | // import "@webcomponents/custom-elements"; 3 | 4 | import { createApp } from "vue"; 5 | import safemodeVue from "./SafeMode.ce.vue"; 6 | import { createPinia } from "pinia"; 7 | 8 | export const injectSafeMode = () => { 9 | // styles, not style 10 | if (safemodeVue.styles !== undefined) { 11 | const style = document.createElement("style"); 12 | style.textContent = safemodeVue.styles; 13 | document.head.appendChild(style); 14 | } 15 | 16 | const app = createApp(safemodeVue); 17 | app.use(createPinia()); 18 | app.mount("#TUICOptionSafemodeEntry"); 19 | console.log("injectSafemode End"); 20 | }; 21 | 22 | // TUICI18N.fetch().then(() => { 23 | // in Twitter, occurs bugs abt CustomElement 24 | 25 | // { 26 | // const ce = defineCustomElement(safemodeVue); 27 | // customElements.define("tuic-option-entry", ce); 28 | // } 29 | 30 | //document.querySelector("#TUICOptionSafemodeMain").appendChild(new ce({})); 31 | // }); 32 | -------------------------------------------------------------------------------- /src/content/icons/logo/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/content/icons/common/dm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/content/icons/common/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/content/icons/common/profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2025 kaonasi-biwa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/content/modules/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { langList } from "@i18nData/_langList"; 2 | 3 | const langRes = import.meta.glob(["@i18nData/*.json", "@i18nData/ti18n/*.json"]); 4 | const i18nData = { en: {}, ja: {} }; 5 | 6 | export const TUICI18N = { 7 | fetch: async () => { 8 | for (const elem of langList) { 9 | i18nData[elem] = Object.assign( 10 | (await langRes[`../i18n/${elem}.json`]() as { default: object }).default, 11 | (await langRes[`../i18n/ti18n/${elem}.json`]() as { default: object }).default, 12 | ); 13 | if (elem.includes("ja")) { 14 | console.log(i18nData[elem]); 15 | } 16 | } 17 | return true; 18 | }, 19 | get: (key: string, selectLang?: string) => { 20 | const lang = selectLang ?? document.querySelector("html").getAttribute("lang"); 21 | for (const _lang of [lang, "en", "ja"]) { 22 | if (_lang in i18nData && key in i18nData[_lang]) { 23 | return i18nData[_lang][key]; 24 | } 25 | } 26 | 27 | return `TUIC 404: ${key}`; 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /src/content/icons/home/home_twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/shared/tlui/components/DivBox.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "./Component"; 2 | 3 | export interface DivBoxComponentInit { 4 | /** 5 | * DivのID(初期値: `undefined`) 6 | */ 7 | id?: string; 8 | } 9 | 10 | /** 11 | * Twitterのボタン風要素 12 | */ 13 | export class DivBoxComponent implements Component { 14 | public element: HTMLDivElement; 15 | 16 | /** 17 | * @param options オプション 18 | */ 19 | constructor(options: DivBoxComponentInit = {}) { 20 | this.element = new DOMParser().parseFromString( 21 | ` 22 |
23 | `, 24 | "text/html", 25 | ).body.children[0] as HTMLDivElement; 26 | 27 | if (options.id) { 28 | this.element.id = options.id; 29 | } 30 | } 31 | 32 | /** 33 | * ボタンの幅を最大にするかどうか(初期値: `true`) 34 | */ 35 | public get fullWidth(): string { 36 | return this.element.id; 37 | } 38 | public set fullWidth(value: string) { 39 | if (value) { 40 | this.element.id = value; 41 | } else { 42 | this.element.removeAttribute("id"); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/content/icons/home/home_x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/shared/tlui/observer.ts: -------------------------------------------------------------------------------- 1 | import { waitForElement } from "@modules/utils/controlElements"; 2 | 3 | /** 4 | * TLUI のオブザーバーを開始します。 5 | */ 6 | export function startTluiObserver() { 7 | async function changedTheme() { 8 | document.documentElement.style.setProperty("--tlui-dialog-background", document.body.style.backgroundColor); 9 | document.documentElement.style.setProperty("--tlui-dialog-text", getComputedStyle((await waitForElement("span"))[0]).color); 10 | } 11 | 12 | new MutationObserver(changedTheme).observe(document.body, { attributes: true, attributeFilter: ["style"] }); 13 | changedTheme(); 14 | } 15 | 16 | /* 17 | 18 | await waitForElement("#layers"); 19 | const dialog = new Dialog("Hello!"); 20 | dialog.addComponents([ 21 | "こんな感じで簡単にダイアログを出せるようになりました。", 22 | "いい感じのAPIにしたつもりなのですが、もしここが使いにくいとかあれば言ってくださいね。", 23 | new ButtonComponent("ふぁみちゃんだいすき", () => dialog.close()), 24 | new ButtonComponent("閉じる", () => dialog.close(), { 25 | invertColor: true 26 | }), 27 | new ContainerComponent([ 28 | new ButtonComponent("第三の選択肢!", () => dialog.close(), { 29 | invertColor: true 30 | }) 31 | ]) 32 | ]).open(); 33 | 34 | */ 35 | -------------------------------------------------------------------------------- /src/content/modules/observer/functions/fixTwittersBugs.ts: -------------------------------------------------------------------------------- 1 | let fixedDMBox = false; 2 | 3 | export function fixTwittersBugs() { 4 | if (!fixedDMBox) { 5 | const dmBox = document.querySelector(`[data-testid="DMDrawerHeader"]`); 6 | if (dmBox) { 7 | if (dmBox.querySelector(`[d="M12 11.59L3.96 3.54 2.54 4.96 12 14.41l9.46-9.45-1.42-1.42L12 11.59zm0 7l-8.04-8.05-1.42 1.42L12 21.41l9.46-9.45-1.42-1.42L12 18.59z"]`)) { 8 | dmBox.querySelector(`div[role="button"]+div[role="button"]`)?.click(); 9 | window.setTimeout(() => { 10 | document.querySelector(`[data-testid="DMDrawerHeader"] div[role="button"]+div[role="button"]`)?.click(); 11 | }, 100); 12 | } 13 | fixedDMBox = true; 14 | } 15 | } 16 | 17 | for (const elem of document.querySelectorAll(`[data-testid="messageEntry"] > div > div~div:not(.TUICOriginalContent) > div:not([role="presentation"]) 18 | :is(.r-1sv84sj, .r-144un9c, .r-11rk87y, .r-1b5uinu, .r-l3vzaz)`)) { 19 | elem.classList.remove("r-1sv84sj", "r-144un9c", "r-11rk87y", "r-1b5uinu", "r-l3vzaz"); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/content/icons/common/sparkles.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/shared/settings/components/TransparentToggleButton.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 34 | -------------------------------------------------------------------------------- /src/content/icons/common/more_circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/content/icons/common/brush_enabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/content/icons/common/more_circle_enabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/content/icons/common/tweet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/shared/settings/components/detailsBox.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 38 | -------------------------------------------------------------------------------- /third-party/sourcemap/src/lib.rs: -------------------------------------------------------------------------------- 1 | use sourcemap::SourceMap; 2 | use wasm_bindgen::prelude::*; 3 | 4 | #[wasm_bindgen] 5 | pub struct NRSourceMap { 6 | instance: SourceMap, 7 | } 8 | 9 | #[wasm_bindgen] 10 | #[derive(Debug)] 11 | pub struct NRToken { 12 | source: String, 13 | pub line: u32, 14 | pub col: u32, 15 | } 16 | 17 | #[wasm_bindgen] 18 | impl NRSourceMap { 19 | #[wasm_bindgen(constructor)] 20 | pub fn new(sourcemap: &str) -> Self { 21 | //sprintln!("{}", sourcemap); 22 | Self { 23 | instance: SourceMap::from_reader(sourcemap.as_bytes()).unwrap(), 24 | } 25 | } 26 | 27 | /// the line staring with 1, and the col starting with 0 28 | /// If you find a source that not exist in original source, this returns source name as "none" 29 | pub fn lookup(&self, line: u32, col: u32) -> NRToken { 30 | let token = self.instance.lookup_token(line - 1, col); 31 | match token { 32 | Some(token) => NRToken { 33 | source: token.get_source().unwrap().to_string(), 34 | line: token.get_src_line() + 1, 35 | col: token.get_src_col(), 36 | }, 37 | None => NRToken { 38 | source: "none".to_string(), 39 | line, 40 | col, 41 | }, 42 | } 43 | } 44 | } 45 | 46 | #[wasm_bindgen] 47 | impl NRToken { 48 | #[wasm_bindgen(getter)] 49 | pub fn source(&self) -> String { 50 | self.source.clone() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/content/modules/observer/functions/fixDM.ts: -------------------------------------------------------------------------------- 1 | import { render } from "solid-js/web"; 2 | import { IconElement } from "@content/modules/observer/resources/dmIcon"; 3 | import { getPref } from "@modules/pref"; 4 | 5 | export function dmPage() { 6 | if (getPref("dmPage.showIcon")) { 7 | if ( 8 | document.querySelector( 9 | `:is([data-testid="DM_Conversation_Avatar"]:not([data-testid="conversation"] *) [data-testid="UserAvatar-Container-unknown"] [role="presentation"] > div+div+div > div > div > div > div,[data-testid="DmScrollerContainer"] [data-testid="UserAvatar-Container-unknown"]:not([href$="/followers_you_follow"] *) [style*="background-image:"])`, 10 | ) 11 | ) { 12 | for (const elem of document.querySelectorAll(`[data-testid="messageEntry"]:not([role="button"]):not(.TUICDMIcon)`)) { 13 | elem.classList.add("TUICDMIcon"); 14 | if (elem.parentElement.querySelector(`[data-testid="messageEntry"] > div > div+div+div:not(.TUICDMIconBox)`)) { 15 | continue; 16 | } 17 | //old Element 18 | elem.querySelector("div > div+div+div.TUICDMIconBox")?.remove(); 19 | 20 | const elemParent = elem.parentElement.querySelector(`[data-testid="messageEntry"] > div`); 21 | render(IconElement, elemParent); 22 | } 23 | } 24 | } else { 25 | document.querySelectorAll(".TUICDMIconBox").forEach((elem) => { 26 | elem.remove(); 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.env.local.example: -------------------------------------------------------------------------------- 1 | # このファイルをコピーして .env.local にリネームし、 2 | # お使いのオプションに対して値を書き換えた後、 3 | # コメントを外してお使いください。 4 | # また、このファイルの中の変数を環境変数で指定しても同じ効果が得られます。 5 | 6 | ######################## Firefox ######################## 7 | 8 | # Firefox系ブラウザでは、about:profiles でプロファイルのパスを見つけることができます。 9 | # development プロファイルを使いたくない場合や、他のFirefox系ブラウザを使う場合、 10 | # エラーが出る場合などに指定してください。 11 | 12 | #TUIC_WEBEXT_FIREFOX_EXECUTABLE="C:\Program Files\Ablaze Floorp\floorp.exe" 13 | #TUIC_WEBEXT_FIREFOX_PROFILE="C:\Users\user\AppData\Roaming\Floorp\Profiles\4lbtdz2n.dev_tuic" 14 | 15 | 16 | ######################## Chromium ######################## 17 | 18 | # Chromium系ブラウザでは、chrome://version でexeのパスとプロファイルのパスを見つけることができます。 19 | # Chromium系ブラウザの場合、プロファイルのPathが表示される名前と一致しないので、 20 | # プロファイルを作成した後、プロファイルのパスは必ず指定してください。 21 | # **新しくUser Dataフォルダを作成する(か、Chrome for Testingを使用する?)必要があります** 22 | 23 | #TUIC_WEBEXT_CHROMIUM_EXECUTABLE="C:\Program Files\Google\Chrome\Application\chrome.exe" 24 | #TUIC_WEBEXT_CHROMIUM_PROFILE=" C:\Users\user\AppData\Local\Google\Chrome\User Data Debug\Default" 25 | 26 | ######################## 他 ######################## 27 | 28 | # keepProfileChangesをtrueにすると、対象(デフォルトではdevelopment)が、 29 | # プロファイルに変更点が保存されるようになります。 30 | # デフォルトではプロファイルに変更点が保存されません。 31 | 32 | # そして、keepProfileChangesとtrueにすると、 33 | # 指定したプロファイル (firefox系ではデフォルトdevelopment)が 34 | # なぜか、そのブラウザでのデフォルトプロファイルになります。(たまにならない(謎)) 35 | # (Firefox系 及び Chromium系 共通) 36 | 37 | # Chromium系の場合、このオプションを有効にしないとログイン情報がちゃんと読み込まれません。 38 | # このオプションを有効にした後、プロファイルスイッチャーを有効にして使うか、 39 | # Chrome Betaなど他のブラウザを指定することをおすすめします。 40 | 41 | #TUIC_WEBEXT_FIREFOX_KEEP_PROFILE_CHANGES=true 42 | #TUIC_WEBEXT_CHROMIUM_KEEP_PROFILE_CHANGES=true -------------------------------------------------------------------------------- /src/options/options.ts: -------------------------------------------------------------------------------- 1 | import "bootstrap-icons/font/bootstrap-icons.css"; 2 | import { TUICI18N } from "@modules/i18n/index"; 3 | 4 | let setting = {}; 5 | 6 | const i18nApply = async () => { 7 | for (const elem of [...document.querySelectorAll(".i18n-t")]) { 8 | if (elem instanceof HTMLElement) { 9 | elem.title = chrome.i18n.getMessage(elem.getAttribute("i18n-t-id") ?? ""); 10 | } 11 | } 12 | for (const elem of [...document.querySelectorAll(".i18n")]) { 13 | elem.textContent = chrome.i18n.getMessage(elem.getAttribute("i18n-id") ?? ""); 14 | } 15 | }; 16 | 17 | const checkbox = (event) => { 18 | const elem = event.target; 19 | setting[elem.id] = elem.checked; 20 | chrome.storage.sync.set({ TUIC: setting }); 21 | }; 22 | 23 | window.onload = () => { 24 | i18nApply(); 25 | chrome.storage.sync.get("TUIC", async (settingT) => { 26 | await TUICI18N.fetch(); 27 | const updateUrl = chrome.runtime.getManifest().update_url; 28 | const isWebstore = !(typeof updateUrl === "string" ? updateUrl.includes("google.com") : undefined); 29 | setting = settingT.TUIC ?? { 30 | iconClick: isWebstore, 31 | runBrowser: isWebstore, 32 | openTwitter: isWebstore, 33 | }; 34 | const settingList = ["iconClick", "openTwitter", "runBrowser"]; 35 | for (const i of settingList) { 36 | const elem = document.querySelector(`#${i}`); 37 | if (setting[i]) { 38 | elem.checked = true; 39 | } 40 | elem.addEventListener("change", checkbox); 41 | } 42 | }); 43 | i18nApply(); 44 | }; 45 | -------------------------------------------------------------------------------- /scripts/change-manifest.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import fsSync from "node:fs"; 3 | import type { Manifest } from "webextension-polyfill"; 4 | import manifest from "../manifest.config.ts"; 5 | 6 | export async function changeManifest(target: string) { 7 | if (target !== "firefox" && target !== "chromium" && target !== "chromiumCRX") return; 8 | const config = manifest; 9 | 10 | const targets = Object.keys(config).filter((k) => k !== "common"); 11 | 12 | if (!targets.includes(target)) { 13 | console.error(`Error: Invalid platform "${target ?? ""}". (${targets.join(", ")})`); 14 | process.exit(1); 15 | } 16 | 17 | let output: Manifest.WebExtensionManifest & { update_url?: string }; 18 | if (target == "chromiumCRX") { 19 | output = Object.assign(config.common, config.chromium, config.chromiumCRX) as Manifest.WebExtensionManifest & { update_url: string }; 20 | const repo = process.env["GITHUB_REPO"]; 21 | output.update_url = output.update_url.replace("$(github.repository)", repo); 22 | } else { 23 | output = Object.assign(config.common, config[target]) as Manifest.WebExtensionManifest; 24 | } 25 | 26 | if (!fsSync.existsSync("./dist")) { 27 | await fs.mkdir("./dist"); 28 | } 29 | 30 | await fs.writeFile("./dist/manifest.json", JSON.stringify(output, undefined, 4)); 31 | } 32 | 33 | if (process.argv[1] === import.meta.filename) { 34 | const target = process.argv[2]; 35 | if (target === "firefox" || target === "chromium" || target === "chromiumCRX") { 36 | changeManifest(target); 37 | } else { 38 | console.error(`Error: Invalid platform "${target ?? ""}".`); 39 | process.exit(1); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/content/modules/observer/functions/composeTweet.ts: -------------------------------------------------------------------------------- 1 | import { TUICI18N } from "@content/modules/i18n"; 2 | import { getPref } from "@content/modules/pref"; 3 | import { processElement, waitForElement } from "@content/modules/utils/controlElements"; 4 | import { placeToastMessage } from "@content/modules/utils/toastMessage"; 5 | import { ProcessedClass } from "@shared/sharedData"; 6 | 7 | export function composetweet() { 8 | if (location.pathname === "/compose/post" && document.querySelector(`[data-testid="tweetButton"]:not(.${ProcessedClass})`)) { 9 | const composeTweetButton = document.querySelector(`[data-testid="tweetButton"]`); 10 | composeTweetButton.addEventListener("click", async () => { 11 | if (composeTweetButton.disabled) return; 12 | if (getPref("composetweet.remainOpened")) { 13 | await waitForElement(`[data-testid="toast"]`); 14 | window.setTimeout(() => document.querySelector(`[data-testid="SideNav_NewTweet_Button"]`)?.click(), 500); 15 | } 16 | if (getPref("composetweet.copyHashtag")) { 17 | const hashs = []; 18 | for (const sentence of document.querySelectorAll(`[data-testid="tweetTextarea_0"] span[data-text="true"]`)) { 19 | if (sentence?.textContent && (sentence.textContent.startsWith("#") || sentence.textContent.startsWith("$"))) hashs.push(sentence.textContent); 20 | } 21 | if (hashs.length > 0) { 22 | navigator.clipboard.writeText(hashs.join(" ")); 23 | placeToastMessage(TUICI18N.get("bottomTweetButtons-urlCopy-layer")); 24 | } 25 | } 26 | }); 27 | processElement(composeTweetButton); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/i18nUpdate.yml: -------------------------------------------------------------------------------- 1 | name: i18n Update 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "i18n/_langList.ts" 9 | - "i18n/_officialTwitterI18n.ts" 10 | - "i18n/_officialTwitterI18nConfig.ts" 11 | - "scripts/update-i18n.ts" 12 | workflow_dispatch: 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | update-i18n: 19 | if: github.repository_owner == 'Ablaze-MIRAI' || github.event_name == 'workflow_dispatch' 20 | name: Update 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v6 25 | 26 | - name: Get pnpm version from package.json 27 | id: pnpm-version 28 | run: echo "PNPM_VERSION=$(node -p 'require(`./package.json`).engines.pnpm')" >> $GITHUB_OUTPUT 29 | 30 | - name: Setup pnpm 31 | uses: pnpm/action-setup@v4 32 | with: 33 | version: ${{ steps.pnpm-version.outputs.PNPM_VERSION }} 34 | 35 | - name: Setup Node.js 36 | uses: actions/setup-node@v6 37 | with: 38 | node-version-file: "package.json" 39 | cache: "pnpm" 40 | 41 | - name: Update 42 | run: | 43 | pnpm install 44 | git checkout -- . 45 | git pull 46 | node ./scripts/update-i18n.ts 47 | 48 | if [ -z "$(git status --porcelain)" ]; then 49 | echo "::notice::No ti18n changes" 50 | exit 0 51 | fi 52 | 53 | git config --global user.name "github-actions[bot]" 54 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 55 | git add . 56 | git commit -m "Update twitter i18n" 57 | git push 58 | echo "::notice::Committed and pushed ti18n changes" 59 | -------------------------------------------------------------------------------- /src/shared/settings/modules/settingEffectText.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 36 | -------------------------------------------------------------------------------- /src/content/icons/home/home_tuic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /scripts/vite-plugin/unocss.ts: -------------------------------------------------------------------------------- 1 | import { transform } from "lightningcss"; 2 | import type { ReturnedRule, Visitor, CustomAtRules } from "lightningcss"; 3 | import postcss from "postcss"; 4 | import postcssUnocss from "unocss/postcss"; 5 | import type { UserConfig } from "unocss"; 6 | import type { Plugin } from "vite"; 7 | 8 | let unoResult: postcss.Result; 9 | 10 | export const vitePluginUnoCSS = async (configOrPath?: string | UserConfig): Promise => { 11 | return { 12 | name: "unocss", 13 | enforce: "pre", 14 | async buildStart() { 15 | unoResult = await postcss([ 16 | postcssUnocss({ 17 | configOrPath: configOrPath, 18 | }), 19 | ]).process("@unocss;", { from: "src/content/styles/uno.css" }); 20 | //unoResult.messages.map((message) => console.log("[vitePluginUnocss]", message.file)); 21 | }, 22 | }; 23 | }; 24 | 25 | const visitor: Visitor = { 26 | Rule: { 27 | custom: { 28 | unocss(rule): ReturnedRule[] { 29 | let rules: ReturnedRule[]; 30 | transform({ 31 | filename: "uno.css", 32 | code: Buffer.from(unoResult.css), 33 | visitor: { 34 | StyleSheet(stylesheet) { 35 | rules = stylesheet.rules; 36 | //return stylesheet; 37 | }, 38 | }, 39 | }); 40 | return rules; 41 | }, 42 | }, 43 | }, 44 | }; 45 | 46 | const customAtRules: CustomAtRules = { 47 | unocss: { 48 | //prelude: "#", 49 | }, 50 | }; 51 | 52 | export const lightningcssPluginUnoCSS = { 53 | customAtRules: customAtRules, 54 | visitor: visitor, 55 | }; 56 | -------------------------------------------------------------------------------- /uno.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, presetWind4 } from "unocss"; 2 | 3 | export default defineConfig({ 4 | content: { 5 | filesystem: [ 6 | "src/{content,shared}/**/*.{ts,tsx,vue}", 7 | ], 8 | }, 9 | presets: [ 10 | presetWind4({ 11 | preflights: { 12 | reset: false, 13 | }, 14 | }), 15 | ], 16 | rules: [ 17 | ["twcss-text-explicit", { 18 | "background-color": "rgba(0,0,0,0.00)", 19 | border: "0 solid black", 20 | "box-sizing": "border-box", 21 | color: "rgba(0,0,0,1.00)", 22 | display: "inline", 23 | font: '14px -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif', 24 | "list-style": "none", 25 | margin: "0px", 26 | padding: "0px", 27 | position: "relative", 28 | "text-align": "start", 29 | "text-decoration": "none", 30 | "white-space": "pre-wrap", 31 | "word-wrap": "break-word", 32 | }, { layer: "base" }], 33 | ["twcss-flex", { 34 | "align-items": "stretch", 35 | "background-color": "rgba(0,0,0,0.00)", 36 | border: "0 solid black", 37 | "box-sizing": "border-box", 38 | display: "flex", 39 | "flex-basis": "auto", 40 | "flex-direction": "column", 41 | "flex-shrink": 0, 42 | "list-style": "none", 43 | margin: "0px", 44 | "min-height": "0px", 45 | "min-width": "0px", 46 | padding: "0px", 47 | position: "relative", 48 | "text-decoration": "none", 49 | "z-index": 0, 50 | }, { layer: "base" }], 51 | ["font-tw", { "font-family": '"Segoe UI",Meiryo,system-ui,-apple-system,BlinkMacSystemFont,sans-serif' }], 52 | ], 53 | }); 54 | -------------------------------------------------------------------------------- /public/icon/newIcon_TUIC_C_Blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/options/tuic_logo_gray.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/popup/tuic_logo_gray.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/content/icons/branding/tuic_unilogo_gray.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/content/modules/observer/errorDialog.ts: -------------------------------------------------------------------------------- 1 | import { TUICI18N } from "@modules/i18n"; 2 | import { getSourceMap, NRStack, parseErrorStringFF } from "@shared/sourcemap"; 3 | import { ButtonComponent } from "@shared/tlui/components/ButtonComponent"; 4 | import { Dialog } from "@shared/tlui/components/Dialog"; 5 | import { TextboxComponent } from "@shared/tlui/components/TextboxComponent"; 6 | 7 | const errors = []; 8 | 9 | /** エラーダイアログを表示します。 */ 10 | export async function catchError(e: Error, callback: (() => unknown) | null = null, callbackTime = 5000) { 11 | console.error(e); 12 | 13 | // (async () => { 14 | let tmp: NRStack; 15 | //currently, the wasm does not work on chrome 16 | if (!e.stack.includes("chrome-extension://")) { 17 | tmp = await parseErrorStringFF(e.stack); 18 | errors.push(await getSourceMap(tmp.sourcemapUrl, tmp.line, tmp.col)); 19 | } 20 | errors.push(`${e.toString()}${"\r"}${e.stack}`); 21 | 22 | // })(); 23 | 24 | if (errors.length > 2) { 25 | const dialog = new Dialog(TUICI18N.get("common-error")); 26 | dialog 27 | .addComponents([ 28 | ...TUICI18N.get("observerError-message").split("\r"), 29 | "", 30 | new TextboxComponent(errors.join("\r\r"), { readonly: true, rows: 5 }), 31 | new ButtonComponent(TUICI18N.get("common-copy-and-close"), () => { 32 | dialog.close(); 33 | navigator.clipboard.writeText(errors.join("\r\r")); 34 | }), 35 | new ButtonComponent( 36 | TUICI18N.get("common-close"), 37 | () => dialog.close(), 38 | 39 | { 40 | invertColor: true, 41 | }, 42 | ), 43 | ]) 44 | .open(); 45 | } else { 46 | if (callback) window.setTimeout(() => callback?.(), callbackTime); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/content/icons/branding/tuic_unilogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/vite_build.md: -------------------------------------------------------------------------------- 1 | # Viteの導入による変更 2 | 3 | ビルドツール Vite の導入により、TypeScriptやSCSSなどコンパイルが必要なソースを使用できるようになりました。 4 | また、ビルド方法が変わりました。 5 | [CONTRIBUTING.md](../CONTRIBUTING.md)にも記載されているように、`pnpm debug`でパッケージ、及びデバッグが実行されます。 6 | 7 | 参照:[PR #73](https://github.com/Ablaze-MIRAI/Twitter-UI-Customizer/pull/73) (2023年9月2日) 8 | 9 | ## ビルドコマンド一覧 10 | 11 | ### `pnpm debug` 系 12 | 13 | コードが変更されたときに自動反映されます。 14 | リソース及びbackground.tsの変更には対応していません。 15 | 16 | [`.env.local.example`](../.env.local.example)で、他のFirefox系ブラウザ(Floorpなど) 17 | またはChromium系ブラウザを指定することができます。 18 | また、必要な初期設定なども記載していますので、ぜひご参照ください。 19 | 20 | - `pnpm debug` または `pnpm debug:firefox` 21 | Firefox または Firefox系ブラウザでのデバッグを実行します。 22 | 23 | - `pnpm debug:chromium` 24 | Chrome または Chromium系ブラウザでのデバッグを実行します。 25 | 26 | **Chromium系ブラウザでは設定無しではデバッグが正常作動しません。** 27 | 詳しくは[`.env.local.example`](../.env.local.example)をご参照ください。 28 | 29 | `pnpm build --watch` で、`pnpm debug` と同じコマンドになります。 30 | 31 | 各コマンドに `--mode disable-web-ext` オプションを付け加えることで、ブラウザが立ち上がりません。 32 | 既存のような手動でデバッグしたい時にご利用ください。 33 | 34 | バージョンの違うFirefoxで作られたプロファイルは実行時にエラーが出る場合があります。 35 | 例えばFirefox Developer Edition (aurora channel)で作られたプロファイルは 36 | Firefox (Stable)で実行される時、Dev Editionより旧バージョンなため、互換性がないとエラーが出ます。 37 | デバッグの時実行されるFirefoxでプロファイルを作成してください。 38 | 39 | Chromium系ブラウザでは、Chrome136からデフォルトのUser Dataディレクトリで実行する際、 40 | セキュリティ上の理由から`--remote-debugging-pipe`オプションが無視されるため、 41 | 別のUser Dataディレクトリを作成し指定する(かChrome for Testingを使用する?)必要があります。 42 | Chromeを`--user-data-dir=""`オプションでパスを指定して起動することで 43 | 新しいUser Dataディレクトリを作成できます。 44 | 45 | デフォルトではデバッグは、元のプロファイルをコピーして行われるので、変更点が保存されません。 46 | 予め "development"プロファイルでTwitterにログインして置くことをおすすめします。 47 | 48 | ### `pnpm build` 系 49 | 50 | 自動反映はされません。 51 | zipファイルが生成されます。 52 | 53 | - `pnpm build` または `pnpm build:firefox` 54 | Firefox 及び Firefox系ブラウザ向けのビルドをします。 55 | 56 | - `pnpm build:chromium` 57 | Chrome 及び Chromium系ブラウザ向けのビルドをします。 58 | 59 | Chromium CRXビルドに関しては、[`.github/workflows/packaging.yml`](../.github/workflows/packaging.yml)をご参照ください。 60 | -------------------------------------------------------------------------------- /src/content/modules/htmlClass/classManager.ts: -------------------------------------------------------------------------------- 1 | import { ProcessedClass } from "@shared/sharedData"; 2 | import { TUICObserver } from "../observer"; 3 | import { applySystemCss } from "@content/applyCSS"; 4 | 5 | const ClassList = [ 6 | "TUIC_NONE_SPACE_BOTTOM_TWEET", 7 | "TUIC_TWEETREPLACE", 8 | "TUIC_UnderTweetButton", 9 | "TUICTweetButtomBarBase", 10 | "TUICScrollBottom", 11 | "TUICDMIcon", 12 | "TUICFollowerListButtons", 13 | ProcessedClass, 14 | ]; 15 | 16 | const AttrList = { 17 | tuicProcessedArticle: "tuic-processed-article", 18 | tuicDiscoverMore: "tuic-discover-more", 19 | tuicDiscoverMoreTweet: "tuic-discover-more-tweet", 20 | tuicTweetTopButtonParent: "tuic-tweet-top-button-parent", 21 | tuicTweetTopButton: "tuic-tweet-top-button", 22 | 23 | tuicSettings: "tuic-settings", 24 | tuicEventHandled: "tuic-event-handled", 25 | tuicIconType: "tuic-icon-type", 26 | tuicHide: "tuic-hide", 27 | tuicHideChildScrollSnap: "tuic-hide-child-scroll-snap", 28 | 29 | tuicZoomingTweet: "tuic-zooming-tweet", 30 | tuicTabsPinned: "data-tuic-tabs-pinned", 31 | }; 32 | 33 | export const updateClasses = (isInit = false) => { 34 | if (!isInit) TUICObserver.unbind(); 35 | deleteClasses(); 36 | applySystemCss(); 37 | if (!isInit) TUICObserver.callback(); 38 | }; 39 | 40 | const deleteClasses = () => { 41 | for (const id of ClassList) { 42 | document.querySelectorAll(`.${id}`).forEach((elem) => { 43 | elem.classList.remove(id); 44 | }); /* 45 | for (const elem of document.getElementsByClassName(id)) { 46 | elem.classList.remove(id); 47 | }*/ 48 | } 49 | for (const id in AttrList) { 50 | document.querySelectorAll(`[data-${AttrList[id]}]`).forEach((elem) => { 51 | delete elem.dataset[id]; 52 | }); /* 53 | for (const elem of document.getElementsByClassName(id)) { 54 | elem.classList.remove(id); 55 | }*/ 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /src/content/styles/features/style-iconSettings.css: -------------------------------------------------------------------------------- 1 | [data-tuic-icon-type="dog"], 2 | [data-tuic-icon-type^="officialLogo"], 3 | [data-tuic-icon-type="x-daruma"], 4 | [data-tuic-icon-type="custom"] { 5 | &:not([role="alertdialog"] [data-testid="confirmationSheetDialog"] *) { 6 | &:not([role="img"] > * > *) { 7 | height: inherit !important; 8 | margin: 8px; 9 | } 10 | background-size: cover; 11 | } 12 | 13 | & > * { 14 | display: none !important; 15 | } 16 | } 17 | 18 | #placeholder > :is([data-tuic-icon-type="dog"], [data-tuic-icon-type^="officialLogo"], [data-tuic-icon-type="x-daruma"], [data-tuic-icon-type="custom"]) { 19 | position: absolute; 20 | margin: auto; 21 | } 22 | 23 | [role="alertdialog"] [data-testid="confirmationSheetDialog"] { 24 | &[data-tuic-icon-type="custom"] { 25 | width: 40px; 26 | height: 40px; 27 | margin-right: auto; 28 | margin-left: auto; 29 | } 30 | 31 | &:is([data-tuic-icon-type="dog"], [data-tuic-icon-type=^"officialLogo"], [data-tuic-icon-type="x-daruma"], [data-tuic-icon-type="custom"]) { 32 | background-repeat: no-repeat; 33 | background-position: center; 34 | background-size: contain !important; 35 | mask-repeat: no-repeat; 36 | mask-position: center; 37 | mask-size: contain !important; 38 | } 39 | } 40 | 41 | [data-tuic-icon-type^="officialLogo"] { 42 | background-color: var(--twitter-twitterIcon-color); 43 | mask-image: var(--TUIC-twitter-icon) !important; 44 | 45 | &:not([role="alertdialog"] [data-testid="confirmationSheetDialog"] *) { 46 | mask-size: cover !important; 47 | } 48 | } 49 | 50 | #layers [data-testid="TopNavBar"] div + [data-tuic-icon-type="custom"] { 51 | width: auto !important; 52 | background-repeat: no-repeat !important; 53 | background-position: center; 54 | background-size: contain !important; 55 | } 56 | 57 | :root[data-tuic-settings*="|twitterIcon.options.roundIcon|"] [data-tuic-icon-type="custom"] { 58 | border-radius: 9999px !important; 59 | } 60 | -------------------------------------------------------------------------------- /i18n/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "@JapaneseLanguageName": "トルコ語", 3 | "settingUI-goBackButton": "Geri", 4 | "settingUI-restoreDefaultAll": "Tümünü Varsayılana Geri Döndür", 5 | "settingUI-upDownList-toLeft": "Göster", 6 | "settingUI-upDownList-toRight": "Gizle", 7 | "settingUI-upDownList-toUp": "Yukarı", 8 | "settingUI-upDownList-toDown": "Aşağı", 9 | "settingUI-upDownList-restoreDefault": "Varsayılanları geri yükle", 10 | "settingUI-upDownList-visible": "Göster", 11 | "settingUI-upDownList-invisible": "Gizle", 12 | "settingUI-colorPicker-transparent": "Saydam", 13 | "settingUI-colorPicker-textColor": "Yazı Rengi", 14 | "settingUI-colorPicker-background": "Arka Plan Rengi", 15 | "bottomTweetButtons-userBlock": "Kullanıcıyı Engelle", 16 | "bottomTweetButtons-userMute": "Kullanıcıyı Sessize Al", 17 | "bottomTweetButtons-likeAndRT": "Beğen ve Retweet'le", 18 | "bottomTweetButtons-deleteButton": "Tweeti Sil", 19 | "bottomTweetButtons-setting-linkCopyURL-twitter": "twitter.com", 20 | "bottomTweetButtons-setting-linkCopyURL-X": "x.com", 21 | "bottomTweetButtons-setting-linkCopyURL-vxTwitter": "vxtwitter.com", 22 | "rightSidebar-links": "Bağlantıları Gizle", 23 | "timeline-discoverMore-nomal": "Varsayılan", 24 | "timeline-discoverMore-invisible": "Gizle", 25 | "customCSS-settingTitle": "Özel CSS", 26 | "customCSS-save": "Özel CSS Kaydet", 27 | "settingColors-settingTitle": "Renk Ayarları", 28 | "settingColors-select-light": "Açık", 29 | "settingColors-select-dark": "Koyu", 30 | "settingColors-notFollowingButton": "Takip butonu", 31 | "settingColors-unfollowButton": "Takibi Bırak butonu", 32 | "twitterIcon-normal": "Varsayılan", 33 | "twitterIcon-invisible": "Gizle", 34 | "twitterIcon-dog": "Doge", 35 | "twitterIcon-twitter": "Twitter", 36 | "twitterIcon-X": "X", 37 | "export-settingTitle": "Dışa Aktar", 38 | "export-exportButton": "Dışa Aktarma Ayarları", 39 | "export-exportButtonCopy": "Dışa aktarılan ayarları kopyala", 40 | "import-settingTitle": "İçe Aktar", 41 | "common-hide": "Gizle" 42 | } 43 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | **パッケージマネージャーをpnpmに変更しました!** 4 | yarnを使用していた方は、pnpmをインストールして`node_modules`を削除した上で 5 | `pnpm i --frozen-lockfile`を実行してください。 6 | 7 | **ビルドツール Vite導入により、デバッグ方法が変わりました!** 8 | ビルド及びデバッグ方法については、[docs/vite_build](./docs/vite_build.md)を御覧ください。 9 | この変更は[`41dff7b`](https://github.com/Ablaze-MIRAI/Twitter-UI-Customizer/commit/41dff7b4e8c01c33ef04d05b8ff5e9e649f2719d) (2023年9月2日)からの適用です。 10 | 11 | ## いるかもわからぬ翻訳者の方へ 12 | 13 | Crowdinで試験的にやってみています! 14 | crowdin 15 | 16 | ### Twitter上でのTUICの翻訳 17 | 18 | - Twitterで使用できる言語は全て(ファイルだけでも)用意していて、一番最初の`@JapaneseLanguageName`に、言語名を書いています 19 | 20 | #### 翻訳の仕方 21 | 22 | 1. `i18n/<言語タグ名>.json`を開く 23 | 2. `i18n/ja.json`をもとに翻訳する 24 | 25 | ### ポップアップなどTUIC自体の翻訳 26 | 27 | [こちらの記事](https://developer.mozilla.org/ja/docs/Mozilla/Add-ons/WebExtensions/Internationalization)と[`_locales/ja/messages.json`](./_locales/ja/messages.json)を参照してください 28 | 29 | ## アドオンのデバッグ方法 30 | 31 | **Chromium、またはFirefoxでのデバッグの詳細は [`docs/vite_build`](./docs/vite_build.md)を御覧ください。** 32 | 33 | manifest.jsonなどのデバッグ・ソースコードの情報は[`docs/manifest_build`](./docs/manifest_build.md)を見てください! 34 | 35 | **重要**: Firefox ブラウザーが事前にインストールされている必要があります。 36 | また、新しいプロファイルを "about:profiles" で "development" といった名前で作成する必要があります。 37 | プロファイルや環境によるバグを防ぐためにプロファイルは分けられます。 38 | 39 | ```bash 40 | 41 | pnpm i --frozen-lockfile 42 | 43 | ## Firefox でデバッグする場合(引数なしの場合はデフォルトで Firefox でデバッグします) 44 | pnpm debug 45 | 46 | # or 47 | pnpm debug:firefox 48 | 49 | ## Chrome または Chromium 系ブラウザー でデバッグする場合 50 | pnpm debug:chromium 51 | 52 | ## Firefox または Firefox 系ブラウザーでデバッグする場合 53 | 54 | # .env.local で `TUIC_WEBEXT_FIREFOX_EXECUTABLE`を使いたいFirefoxの経路に設定した後に 55 | pnpm debug:firefox 56 | 57 | # 例 58 | # TUIC_WEBEXT_FIREFOX_EXECUTABLE="C:\Program Files\Firefox Developer Edition\firefox.exe" 59 | # プロファイルでエラーが出る場合や直接指定したい場合 60 | # TUIC_WEBEXT_FIREFOX_PROFILE="C:\Users\user\AppData\Roaming\Mozilla\Firefox\Profiles\h6jvvuqd.dev_tuic" 61 | pnpm debug:firefox 62 | 63 | ``` 64 | 65 | デバッグでは web-ext を使用しているためデバッグ中に加えた変更はブラウザーをリロードしなくても反映されます。 66 | -------------------------------------------------------------------------------- /src/shared/tlui/components/ButtonComponent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "./Component"; 2 | 3 | export interface ButtonComponentInit { 4 | /** 5 | * ボタンの幅を最大にするかどうか(初期値: `true`) 6 | */ 7 | fullWidth?: boolean; 8 | /** 9 | * ボタンの色を反転させるかどうか(初期値: `false`) 10 | */ 11 | invertColor?: boolean; 12 | } 13 | 14 | type OnclickListener = GlobalEventHandlers["onclick"]; 15 | 16 | /** 17 | * Twitterのボタン風要素 18 | */ 19 | export class ButtonComponent implements Component { 20 | public element: HTMLButtonElement; 21 | 22 | /** 23 | * ボタンコンポーネントのクリックイベント 24 | */ 25 | public onclick: OnclickListener; 26 | 27 | /** 28 | * @param text ボタンテキスト 29 | * @param onclick クリックイベント 30 | * @param options オプション 31 | */ 32 | constructor(text: string, onclick: OnclickListener, options: ButtonComponentInit = {}) { 33 | this.element = new DOMParser().parseFromString( 34 | ` 35 | 36 | `, 37 | "text/html", 38 | ).body.children[0] as HTMLButtonElement; 39 | this.onclick = onclick; 40 | 41 | this.element.onclick = onclick; 42 | 43 | for (const [key, value] of Object.entries(Object.assign({}, options))) { 44 | this[key] = value; 45 | } 46 | } 47 | 48 | /** 49 | * ボタンの幅を最大にするかどうか(初期値: `true`) 50 | */ 51 | public get fullWidth(): boolean { 52 | return this.element.classList.contains("full-width"); 53 | } 54 | public set fullWidth(value: boolean) { 55 | if (value) { 56 | this.element.classList.add("full-width"); 57 | } else { 58 | this.element.classList.remove("full-width"); 59 | } 60 | } 61 | 62 | /** 63 | * ボタンの色を反転させるかどうか(初期値: `false`) 64 | */ 65 | public get invertColor(): boolean { 66 | return this.element.classList.contains("invert-color"); 67 | } 68 | public set invertColor(value: boolean) { 69 | if (value) { 70 | this.element.classList.add("invert-color"); 71 | } else { 72 | this.element.classList.remove("invert-color"); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/content/icons/common/verified.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/content/modules/observer/functions/profile/followersListButton/buttons.tsx: -------------------------------------------------------------------------------- 1 | import { fontSizeClass } from "@modules/utils/fontSize"; 2 | import { backgroundColorClass } from "@content/modules/utils/color"; 3 | import { JSX } from "solid-js"; 4 | import { data } from "./data"; 5 | 6 | export function followersListButton(id: string, baseElement: HTMLElement): () => JSX.Element { 7 | return (): JSX.Element => ( 8 |
{ 12 | if (e.key === "Enter") { 13 | data[id].clickEvent(baseElement); 14 | } 15 | }} 16 | onClick={() => data[id].clickEvent(baseElement)} 17 | > 18 |
19 |
25 |
26 |
27 | 38 |
39 |
40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 4, 3 | "[markdown][yaml]": { 4 | "editor.tabSize": 2, 5 | }, 6 | "editor.formatOnSave": false, 7 | "[javascript][json]": { 8 | "editor.formatOnSave": false, 9 | }, 10 | "editor.quickSuggestions": { 11 | "strings": true, 12 | }, 13 | "files.autoSave": "onFocusChange", 14 | "json.schemaDownload.enable": true, 15 | // linter 16 | "editor.codeActionsOnSave": { 17 | "source.fixAll.eslint": "explicit", 18 | "source.fixAll.stylelint": "explicit", 19 | "source.organizeImports": "never", 20 | }, 21 | // prettier 22 | "prettier.enable": true, 23 | "[css][scss][postcss]": { 24 | "editor.defaultFormatter": "esbenp.prettier-vscode", 25 | "editor.formatOnSave": true, 26 | "prettier.tabWidth": 4, 27 | "prettier.semi": true, 28 | "prettier.singleQuote": false, 29 | "prettier.endOfLine": "auto", 30 | "prettier.printWidth": 300, 31 | }, 32 | // stylelint 33 | "css.validate": false, 34 | "scss.validate": false, 35 | "stylelint.packageManager": "pnpm", 36 | "stylelint.snippet": [ 37 | "css", 38 | "scss", 39 | "postcss", 40 | "vue" 41 | ], 42 | "stylelint.validate": [ 43 | "css", 44 | "scss", 45 | "postcss", 46 | "vue" 47 | ], 48 | // eslint 49 | "eslint.validate": [ 50 | "javascript", 51 | "typescript", 52 | "typescriptreact", 53 | "vue", 54 | ], 55 | "eslint.rules.customizations": [ 56 | { "rule": "style/*", "severity": "off", "fixable": true }, 57 | { "rule": "*-indent" , "severity": "off", "fixable": true }, 58 | { "rule": "*-spacing", "severity": "off", "fixable": true }, 59 | { "rule": "*-spaces" , "severity": "off", "fixable": true }, 60 | { "rule": "*-order" , "severity": "off", "fixable": true }, 61 | { "rule": "*-dangle" , "severity": "off", "fixable": true }, 62 | { "rule": "*-newline", "severity": "off", "fixable": true }, 63 | { "rule": "*quotes" , "severity": "off", "fixable": true }, 64 | { "rule": "*semi" , "severity": "off", "fixable": true }, 65 | ], 66 | } 67 | -------------------------------------------------------------------------------- /src/shared/sourcemap/index.ts: -------------------------------------------------------------------------------- 1 | import init, { NRSourceMap } from "@third-party/sourcemap/dist/sourcemap_js"; 2 | 3 | //TODO: ChromeでWASMが動作するようする 4 | /** 5 | * @experimental Does not work on Chrome 6 | */ 7 | export async function getSourceMap(sourcemapUrl: string, line: number, col: number): Promise { 8 | const _inst = await init(); 9 | const nrSourceMap = new NRSourceMap(await (await fetch(sourcemapUrl)).text()); 10 | const token = nrSourceMap.lookup(line, col); 11 | const source = token.source; 12 | const src_line = token.line; 13 | const src_col = token.col; 14 | token.free(); 15 | nrSourceMap.free(); 16 | return `${source}:${src_line}:${src_col}`; 17 | } 18 | 19 | export interface NRStack { 20 | funcName: string; 21 | sourcemapUrl: string; 22 | line: number; 23 | col: number; 24 | } 25 | 26 | export async function parseErrorStringFF(stack: string): Promise { 27 | const [funcName, urlLineCol] = stack.split("@", 2); 28 | 29 | const [moz_ext, url, line, col] = urlLineCol.split(":"); 30 | 31 | return { 32 | funcName, 33 | sourcemapUrl: moz_ext + ":" + url + ".map", 34 | line: Number(line), 35 | col: Number(col), 36 | }; 37 | 38 | //throwTestError@moz-extension://59481b91-5073-4ff5-9606-24dfcf0e60ea/index.js:3783:9 39 | } 40 | 41 | /** 42 | * @experimental getSourceMap does not work on Chrome 43 | */ 44 | export async function parseErrorStringCH(stack: string): Promise { 45 | // at throwTestError (chrome-extension://cecnhkopjammcfjllglmcgdpacjnfeed/index.js:4032:9) 46 | const ch_ext_index = stack.indexOf("chrome-extension://"); 47 | const funcName = stack.slice(0, ch_ext_index).replace(" at ", "").trim(); 48 | const urlLineCol = stack.slice(ch_ext_index, stack.length - 2); 49 | 50 | const [ch_ext, url, line, col] = urlLineCol.split(":"); 51 | 52 | return { 53 | funcName, 54 | sourcemapUrl: ch_ext + ":" + url + ".map", 55 | line: Number(line), 56 | col: Number(col), 57 | }; 58 | // Uncaught (in promise) CompileError: WebAssembly.instantiateStreaming(): 59 | // at __wbg_load (index.js:3573:34) 60 | // at __wbg_init (index.js:3618:38) 61 | // at async getSourceMap (index.js:3622:3) 62 | // at async index.js:3873:5 63 | } 64 | -------------------------------------------------------------------------------- /src/shared/settings/components/defaultPrefButton.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 54 | -------------------------------------------------------------------------------- /docs/i18n.md: -------------------------------------------------------------------------------- 1 | # 翻訳について 2 | 3 | Twitter UI Customizerには2種類の翻訳の方法があります 4 | その2つについて説明します 5 | 6 | ## ①Crowdinを通した翻訳 7 | 8 | TUIC独自の機能について翻訳するときは、この方法を使います。 9 | Crowdinはこちらです 10 | → 11 | 12 | では、方法を説明します 13 | 14 | 1. `/i18n/ja.json`に、翻訳IDと日本語訳を記入する 15 | 16 | ...これだけです() 17 | あとはCrowdin側で勝手に同期してくれるので、翻訳者のみなさまが翻訳してくれるのを待つだけです! 18 | 19 | また、オプションページやポップアップなどの翻訳もCrowdinを使います 20 | こちらも日本語に関しては予め登録する必要があります 21 | やり方はこちらを見たほうが早いと思います(独自の機能ではないので) 22 | 23 | 24 | ## ②Twitterを流用した翻訳 25 | 26 | Twitterに存在する機能や、昔はあった表現(リツイート→ポスト)などは、こちらを利用します 27 | こっちの方法のほうがやり方は複雑です 28 | 29 | 1. `/i18n/_officialTwitterI18n.ts`に、`TUIC側の翻訳ID:Twitter側の翻訳ID`という形で記入する 30 | 2. 必要に応じて(昔の翻訳や最新の翻訳を使いたい場合)`/scripts/update-i18n.ts`や`/i18n/_officialTwitterI18nConfig.ts`、を修正する 31 | 3. (もしデバッグするなら)`node ./scripts/update-i18n.ts ja`などのコマンドで翻訳を更新する 32 | 33 | ※Twitter側の翻訳IDはこちらにあります 34 | 最新の翻訳 35 | → 36 | 何も`/scripts/updateI18n.json`をいじらなければ使用される翻訳(少し古めのやつをわざわざふぁさんが用意してくれました!) 37 | → 38 | 少し古い翻訳 39 | → 40 | 41 | ## update-i18n.tsについてもう少し詳しく 42 | 43 | update-i18n.tsは、上に挙げたGitHubのファイルから、TUICに翻訳を移すスクリプトです 44 | `/i18n/_officialTwitterI18n.ts` `/i18n/_officialTwitterI18nConfig.ts`という2つのファイルを元に処理を行います 45 | 基本的には`/i18n/_officialTwitterI18n.ts`だけで問題ないです 46 | ただし、稀に要らないテキストが含まれていたり、最新/昔の翻訳を利用したい場合があるので、その時に`/i18n/_officialTwitterI18nConfig.ts`をいじります 47 | 48 | ### `/i18n/_officialTwitterI18nConfig.ts`の設定 49 | 50 | 基本的にTwitter側の翻訳IDを利用します 51 | 52 | - 昔の翻訳を利用したい場合:"oldTranslate"に翻訳IDを追記 53 | - 最新の翻訳を利用したい場合:"latestTranslate"に翻訳IDを追記 54 | - 単数混ざってるやつを複数形に統一したい場合:"fixPlural"に翻訳IDを追記 55 | - 単数混ざってるやつを単数形に統一したい場合:"fixSingular"に翻訳IDを追記 56 | - 文字列を削除したい場合:"deleteString"に翻訳IDをKeyとして配列を追記し、その中に削除したい文字列を入れる 57 | 58 | ### update-i18n.tsのコマンド 59 | 60 | - `node ./scripts/update-i18n.ts` 61 | 全ての言語の翻訳が更新されます 62 | 処理時間が長いのでおすすめしません 63 | (GitHub Actionsで更新する時に利用します) 64 | - `node ./scripts/update-i18n.ts <言語コード> ...` 65 | 指定した言語コードの言語の翻訳が更新されます 66 | デバッグ時はこちらをおすすめします 67 | -------------------------------------------------------------------------------- /src/content/modules/utils/dateAndTime.ts: -------------------------------------------------------------------------------- 1 | import { TUICI18N } from "../i18n"; 2 | import { getPref } from "../pref"; 3 | 4 | let TimeFormat: Intl.DateTimeFormat; 5 | let DateFormat: Intl.DateTimeFormat; 6 | let language = ""; 7 | 8 | let second: boolean; 9 | let hour12: boolean; 10 | let requireDate: boolean; 11 | 12 | function getIntlFormat() { 13 | const [prefSecond, prefHour12, prefRequireDate] = [ 14 | getPref("dateAndTime.options.second"), 15 | getPref("dateAndTime.options.hour12"), 16 | getPref("dateAndTime.dateAboveTweet") === "absolutely", 17 | ]; 18 | const changedLang = language !== document.querySelector("html").lang; 19 | if (changedLang) { 20 | language = document.querySelector("html").lang; 21 | DateFormat = Intl.DateTimeFormat(language, { month: "short", day: "numeric" }); 22 | } 23 | if ( 24 | changedLang 25 | || !TimeFormat 26 | || prefHour12 !== hour12 27 | || prefSecond !== second 28 | || prefRequireDate !== requireDate 29 | ) { 30 | [hour12, second, requireDate] = [prefHour12, prefSecond, prefRequireDate]; 31 | TimeFormat = new Intl.DateTimeFormat(language, 32 | { 33 | dateStyle: requireDate ? "medium" : undefined, 34 | timeStyle: second ? "medium" : "short", 35 | hour12: hour12, 36 | }, 37 | ); 38 | } 39 | } 40 | 41 | export function formatTimeText(dateTime: string): string { 42 | getIntlFormat(); 43 | return TimeFormat.format(new Date(dateTime)); 44 | } 45 | 46 | export function getAbsolutelyTime(dateTime: string): string { 47 | getIntlFormat(); 48 | const date = new Date(dateTime); 49 | const nowDate = new Date(); 50 | if (requireDate || date.getDate() >= nowDate.getDate()) { 51 | return TimeFormat.format(date); 52 | } else { 53 | return DateFormat.format(date); 54 | } 55 | } 56 | 57 | export function isRelativeTime(dateText: string): boolean { 58 | const timeTempleteText: string = TUICI18N.get("dateAndTime.options.absolutelyTime.ago"); 59 | const timeTempleteReg = new RegExp(`^${timeTempleteText.replace("{0}", ".*")}$`, "i"); 60 | const timeTempleteReg2 = new RegExp(`^${timeTempleteText.replaceAll("{", "\\{").replaceAll("}", "\\}")}`, "i"); 61 | return timeTempleteReg.test(dateText) || timeTempleteReg2.test(dateText); 62 | } 63 | -------------------------------------------------------------------------------- /src/shared/tlui/components/TextboxComponent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "./Component"; 2 | 3 | export interface TextboxComponentInit { 4 | /** 5 | * テキストボックスの幅を最大にするかどうか(初期値: `true`) 6 | */ 7 | fullWidth?: boolean; 8 | /** 9 | * リードオンリーかどうか(初期値: `false`) 10 | */ 11 | readonly?: boolean; 12 | /** 13 | * デフォルトの行数+1(初期値: `2`) 14 | */ 15 | rows?: number; 16 | } 17 | 18 | /** 19 | * テキストボックス 20 | */ 21 | export class TextboxComponent implements Component { 22 | public element: HTMLTextAreaElement; 23 | 24 | /** 25 | * @param text テキストボックスの中身 26 | * @param options オプション 27 | */ 28 | constructor(text: string, options: TextboxComponentInit = {}) { 29 | this.element = new DOMParser().parseFromString( 30 | ` 31 | 17 | 25 | 26 | 34 |

{TUICI18N.get("rescuePref-complete")}

35 | 36 | ); 37 | }; 38 | 39 | const elem = (): JSX.Element => { 40 | return ( 41 | 57 | ); 58 | }; 59 | 60 | export async function placePrintPrefButton() { 61 | const baseELement = (await waitForElement(".u01b-01__desktop-primary-links"))[0]; 62 | render(elem, baseELement); 63 | } 64 | -------------------------------------------------------------------------------- /src/content/styles/style-tlui.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --tlui-dialog-background: #000; 3 | --tlui-dialog-text: #fff; 4 | --tlui-dialog-mask: rgb(91 112 131 / 40%); 5 | --tlui-button-background: var(--tlui-dialog-text); 6 | --tlui-button-text: var(--tlui-dialog-background); 7 | --tlui-button-background-hover: rgb(215 219 220); 8 | --tlui-button-background-invert-hover: rgb(239 243 244 / 10%); 9 | } 10 | 11 | /* コンテナ */ 12 | .tlui .tlui-container { 13 | padding: 32px; 14 | } 15 | 16 | /* ダイアログ > マスク */ 17 | .tlui-dialog { 18 | position: fixed; 19 | inset: 0; 20 | 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | 25 | background-color: var(--tlui-dialog-mask); 26 | } 27 | 28 | /* ダイアログ > コンテナ */ 29 | .tlui-dialog > .tlui-container { 30 | width: fit-content; 31 | min-width: 380px; 32 | max-width: 80vw; 33 | max-height: 80vh; 34 | margin: auto; 35 | 36 | overflow-y: auto; 37 | 38 | font-family: 39 | "Segoe UI", 40 | Meiryo, 41 | system-ui, 42 | -apple-system, 43 | BlinkMacSystemFont, 44 | sans-serif; 45 | color: var(--tlui-dialog-text); 46 | background-color: var(--tlui-dialog-background); 47 | 48 | border-radius: 16px; 49 | } 50 | .tlui-dialog:not(.has-padding) > .tlui-container { 51 | padding: 0; 52 | } 53 | .tlui-dialog:not(.fit-content-width) > .tlui-container { 54 | width: var(--tuic-dialog-width, 380px); 55 | } 56 | /* ダイアログ > 外枠 > コンテナ > ヘッダー */ 57 | .tlui-dialog h1 { 58 | margin: 0; 59 | margin-bottom: 8px; 60 | font-size: 20px; 61 | font-weight: 700; 62 | line-height: 24px; 63 | overflow-wrap: break-word; 64 | } 65 | /* ダイアログ > 外枠 > コンテナ > 本文 */ 66 | .tlui-dialog p { 67 | margin: 0; 68 | font-size: 15px; 69 | line-height: 22px; 70 | color: rgb(113 118 123); 71 | } 72 | /* ダイアログ > 外枠 > コンテナ > ボタン */ 73 | .tlui-dialog button:not(#TUICOriginalDisplaySetting *) { 74 | height: 45px; 75 | padding-inline: 16px; 76 | margin-inline: auto; 77 | margin-top: 16px; 78 | 79 | font-size: 1.1em; 80 | font-weight: 700; 81 | line-height: 1em; 82 | color: var(--tlui-button-text); 83 | text-align: center; 84 | cursor: pointer; 85 | background: var(--tlui-button-background); 86 | border: 1px solid rgb(83 100 113); 87 | border-radius: 100000px; 88 | transition-duration: 0.2s; 89 | } 90 | .tlui-dialog .full-width:is(button, textarea) { 91 | width: 100%; 92 | } 93 | .tlui-dialog button.invert-color { 94 | color: var(--tlui-button-background); 95 | background-color: var(--tlui-button-text); 96 | } 97 | .tlui-dialog button:is(:hover, :focus-visible) { 98 | background-color: var(--tlui-button-background-hover); 99 | } 100 | .tlui-dialog button.invert-color:is(:hover, :focus-visible) { 101 | background-color: var(--tlui-button-background-invert-hover); 102 | } 103 | 104 | html.tlui-has-dialog { 105 | overflow: hidden !important; 106 | overscroll-behavior-y: none !important; 107 | } 108 | --------------------------------------------------------------------------------