├── .gitignore ├── .npmrc ├── gui ├── src-tauri │ ├── src │ │ ├── build.rs │ │ ├── main.rs │ │ └── cmd.rs │ ├── icons │ │ ├── icon.ico │ │ ├── icon.png │ │ ├── 32x32.png │ │ ├── icon.icns │ │ ├── 128x128.png │ │ ├── StoreLogo.png │ │ ├── 128x128@2x.png │ │ ├── Square30x30Logo.png │ │ ├── Square44x44Logo.png │ │ ├── Square71x71Logo.png │ │ ├── Square89x89Logo.png │ │ ├── Square107x107Logo.png │ │ ├── Square142x142Logo.png │ │ ├── Square150x150Logo.png │ │ ├── Square284x284Logo.png │ │ └── Square310x310Logo.png │ ├── .gitignore │ ├── capabilities │ │ └── desktop.json │ ├── Cargo.toml │ └── tauri.conf.json ├── favicon.ico ├── setup-test.ts ├── .prettierrc ├── src │ ├── components │ │ ├── icons │ │ │ ├── index.ts │ │ │ ├── PlusIcon.tsx │ │ │ └── CloseIcon.tsx │ │ ├── layout │ │ │ ├── index.ts │ │ │ ├── Divider │ │ │ │ ├── Divider.module.css │ │ │ │ └── Divider.tsx │ │ │ ├── Header │ │ │ │ ├── Header.module.css │ │ │ │ ├── Header.tsx │ │ │ │ ├── Menu.module.css │ │ │ │ └── Menu.tsx │ │ │ └── Modal │ │ │ │ ├── Modal.module.css │ │ │ │ └── Modal.tsx │ │ ├── feedback │ │ │ ├── index.ts │ │ │ ├── Badge │ │ │ │ ├── Badge.tsx │ │ │ │ └── Badge.module.css │ │ │ ├── Spinner │ │ │ │ ├── Spinner.tsx │ │ │ │ └── Spinner.module.css │ │ │ └── Toast │ │ │ │ ├── Toast.module.css │ │ │ │ ├── Toast.tsx │ │ │ │ └── ToastManager.tsx │ │ ├── forms │ │ │ ├── Label │ │ │ │ ├── Label.module.css │ │ │ │ └── Label.tsx │ │ │ ├── index.ts │ │ │ ├── Checkbox │ │ │ │ ├── Checkbox.module.css │ │ │ │ └── Checkbox.tsx │ │ │ ├── Input │ │ │ │ ├── Input.module.css │ │ │ │ └── Input.tsx │ │ │ ├── Textarea │ │ │ │ ├── Textarea.module.css │ │ │ │ └── Textarea.tsx │ │ │ ├── Select │ │ │ │ ├── Select.module.css │ │ │ │ ├── Select.tsx │ │ │ │ └── Select.test.tsx │ │ │ └── Button │ │ │ │ ├── Button.tsx │ │ │ │ └── Button.module.css │ │ └── index.ts │ ├── pages │ │ ├── Download │ │ │ ├── Download.module.css │ │ │ └── Download.tsx │ │ ├── Purge │ │ │ ├── Purge.module.css │ │ │ └── Purge.tsx │ │ ├── index.ts │ │ ├── Upload │ │ │ ├── FileList.tsx │ │ │ ├── FileList.module.css │ │ │ ├── Upload.module.css │ │ │ └── Upload.tsx │ │ ├── Move │ │ │ ├── Move.module.css │ │ │ └── Move.tsx │ │ ├── Delete │ │ │ ├── Delete.module.css │ │ │ └── Delete.tsx │ │ ├── List │ │ │ ├── List.module.css │ │ │ └── List.tsx │ │ ├── Edit │ │ │ ├── FindReplaceModal.module.css │ │ │ ├── Edit.module.css │ │ │ ├── FindReplaceModal.tsx │ │ │ └── Edit.tsx │ │ └── Account │ │ │ ├── Account.module.css │ │ │ └── Account.tsx │ ├── helpers │ │ ├── array.tsx │ │ ├── types.ts │ │ ├── consts.ts │ │ ├── invoke.ts │ │ └── toast.tsx │ ├── App.module.css │ ├── index.tsx │ ├── App.test.tsx │ ├── index.css │ └── App.tsx ├── .gitignore ├── index.html ├── README.md ├── tsconfig.json ├── eslint.config.mjs ├── vite.config.ts ├── .eslintrc.json └── package.json ├── pnpm-workspace.yaml ├── crates ├── mw-tools │ ├── src │ │ ├── api │ │ │ ├── mod.rs │ │ │ ├── parse.rs │ │ │ ├── purge.rs │ │ │ ├── edit.rs │ │ │ ├── delete.rs │ │ │ ├── upload.rs │ │ │ ├── download.rs │ │ │ ├── rename.rs │ │ │ └── list.rs │ │ ├── lib.rs │ │ ├── response │ │ │ ├── edit.rs │ │ │ ├── parse.rs │ │ │ ├── rename.rs │ │ │ ├── upload.rs │ │ │ ├── delete.rs │ │ │ ├── token.rs │ │ │ ├── login.rs │ │ │ ├── download.rs │ │ │ ├── mod.rs │ │ │ └── list.rs │ │ ├── error.rs │ │ └── client.rs │ └── Cargo.toml └── storage │ ├── Cargo.toml │ └── src │ └── lib.rs ├── Cargo.toml ├── cli ├── Cargo.toml └── src │ └── main.rs ├── renovate.json ├── README.md ├── LICENSE └── .github └── workflows ├── ci.yml └── publish.yml /.gitignore: -------------------------------------------------------------------------------- 1 | target/ -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shared-workspace-lockfile = false -------------------------------------------------------------------------------- /gui/src-tauri/src/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build(); 3 | } 4 | -------------------------------------------------------------------------------- /gui/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FabianLars/mw-toolbox/HEAD/gui/favicon.ico -------------------------------------------------------------------------------- /gui/setup-test.ts: -------------------------------------------------------------------------------- 1 | globalThis.IS_REACT_ACT_ENVIRONMENT = true; 2 | import '@testing-library/jest-dom'; 3 | -------------------------------------------------------------------------------- /gui/src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FabianLars/mw-toolbox/HEAD/gui/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /gui/src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FabianLars/mw-toolbox/HEAD/gui/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'gui' 3 | # workspace file needed to fix renovate lock-file-maintenance 4 | -------------------------------------------------------------------------------- /gui/src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FabianLars/mw-toolbox/HEAD/gui/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /gui/src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FabianLars/mw-toolbox/HEAD/gui/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /gui/src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FabianLars/mw-toolbox/HEAD/gui/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /gui/src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FabianLars/mw-toolbox/HEAD/gui/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /gui/src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FabianLars/mw-toolbox/HEAD/gui/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /gui/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "tabWidth": 4, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /gui/src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FabianLars/mw-toolbox/HEAD/gui/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /gui/src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FabianLars/mw-toolbox/HEAD/gui/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /gui/src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FabianLars/mw-toolbox/HEAD/gui/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /gui/src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FabianLars/mw-toolbox/HEAD/gui/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /gui/src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FabianLars/mw-toolbox/HEAD/gui/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /gui/src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FabianLars/mw-toolbox/HEAD/gui/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /gui/src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FabianLars/mw-toolbox/HEAD/gui/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /gui/src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FabianLars/mw-toolbox/HEAD/gui/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /gui/src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FabianLars/mw-toolbox/HEAD/gui/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /gui/src/components/icons/index.ts: -------------------------------------------------------------------------------- 1 | import CloseIcon from './CloseIcon'; 2 | import PlusIcon from './PlusIcon'; 3 | 4 | export { CloseIcon, PlusIcon }; 5 | -------------------------------------------------------------------------------- /crates/mw-tools/src/api/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod delete; 2 | pub mod download; 3 | pub mod edit; 4 | pub mod list; 5 | pub mod parse; 6 | pub mod purge; 7 | pub mod rename; 8 | pub mod upload; 9 | -------------------------------------------------------------------------------- /crates/mw-tools/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | 3 | pub use client::Client; 4 | pub use error::Error; 5 | 6 | mod client; 7 | mod error; 8 | 9 | pub mod api; 10 | pub mod response; 11 | -------------------------------------------------------------------------------- /gui/src/components/layout/index.ts: -------------------------------------------------------------------------------- 1 | import Divider from './Divider/Divider'; 2 | import Header from './Header/Header'; 3 | import Modal from './Modal/Modal'; 4 | 5 | export { Divider, Header, Modal }; 6 | -------------------------------------------------------------------------------- /gui/src/components/feedback/index.ts: -------------------------------------------------------------------------------- 1 | import Badge from './Badge/Badge'; 2 | import Spinner from './Spinner/Spinner'; 3 | import { toast } from './Toast/ToastManager'; 4 | 5 | export { Badge, Spinner, toast }; 6 | -------------------------------------------------------------------------------- /gui/src/pages/Download/Download.module.css: -------------------------------------------------------------------------------- 1 | .area { 2 | margin-bottom: 1rem; 3 | flex: 1; 4 | } 5 | 6 | .container { 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | height: 100%; 11 | width: 100%; 12 | } 13 | -------------------------------------------------------------------------------- /gui/src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | WixTools 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | config.json 10 | bundle.json 11 | 12 | Cargo.lock 13 | 14 | gen/schemas -------------------------------------------------------------------------------- /crates/mw-tools/src/response/edit.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Deserialize)] 4 | pub struct Edit { 5 | pub edit: Response, 6 | } 7 | 8 | #[derive(Debug, Deserialize)] 9 | pub struct Response { 10 | pub result: String, 11 | pub title: String, 12 | } 13 | -------------------------------------------------------------------------------- /gui/src/helpers/array.tsx: -------------------------------------------------------------------------------- 1 | const removeFirst = (array: string[], element: string): string[] => { 2 | const index = array.indexOf(element); 3 | if (index === -1) return array; 4 | return [...array.slice(0, index), ...array.slice(index + 1)]; 5 | }; 6 | 7 | export { removeFirst }; 8 | -------------------------------------------------------------------------------- /gui/src/components/forms/Label/Label.module.css: -------------------------------------------------------------------------------- 1 | .label { 2 | display: block; 3 | margin-bottom: 0.5rem; 4 | font-weight: 500; 5 | white-space: nowrap; 6 | } 7 | 8 | .indicator { 9 | color: var(--color-red); 10 | margin-inline-start: 0.25rem; 11 | } 12 | 13 | .disabled { 14 | opacity: 0.4; 15 | } 16 | -------------------------------------------------------------------------------- /gui/src/components/forms/index.ts: -------------------------------------------------------------------------------- 1 | import Button from './Button/Button'; 2 | import Checkbox from './Checkbox/Checkbox'; 3 | import Input from './Input/Input'; 4 | import Label from './Label/Label'; 5 | import Select from './Select/Select'; 6 | import Textarea from './Textarea/Textarea'; 7 | 8 | export { Button, Checkbox, Input, Label, Select, Textarea }; 9 | -------------------------------------------------------------------------------- /crates/mw-tools/src/api/parse.rs: -------------------------------------------------------------------------------- 1 | use crate::{response::parse::Parse, Client, Error}; 2 | 3 | pub async fn get_page_content(client: &Client, page: &str) -> Result { 4 | let res: Parse = client 5 | .get(&[("action", "parse"), ("prop", "wikitext"), ("page", page)]) 6 | .await?; 7 | 8 | Ok(res.parse.wikitext) 9 | } 10 | -------------------------------------------------------------------------------- /crates/mw-tools/src/response/parse.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Deserialize)] 4 | pub(crate) struct Parse { 5 | pub(crate) parse: Response, 6 | } 7 | 8 | #[derive(Debug, Deserialize)] 9 | pub(crate) struct Response { 10 | #[allow(dead_code)] 11 | pub(crate) title: String, 12 | pub(crate) wikitext: String, 13 | } 14 | -------------------------------------------------------------------------------- /crates/mw-tools/src/response/rename.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Deserialize)] 4 | pub(crate) struct Rename { 5 | #[serde(rename = "move")] 6 | pub(crate) rename: Response, 7 | } 8 | 9 | #[derive(Debug, Deserialize)] 10 | pub(crate) struct Response { 11 | pub(crate) from: String, 12 | pub(crate) to: String, 13 | } 14 | -------------------------------------------------------------------------------- /crates/mw-tools/src/response/upload.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Deserialize)] 4 | pub(crate) struct Upload { 5 | pub(crate) upload: Response, 6 | } 7 | 8 | #[derive(Debug, Deserialize)] 9 | pub(crate) struct Response { 10 | pub(crate) result: String, 11 | #[allow(dead_code)] 12 | pub(crate) filename: String, 13 | } 14 | -------------------------------------------------------------------------------- /gui/src/components/feedback/Badge/Badge.tsx: -------------------------------------------------------------------------------- 1 | import cls from './Badge.module.css'; 2 | 3 | type Props = { 4 | children: React.ReactNode; 5 | type?: 'success' | 'error'; 6 | }; 7 | 8 | const Badge = ({ children, type }: Props) => { 9 | return {children}; 10 | }; 11 | 12 | export default Badge; 13 | -------------------------------------------------------------------------------- /gui/src/components/forms/Checkbox/Checkbox.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | white-space: nowrap; 6 | } 7 | .wrapper > label { 8 | margin-left: 0.5rem; 9 | } 10 | .disabled { 11 | opacity: 0.4; 12 | cursor: not-allowed; 13 | } 14 | .disabled > * { 15 | cursor: not-allowed; 16 | } 17 | -------------------------------------------------------------------------------- /gui/src/App.module.css: -------------------------------------------------------------------------------- 1 | .center { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | flex: 1 1 auto; 6 | padding: 1rem; 7 | overflow: hidden; 8 | } 9 | 10 | .container { 11 | display: flex; 12 | flex-direction: column; 13 | height: 100vh; 14 | width: 100vw; 15 | user-select: none; 16 | -webkit-user-select: none; 17 | } 18 | -------------------------------------------------------------------------------- /gui/src/components/layout/Divider/Divider.module.css: -------------------------------------------------------------------------------- 1 | .hr { 2 | opacity: 0.6; 3 | border: 0; 4 | border-color: var(--border-color); 5 | border-style: solid; 6 | } 7 | 8 | .horizontal { 9 | border-bottom-width: 1px; 10 | margin: 0.25rem 0; 11 | width: 100%; 12 | } 13 | 14 | .vertical { 15 | border-left-width: 1px; 16 | margin: 0 0.25rem; 17 | height: 100%; 18 | } 19 | -------------------------------------------------------------------------------- /crates/mw-tools/src/response/delete.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Deserialize)] 4 | pub(crate) struct Delete { 5 | #[allow(dead_code)] 6 | pub(crate) delete: Response, 7 | } 8 | 9 | #[derive(Debug, Deserialize)] 10 | pub(crate) struct Response { 11 | #[allow(dead_code)] 12 | pub(crate) title: String, 13 | #[allow(dead_code)] 14 | pub(crate) reason: String, 15 | } 16 | -------------------------------------------------------------------------------- /gui/src/components/feedback/Spinner/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import cls from './Spinner.module.css'; 2 | 3 | type Props = { className?: string }; 4 | 5 | const Spinner = ({ className = '' }: Props) => { 6 | return ( 7 |
8 | {/* Loading... */} 9 |
10 | ); 11 | }; 12 | 13 | export default Spinner; 14 | -------------------------------------------------------------------------------- /gui/src-tauri/capabilities/desktop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "desktop-capability", 4 | "platforms": ["macOS", "windows", "linux"], 5 | "windows": ["main"], 6 | "permissions": [ 7 | "core:default", 8 | "dialog:default", 9 | "dialog:allow-open", 10 | "opener:default", 11 | "updater:default" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /gui/src/pages/Purge/Purge.module.css: -------------------------------------------------------------------------------- 1 | .area { 2 | flex: 1; 3 | margin-bottom: 1rem; 4 | } 5 | 6 | .buttons { 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | } 11 | 12 | .container { 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | height: 100%; 17 | width: 100%; 18 | } 19 | 20 | .mx { 21 | margin: 0 0.5rem; 22 | } 23 | -------------------------------------------------------------------------------- /gui/src/pages/index.ts: -------------------------------------------------------------------------------- 1 | import Account from './Account/Account'; 2 | import Delete from './Delete/Delete'; 3 | import Download from './Download/Download'; 4 | import Edit from './Edit/Edit'; 5 | import List from './List/List'; 6 | import Move from './Move/Move'; 7 | import Purge from './Purge/Purge'; 8 | import Upload from './Upload/Upload'; 9 | 10 | export { Account, Delete, Download, Edit, List, Move, Purge, Upload }; 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "cli", 4 | "crates/mw-tools", 5 | "crates/storage", 6 | "gui/src-tauri", 7 | ] 8 | resolver = "2" 9 | 10 | [profile.dev] 11 | # Disabling debug info speeds up builds a bunch, 12 | # and we don't rely on it for debugging that much. 13 | debug = 0 14 | 15 | [profile.release] 16 | codegen-units = 1 17 | lto = true 18 | opt-level = "s" #"z" 19 | panic = "abort" 20 | strip = "symbols" -------------------------------------------------------------------------------- /gui/src/components/index.ts: -------------------------------------------------------------------------------- 1 | import { Badge, Spinner, toast } from './feedback'; 2 | import { Button, Checkbox, Input, Label, Select, Textarea } from './forms'; 3 | import { Divider, Header, Modal } from './layout'; 4 | 5 | export { 6 | Badge, 7 | Button, 8 | Checkbox, 9 | Divider, 10 | Header, 11 | Input, 12 | Label, 13 | Modal, 14 | Select, 15 | Spinner, 16 | Textarea, 17 | toast, 18 | }; 19 | -------------------------------------------------------------------------------- /gui/src/helpers/types.ts: -------------------------------------------------------------------------------- 1 | type FocusableElement = { 2 | focus(options?: FocusOptions): void; 3 | }; 4 | 5 | type Profile = { 6 | // fix dynamic indexing in Account.tsx 7 | [index: string]: string | boolean; 8 | profile: string; 9 | username: string; 10 | password: string; 11 | url: string; 12 | savePassword: boolean; 13 | isOnline: boolean; 14 | }; 15 | 16 | export type { FocusableElement, Profile }; 17 | -------------------------------------------------------------------------------- /crates/mw-tools/src/response/token.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Deserialize)] 4 | pub(crate) struct Token { 5 | pub(crate) query: Query, 6 | } 7 | 8 | #[derive(Debug, Deserialize)] 9 | pub(crate) struct Query { 10 | pub(crate) tokens: Tokens, 11 | } 12 | 13 | #[derive(Debug, Deserialize)] 14 | pub(crate) struct Tokens { 15 | pub(crate) logintoken: Option, 16 | pub(crate) csrftoken: Option, 17 | } 18 | -------------------------------------------------------------------------------- /gui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /dist 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .eslintcache 26 | 27 | .snowpack -------------------------------------------------------------------------------- /gui/src/pages/Upload/FileList.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'react'; 2 | import cls from './FileList.module.css'; 3 | 4 | const FileList = ({ children, placeholder }: { children: JSX.Element[]; placeholder?: string }) => { 5 | return ( 6 |
7 | {children.length === 0 ? placeholder : children} 8 |
9 | ); 10 | }; 11 | 12 | export default FileList; 13 | -------------------------------------------------------------------------------- /gui/src/helpers/consts.ts: -------------------------------------------------------------------------------- 1 | export const routes = ['/', '/Delete', '/Download', '/Edit', '/List', '/Move', '/Purge', '/Upload']; 2 | 3 | export const categories = [ 4 | 'allcategories', 5 | 'allimages', 6 | 'allinfoboxes', 7 | 'alllinks', 8 | 'allpages', 9 | 'backlinks', 10 | 'categorymembers', 11 | 'embeddedin', 12 | 'exturlusage', 13 | 'imageusage', 14 | 'protectedtitles', 15 | 'querypage', 16 | 'search', 17 | ]; 18 | -------------------------------------------------------------------------------- /gui/src/components/layout/Divider/Divider.tsx: -------------------------------------------------------------------------------- 1 | import cls from './Divider.module.css'; 2 | 3 | type Props = { 4 | orientation?: 'horizontal' | 'vertical'; 5 | }; 6 | 7 | const Divider = ({ orientation = 'horizontal' }: Props) => { 8 | return ( 9 |
13 | ); 14 | }; 15 | 16 | export default Divider; 17 | -------------------------------------------------------------------------------- /crates/mw-tools/src/response/login.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Default, Deserialize)] 4 | #[serde(default)] 5 | pub(crate) struct Login { 6 | pub(crate) login: Response, 7 | } 8 | 9 | #[derive(Debug, Default, Deserialize)] 10 | #[serde(default)] 11 | pub(crate) struct Response { 12 | pub(crate) result: String, 13 | pub(crate) reason: Option, 14 | pub(crate) lguserid: u64, 15 | pub(crate) lgusername: String, 16 | } 17 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["FabianLars "] 3 | default-run = "mw-cli" 4 | edition = "2021" 5 | name = "mw-cli" 6 | publish = false 7 | version = "0.1.0" 8 | 9 | [dependencies] 10 | mw-tools = {path = "../crates/mw-tools"} 11 | 12 | anyhow = "1" 13 | clap = {version = "4", features = ["derive", "env"]} 14 | pretty_env_logger = "0.5" 15 | serde_json = {version = "1"} 16 | tokio = {version = "1", features = ["fs", "macros", "rt-multi-thread"]} 17 | -------------------------------------------------------------------------------- /gui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './App'; 4 | import './index.css'; 5 | 6 | declare global { 7 | interface Window { 8 | OS: string; 9 | __TAURI__: unknown; 10 | } 11 | } 12 | 13 | const container = document.getElementById('root') as HTMLDivElement; 14 | const root = createRoot(container); 15 | root.render( 16 | 17 | 18 | , 19 | ); 20 | -------------------------------------------------------------------------------- /gui/src/pages/Move/Move.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | height: 100%; 6 | width: 100%; 7 | } 8 | 9 | .fields { 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | flex: 1; 14 | width: 100%; 15 | margin-bottom: 1rem; 16 | } 17 | 18 | .from { 19 | height: 100%; 20 | margin-right: 0.5rem; 21 | } 22 | 23 | .to { 24 | height: 100%; 25 | margin-left: 0.5rem; 26 | } 27 | -------------------------------------------------------------------------------- /gui/src/components/icons/PlusIcon.tsx: -------------------------------------------------------------------------------- 1 | const PlusIcon = () => { 2 | return ( 3 | 4 | 8 | 9 | ); 10 | }; 11 | 12 | export default PlusIcon; 13 | -------------------------------------------------------------------------------- /gui/src/pages/Delete/Delete.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | 9 | .input { 10 | width: 100%; 11 | align-self: flex-end; 12 | } 13 | 14 | @media screen and (min-width: 48em) { 15 | .input { 16 | width: 75%; 17 | } 18 | } 19 | 20 | @media screen and (min-width: 62em) { 21 | .input { 22 | width: 50%; 23 | } 24 | } 25 | 26 | .area { 27 | margin: 1rem 0; 28 | flex: 1; 29 | } 30 | -------------------------------------------------------------------------------- /crates/mw-tools/src/response/download.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Deserialize)] 4 | pub(crate) struct Imageinfo { 5 | pub(crate) query: Query, 6 | } 7 | 8 | #[derive(Debug, Deserialize)] 9 | pub(crate) struct Query { 10 | pub(crate) pages: Vec, 11 | } 12 | 13 | #[derive(Debug, Deserialize)] 14 | pub(crate) struct Page { 15 | pub(crate) title: String, 16 | pub(crate) imageinfo: Option>, 17 | } 18 | 19 | #[derive(Debug, Deserialize)] 20 | pub(crate) struct Info { 21 | pub(crate) url: String, 22 | } 23 | -------------------------------------------------------------------------------- /crates/storage/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["FabianLars "] 3 | edition = "2021" 4 | name = "storage" 5 | publish = false 6 | version = "0.1.0" 7 | 8 | [dependencies] 9 | anyhow = "1.0" 10 | bincode = "1.3" 11 | chacha20poly1305 = "0.10" 12 | dirs-next = "2" 13 | once_cell = "1.19" 14 | rand_chacha = "0.3" 15 | serde = {version = "1", features = ["derive"]} 16 | tokio = {version = "1", features = ["fs", "io-util"]} 17 | 18 | [dev-dependencies] 19 | rand = "0.8" 20 | tokio = {version = "*", features = ["macros", "rt-multi-thread"]} 21 | -------------------------------------------------------------------------------- /gui/src/components/icons/CloseIcon.tsx: -------------------------------------------------------------------------------- 1 | const CloseIcon = () => { 2 | return ( 3 | 4 | 8 | 9 | ); 10 | }; 11 | 12 | export default CloseIcon; 13 | -------------------------------------------------------------------------------- /crates/mw-tools/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["FabianLars "] 3 | edition = "2021" 4 | license = "MIT" 5 | name = "mw-tools" 6 | version = "0.1.0" 7 | 8 | [dependencies] 9 | directories-next = "2" 10 | futures-util = {version = "0.3", default-features = false, features = ["alloc"]} 11 | log = "0.4" 12 | regex = "1" 13 | reqwest = {version = "0.12", features = ["json", "cookies", "multipart"]} 14 | serde = {version = "1", features = ["derive"]} 15 | serde_json = "1" 16 | thiserror = "2" 17 | tokio = {version = "1", features = ["fs", "time"]} 18 | -------------------------------------------------------------------------------- /gui/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { afterEach, describe, expect, it, vi } from 'vitest'; 3 | import App from './App'; 4 | 5 | // Placeholder test 6 | 7 | vi.mock('react-dom/client', () => { 8 | return { 9 | createRoot: vi.fn(), 10 | }; 11 | }); 12 | 13 | describe('', () => { 14 | afterEach(() => { 15 | vi.clearAllMocks(); 16 | }); 17 | 18 | it('renders "Profile 1"', async () => { 19 | render(); 20 | expect(screen.getByText(/Profile Name/i)).toBeDefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /gui/src/components/feedback/Badge/Badge.module.css: -------------------------------------------------------------------------------- 1 | .badge { 2 | display: inline-block; 3 | white-space: nowrap; 4 | vertical-align: middle; 5 | padding: 0 0.25rem; 6 | margin: 0.5rem; 7 | text-transform: uppercase; 8 | font-size: 0.75rem; 9 | border-radius: 0.125rem; 10 | font-weight: 700; 11 | background: rgba(226, 232, 240, 0.16); 12 | color: #e2e8f0; 13 | } 14 | 15 | .success { 16 | background: rgba(154, 230, 180, 0.16); 17 | color: #9ae6b4; 18 | } 19 | 20 | .error { 21 | background: rgba(254, 178, 178, 0.16); 22 | color: #feb2b2; 23 | } 24 | -------------------------------------------------------------------------------- /gui/src/helpers/invoke.ts: -------------------------------------------------------------------------------- 1 | import { invoke } from '@tauri-apps/api/core'; 2 | 3 | /** 4 | * Get an object from the cache managed by rust. 5 | */ 6 | const getCache = async (key: string): Promise => { 7 | return await invoke('cache_get', { key }); 8 | }; 9 | 10 | /** 11 | * Store an object in rust. 12 | * 13 | * Returns a boolean indicating if a value behind the key already existed and therefore got updated. 14 | */ 15 | const setCache = async (key: string, value: unknown): Promise => { 16 | return await invoke('cache_set', { key, value }); 17 | }; 18 | 19 | export { getCache, setCache }; 20 | -------------------------------------------------------------------------------- /crates/mw-tools/src/api/purge.rs: -------------------------------------------------------------------------------- 1 | use crate::{response::Ignore, Client, Error}; 2 | 3 | pub async fn purge(client: &Client, titles: &[&str], recursive: bool) -> Result<(), Error> { 4 | for chunk in titles.chunks(50) { 5 | client 6 | .post::(&[ 7 | ("action", "purge"), 8 | ("forcelinkupdate", "true"), 9 | ("forcerecursivelinkupdate", &recursive.to_string()), 10 | ("titles", &chunk.join("|")), 11 | ]) 12 | .await?; 13 | tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; 14 | } 15 | 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /gui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | mw-toolbox by FabianLars 10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended"], 4 | "postUpdateOptions": ["pnpmDedupe"], 5 | "packageRules": [ 6 | { 7 | "semanticCommitType": "chore", 8 | "matchPackageNames": ["*"] 9 | }, 10 | { 11 | "automerge": true, 12 | "automergeType": "branch", 13 | "matchUpdateTypes": ["patch", "pin", "digest"] 14 | }, 15 | { 16 | "automerge": true, 17 | "automergeType": "branch", 18 | "matchUpdateTypes": ["minor"], 19 | "matchCurrentVersion": ">=1.0.0" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /gui/README.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | Be sure to have all of tauri's dependencies installed: [linux](https://tauri.studio/en/docs/getting-started/setup-linux/) / [windows](https://tauri.studio/en/docs/getting-started/setup-windows/) / [macOS](https://tauri.studio/en/docs/getting-started/setup-macos/) 4 | 5 | 1. Start the react dev server: "pnpm|yarn|npm start" 6 | 2. Start tauri in dev mode: "pnpm|yarn|npm tauri dev" 7 | 8 | # Build 9 | 10 | 11 | 12 | Run "pnpm|yarn|npm tauri build". This automatically runs "pnpm build" to start vite's build process. (Note: edit "beforeBuildCommand" in tauri.conf.json if you're not using pnpm) 13 | -------------------------------------------------------------------------------- /gui/src/components/feedback/Toast/Toast.module.css: -------------------------------------------------------------------------------- 1 | .toast { 2 | padding: 0.5rem; 3 | background: #293347; 4 | border-radius: 0.375rem; 5 | border-width: 1px; 6 | box-shadow: rgba(0, 0, 0, 0.1) 0 0 0 1px, rgba(0, 0, 0, 0.2) 0 5px 10px, 7 | rgba(0, 0, 0, 0.4) 0 15px 40px; 8 | 9 | display: flex; 10 | justify-items: center; 11 | align-items: center; 12 | 13 | transform: translateY(50px); 14 | visibility: hidden; 15 | transition: opacity 0.2s, visibility linear 0.2s, transform 0.2s; 16 | opacity: 0; 17 | } 18 | 19 | .visible { 20 | opacity: 1; 21 | transform: translateY(0); 22 | visibility: visible; 23 | transition: opacity 0.2s, visibility 0.2s, transform 0.2s; 24 | } 25 | -------------------------------------------------------------------------------- /gui/src/components/feedback/Spinner/Spinner.module.css: -------------------------------------------------------------------------------- 1 | .spinner { 2 | display: inline-block; 3 | margin: 0.5rem; 4 | border: 2px solid #9b2c2c; 5 | border-radius: 99999px; 6 | border-bottom-color: transparent; 7 | border-left-color: transparent; 8 | width: 1.5rem; 9 | height: 1.5rem; 10 | animation: 0.45s linear 0s infinite normal none running spin; 11 | } 12 | 13 | @keyframes spin { 14 | 0% { 15 | transform: rotate(0deg); 16 | } 17 | 100% { 18 | transform: rotate(360deg); 19 | } 20 | } 21 | 22 | /* .span { 23 | height: 1px; 24 | width: 1px; 25 | margin: -1px; 26 | overflow: hidden; 27 | white-space: nowrap; 28 | clip: rect(0, 0, 0, 0); 29 | position: absolute; 30 | } */ 31 | -------------------------------------------------------------------------------- /gui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src", 4 | "paths": { 5 | "@/*": ["./*"] 6 | }, 7 | "target": "ESNext", 8 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 9 | "types": ["vite/client"], 10 | "allowJs": false, 11 | "skipLibCheck": true, 12 | "esModuleInterop": false, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": ["./src"] 24 | } 25 | -------------------------------------------------------------------------------- /gui/src/components/forms/Label/Label.tsx: -------------------------------------------------------------------------------- 1 | import cls from './Label.module.css'; 2 | 3 | type Props = { 4 | className?: string; 5 | children: React.ReactNode; 6 | htmlFor: string; 7 | isDisabled?: boolean; 8 | isRequired?: boolean; 9 | }; 10 | 11 | const Label = ({ children, className = '', htmlFor, isDisabled = false, isRequired }: Props) => { 12 | return ( 13 | 24 | ); 25 | }; 26 | 27 | export default Label; 28 | -------------------------------------------------------------------------------- /gui/src/helpers/toast.tsx: -------------------------------------------------------------------------------- 1 | import { Badge, toast } from '@/components'; 2 | 3 | const errorToast = (error: { code: string; description: string }): void => { 4 | // TODO: log description to status bar or something 5 | console.log('error', error.code, error.description); 6 | toast.show( 7 | <> 8 | ERROR 9 | {`Code: ${error.code}`} 10 | , 11 | ); 12 | }; 13 | 14 | const successToast = (message?: string, description?: string): void => { 15 | // TODO: log description to status bar or something 16 | console.log('success', message, description); 17 | toast.show( 18 | <> 19 | SUCCES 20 | {`${message}`} 21 | , 22 | ); 23 | }; 24 | 25 | export { errorToast, successToast }; 26 | -------------------------------------------------------------------------------- /gui/src/pages/Upload/FileList.module.css: -------------------------------------------------------------------------------- 1 | .flist { 2 | flex: 1; 3 | width: 100%; 4 | padding: 8px 16px; 5 | border: 1px solid rgba(255, 255, 255, 0.16); 6 | border-radius: 6px; 7 | user-select: text; 8 | overflow-y: auto; 9 | white-space: pre-line; 10 | 11 | /* flex: '1', 12 | width: '100%', 13 | padding: '8px 16px', 14 | border: '1px solid', 15 | borderColor: 'rgba(255, 255, 255, 0.16)', 16 | borderRadius: '6px', 17 | userSelect: children.length === 0 ? 'none' : 'text', 18 | overflowY: 'auto', 19 | whiteSpace: 'pre-line', 20 | opacity: children.length === 0 ? '0.5' : 'initial', */ 21 | } 22 | 23 | .empty { 24 | user-select: none; 25 | opacity: 0.5; 26 | } 27 | -------------------------------------------------------------------------------- /gui/src/pages/List/List.module.css: -------------------------------------------------------------------------------- 1 | .area { 2 | flex: 1; 3 | } 4 | 5 | .buttons { 6 | margin-top: 1rem; 7 | flex: 1 0 auto; 8 | align-self: flex-end; 9 | } 10 | 11 | .container { 12 | display: flex; 13 | flex-direction: column; 14 | width: 100%; 15 | height: 100%; 16 | } 17 | 18 | .endpoint { 19 | flex: 1 1 auto; 20 | width: 100%; 21 | } 22 | 23 | .fields { 24 | display: flex; 25 | align-items: center; 26 | flex-direction: column; 27 | width: 100%; 28 | margin-bottom: 1rem; 29 | } 30 | 31 | .mr { 32 | margin-right: 1rem; 33 | } 34 | 35 | .parameter { 36 | margin: 0.5rem 1rem 0; 37 | flex: 1 1 auto; 38 | width: 100%; 39 | } 40 | 41 | @media screen and (min-width: 48em) { 42 | .fields { 43 | flex-direction: row; 44 | } 45 | 46 | .parameter { 47 | margin-top: 0; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /gui/src/pages/Edit/FindReplaceModal.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | height: 100%; 5 | width: 100%; 6 | } 7 | 8 | .entry { 9 | display: flex; 10 | align-items: center; 11 | } 12 | 13 | .input { 14 | margin: 0.25rem; 15 | } 16 | 17 | .link { 18 | transition-property: var(--transition-property); 19 | transition-duration: 150ms; 20 | transition-timing-function: cubic-bezier(0, 0, 0.2, 1); 21 | cursor: pointer; 22 | text-decoration: none; 23 | outline: 2px solid transparent; 24 | outline-offset: 2px; 25 | } 26 | 27 | .link:focus { 28 | box-shadow: var(--shadow-outline); 29 | } 30 | 31 | .link:hover { 32 | text-decoration: underline; 33 | } 34 | 35 | .mr { 36 | margin-right: 0.5rem; 37 | } 38 | 39 | .regexinfo { 40 | font-size: small; 41 | float: right; 42 | padding-top: 0.3rem; 43 | } 44 | 45 | .spacer { 46 | flex: 1; 47 | } 48 | -------------------------------------------------------------------------------- /gui/src/components/forms/Input/Input.module.css: -------------------------------------------------------------------------------- 1 | .input { 2 | appearance: none; 3 | -webkit-appearance: none; 4 | background: inherit; 5 | border: 1px solid var(--border-color); 6 | border-radius: 0.35rem; 7 | height: 2.5rem; 8 | outline: 2px solid transparent; 9 | outline-offset: 2px; 10 | padding: 0 1rem; 11 | transition-duration: var(--transition-duration); 12 | transition-property: var(--transition-property); 13 | width: 100%; 14 | } 15 | 16 | .input:not([disabled]):not(:focus):hover { 17 | border-color: var(--border-color-hover); 18 | } 19 | 20 | .input:focus { 21 | border-color: var(--border-color-focus); 22 | box-shadow: var(--box-shadow-focus); 23 | } 24 | 25 | .input:not(:focus)[aria-invalid='true'] { 26 | border-color: var(--border-color-invalid); 27 | box-shadow: var(--box-shadow-invalid); 28 | } 29 | 30 | .input[disabled] { 31 | opacity: 0.4; 32 | cursor: not-allowed; 33 | } 34 | -------------------------------------------------------------------------------- /gui/src/components/layout/Header/Header.module.css: -------------------------------------------------------------------------------- 1 | .link { 2 | border-top: 1px solid transparent; 3 | padding: 0.75rem 1rem; 4 | border-radius: 5px; 5 | transition-property: var(--transition-property); 6 | transition-duration: 150ms; 7 | } 8 | .current { 9 | border-top-color: #718096; 10 | } 11 | .disabled { 12 | color: #9b2c2c; 13 | pointer-events: none; 14 | } 15 | .link:focus { 16 | box-shadow: var(--shadow-outline); 17 | } 18 | .link:hover { 19 | background: #293347; 20 | } 21 | 22 | .nav { 23 | display: flex; 24 | align-items: center; 25 | justify-content: left; 26 | width: 100%; 27 | padding: 0.5rem; 28 | border-bottom: 1px solid #deb992; 29 | } 30 | 31 | .spacer { 32 | flex: 1; 33 | } 34 | 35 | .wide { 36 | display: none; 37 | height: 50px; 38 | padding-right: 0.5rem; 39 | } 40 | 41 | @media screen and (min-width: 48em) { 42 | .wide { 43 | display: flex; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /gui/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import globals from 'globals'; 3 | import prettierConfig from 'eslint-config-prettier'; 4 | import reactHooks from 'eslint-plugin-react-hooks'; 5 | import reactRefresh from 'eslint-plugin-react-refresh'; 6 | import tseslint from 'typescript-eslint'; 7 | 8 | export default tseslint.config( 9 | { ignores: ['dist'] }, 10 | { 11 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 12 | files: ['**/*.{ts,tsx}'], 13 | languageOptions: { 14 | ecmaVersion: 2021, 15 | globals: globals.browser, 16 | }, 17 | plugins: { 18 | 'react-hooks': reactHooks, 19 | 'react-refresh': reactRefresh, 20 | }, 21 | rules: { 22 | ...reactHooks.configs.recommended.rules, 23 | 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], 24 | }, 25 | }, 26 | prettierConfig, 27 | ); 28 | -------------------------------------------------------------------------------- /gui/src/components/forms/Textarea/Textarea.module.css: -------------------------------------------------------------------------------- 1 | .area { 2 | appearance: none; 3 | -webkit-appearance: none; 4 | background: inherit; 5 | border: 1px solid var(--border-color); 6 | border-radius: 0.35rem; 7 | height: 2.5rem; 8 | outline: 2px solid transparent; 9 | outline-offset: 2px; 10 | padding: 8px 1rem 1px; 11 | transition-duration: var(--transition-duration); 12 | transition-property: var(--transition-property); 13 | width: 100%; 14 | resize: none; 15 | min-height: 80px; 16 | } 17 | 18 | .area:not([disabled]):not(:focus):hover { 19 | border-color: var(--border-color-hover); 20 | } 21 | 22 | .area:focus { 23 | border-color: var(--border-color-focus); 24 | box-shadow: var(--box-shadow-focus); 25 | } 26 | 27 | .area:not(:focus)[aria-invalid='true'] { 28 | border-color: var(--border-color-invalid); 29 | box-shadow: var(--box-shadow-invalid); 30 | } 31 | 32 | .area[disabled] { 33 | opacity: 0.4; 34 | cursor: not-allowed; 35 | } 36 | -------------------------------------------------------------------------------- /gui/src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["FabianLars "] 3 | build = "src/build.rs" 4 | description = "mw-toolbox GUI" 5 | edition = "2021" 6 | license = "MIT" 7 | name = "mw-toolbox" 8 | publish = false 9 | version = "0.1.0" 10 | 11 | [build-dependencies] 12 | tauri-build = { version = "2", features = [] } 13 | 14 | [dependencies] 15 | anyhow = "1" 16 | mw-tools = {path = "../../crates/mw-tools"} 17 | once_cell = "1" 18 | parking_lot = "0.12" 19 | pretty_env_logger = "0.5" 20 | regex = "1" 21 | reqwest = "0.12" 22 | serde = {version = "1", features = ["derive"] } 23 | serde_json = "1" 24 | storage = {path = "../../crates/storage"} 25 | tauri = {version = "2", features = [] } 26 | tauri-plugin-dialog = "2" 27 | tauri-plugin-opener = "2" 28 | tokio = {version = "1", features = ["sync", "time"] } 29 | unescape = "0.1" 30 | 31 | [[bin]] 32 | name = "mw-toolbox" 33 | path = "src/main.rs" 34 | 35 | [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] 36 | tauri-plugin-updater = "2" 37 | -------------------------------------------------------------------------------- /gui/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import { defineConfig } from 'vite'; 5 | import { resolve } from 'path'; 6 | import react from '@vitejs/plugin-react'; 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | plugins: [react()], 11 | resolve: { 12 | alias: { 13 | '@': resolve(__dirname, 'src'), 14 | }, 15 | }, 16 | server: { 17 | strictPort: true, 18 | }, 19 | clearScreen: false, 20 | envPrefix: ['VITE_', 'TAURI_'], 21 | build: { 22 | target: ['es2021', 'chrome100', 'safari13'], 23 | minify: !process.env.TAURI_DEBUG ? 'esbuild' : false, 24 | sourcemap: !!process.env.TAURI_DEBUG, 25 | }, 26 | test: { 27 | environment: 'jsdom', 28 | watch: false, 29 | globals: true, 30 | setupFiles: './setup-test.ts', 31 | deps: { 32 | inline: ['react-focus-lock', '@testing-library/user-event'], 33 | }, 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /gui/src/components/feedback/Toast/Toast.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import cls from './Toast.module.css'; 3 | 4 | type Props = { 5 | children: React.ReactNode; 6 | destroy: () => void; 7 | }; 8 | 9 | const Toast = ({ children, destroy }: Props) => { 10 | const [show, setShow] = useState(false); 11 | 12 | useEffect(() => { 13 | const timer = setTimeout(() => { 14 | setShow(false); 15 | setTimeout(() => { 16 | destroy(); 17 | }, 1000); 18 | }, 5000); 19 | 20 | return () => clearTimeout(timer); 21 | }, [destroy]); 22 | 23 | useEffect(() => { 24 | setTimeout(() => { 25 | setShow(true); 26 | }, 100); 27 | }, []); 28 | 29 | return ( 30 |
destroy()} 32 | role="alert" 33 | className={`${cls.toast} ${show ? cls.visible : ''}`} 34 | > 35 | {children} 36 |
37 | ); 38 | }; 39 | 40 | export default Toast; 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mw-toolbox 2 | 3 | some tools to interact with [fandom](https://community.fandom.com/) wikis 4 | 5 | uses limited editing rate of ~1-2 edits per second according to fandoms rules 6 | 7 | - GUI usage (development): 8 | - cd into gui subdir 9 | - run "pnpm|yarn|npm install" 10 | - run "pnpm|yarn|npm start" to start the dev server 11 | - run "pnpm|yarn|npm tauri dev" to start the tauri app 12 | - CLI usage (development): 13 | - Input files need to be formatted with newline seperation (eg 1 wiki article per line) 14 | - run via "cargo run \ (\)" inside cli folder 15 | - every command needs Fandom login credentails created via Special:BotPasswords. There are two ways to provide them: 16 | - the FANDOM_BOT_NAME & FANDOM_BOT_PASSWORD environment variables 17 | - cli flags: "cargo run [--loginname \|-n \] and [--loginpassword \|-p \]" 18 | - example: "cargo run delete ../todelete.txt" 19 | - deletes every page listed in specified file (separation via newline) 20 | -------------------------------------------------------------------------------- /gui/src/components/layout/Modal/Modal.module.css: -------------------------------------------------------------------------------- 1 | .modal { 2 | position: fixed; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate(-50%, -50%); 6 | z-index: 1000; 7 | background: #293347; 8 | border-radius: 0.375rem; 9 | border-width: 1px; 10 | outline: 2px solid transparent; 11 | outline-offset: 2px; 12 | box-shadow: rgba(0, 0, 0, 0.1) 0 0 0 1px, rgba(0, 0, 0, 0.2) 0 5px 10px, 13 | rgba(0, 0, 0, 0.4) 0 15px 40px; 14 | opacity: 0; 15 | } 16 | .modal.visible { 17 | opacity: 1; 18 | transition: opacity 0.2s; 19 | } 20 | 21 | .header { 22 | font-size: 1.25rem; 23 | font-weight: 500; 24 | padding: 1rem 1.5rem; 25 | } 26 | 27 | .body { 28 | padding: 0 1.5rem; 29 | } 30 | 31 | .footer { 32 | display: flex; 33 | padding: 1rem 1.5rem; 34 | } 35 | 36 | .overlay { 37 | position: fixed; 38 | top: 0; 39 | bottom: 0; 40 | left: 0; 41 | right: 0; 42 | background: transparent; 43 | z-index: 1000; 44 | } 45 | .overlay.visible { 46 | background: rgba(0, 0, 0, 0.7); 47 | transition: background 0.15s; 48 | } 49 | -------------------------------------------------------------------------------- /gui/src/components/feedback/Toast/ToastManager.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot, Root } from 'react-dom/client'; 2 | import Toast from './Toast'; 3 | 4 | export class ToastManager { 5 | private currentToast: React.ReactNode = null; 6 | private root: Root; 7 | 8 | constructor() { 9 | const toastContainer = document.getElementById('toast-portal') as HTMLDivElement; 10 | this.root = createRoot(toastContainer); 11 | } 12 | 13 | public show(message: React.ReactNode): void { 14 | if (this.currentToast) { 15 | this.destroy(); 16 | } 17 | 18 | this.currentToast = message; 19 | this.render(); 20 | } 21 | 22 | public destroy(): void { 23 | this.currentToast = null; 24 | this.root.unmount(); 25 | } 26 | 27 | private render() { 28 | this.root.render( 29 | this.currentToast ? ( 30 | this.destroy()}>{this.currentToast} 31 | ) : ( 32 | [] 33 | ), 34 | ); 35 | } 36 | } 37 | 38 | export const toast = new ToastManager(); 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 FabianLars 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 | -------------------------------------------------------------------------------- /gui/src/pages/Account/Account.module.css: -------------------------------------------------------------------------------- 1 | .add { 2 | width: 2.5rem; 3 | margin: 0 0.75rem; 4 | } 5 | .add svg, 6 | .remove svg { 7 | height: 1em; 8 | width: 1em; 9 | line-height: 1em; 10 | display: inline-block; 11 | vertical-align: middle; 12 | flex-shrink: 0; 13 | } 14 | 15 | .remove { 16 | width: 2.5rem; 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | justify-content: flex-end; 22 | width: 100%; 23 | height: 40px; 24 | margin-top: 0.5rem; 25 | } 26 | 27 | .container { 28 | display: flex; 29 | flex-direction: column; 30 | align-items: center; 31 | justify-content: center; 32 | width: 100%; 33 | } 34 | 35 | .profile { 36 | display: flex; 37 | width: 100%; 38 | align-items: flex-end; 39 | } 40 | .profile > div:first-child { 41 | flex: 2; 42 | margin-right: 0.75rem; 43 | } 44 | .profile > div:last-of-type { 45 | flex: 3; 46 | } 47 | 48 | @media screen and (min-width: 48em) { 49 | .container { 50 | width: 75%; 51 | } 52 | } 53 | 54 | @media screen and (min-width: 80em) { 55 | .container { 56 | width: 50%; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /gui/src/components/forms/Textarea/Textarea.tsx: -------------------------------------------------------------------------------- 1 | import cls from './Textarea.module.css'; 2 | 3 | type Props = { 4 | label?: string; 5 | className?: string; 6 | id?: string; 7 | name?: string; 8 | isDisabled?: boolean; 9 | placeholder?: string; 10 | readOnly?: boolean; 11 | value?: string | number; 12 | onBlur?: React.FocusEventHandler; 13 | onChange?: React.ChangeEventHandler; 14 | }; 15 | 16 | const Textarea = ({ 17 | label, 18 | className = '', 19 | id, 20 | name, 21 | isDisabled, 22 | placeholder, 23 | readOnly, 24 | value, 25 | onBlur, 26 | onChange, 27 | }: Props) => { 28 | return ( 29 | 59 |
60 | 69 |
70 | 71 | ); 72 | }; 73 | 74 | export default Delete; 75 | -------------------------------------------------------------------------------- /crates/mw-tools/src/api/rename.rs: -------------------------------------------------------------------------------- 1 | use crate::{response::rename::Rename, Client, Error}; 2 | 3 | pub async fn rename( 4 | client: &Client, 5 | from: Vec, 6 | to: Option, 7 | prepend: Option<&str>, 8 | append: Option<&str>, 9 | ) -> Result<(), Error> { 10 | let mut actual_destination: Vec = Vec::new(); 11 | 12 | if let Some(to) = to { 13 | match to { 14 | Destination::Plain(dest) => { 15 | if from.len() != dest.len() { 16 | return Err(Error::InvalidInput( 17 | "amount of from/to pages is not the same".to_string(), 18 | )); 19 | } 20 | actual_destination = dest; 21 | } 22 | Destination::Replace((replace, with)) => { 23 | for x in &from { 24 | actual_destination.push(x.replace(&replace, &with)); 25 | } 26 | } 27 | } 28 | } else { 29 | if prepend.is_none() && append.is_none() { 30 | return Err(Error::InvalidInput( 31 | "at least one of 'to', 'prepend' or 'append' needed".to_string(), 32 | )); 33 | } 34 | actual_destination = from.clone(); 35 | } 36 | 37 | if prepend.is_some() || append.is_some() { 38 | for x in &mut actual_destination { 39 | if let Some(p) = &prepend { 40 | x.insert_str(0, p); 41 | } 42 | if let Some(a) = &prepend { 43 | x.push_str(a); 44 | } 45 | } 46 | } 47 | 48 | for (x, y) in from.iter().zip(actual_destination.iter()) { 49 | let response: Result = client 50 | .post(&[ 51 | ("action", "move"), 52 | ("from", x), 53 | ("to", y), 54 | ("reason", "automated action"), 55 | ("movetalk", ""), 56 | ("movesubpages", ""), 57 | ("ignorewarnings", ""), 58 | ]) 59 | .await; 60 | 61 | log::debug!("{:?}", response); 62 | 63 | match response { 64 | Ok(m) => println!("{} => MOVED TO => {}", m.rename.from, m.rename.to), 65 | Err(err) => println!( 66 | "Error moving {} to {}: {}\nProceeding with next pages...", 67 | x, 68 | y, 69 | match err { 70 | Error::MediaWikiApi(err) => format!("{} - {}", err.code, err.description), 71 | _ => err.code().to_string(), 72 | } 73 | ), 74 | } 75 | 76 | tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; 77 | } 78 | 79 | Ok(()) 80 | } 81 | 82 | #[derive(Debug)] 83 | pub enum Destination { 84 | Plain(Vec), 85 | Replace((String, String)), 86 | } 87 | -------------------------------------------------------------------------------- /crates/mw-tools/src/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, thiserror::Error)] 2 | pub enum Error { 3 | /// MediaWiki API Errors 4 | #[error(transparent)] 5 | MediaWikiApi(#[from] super::response::Error), 6 | #[error("Couldn't extract token from response json: \"{0}\"")] 7 | TokenNotFound(String), 8 | #[error("Login failed! API returned: \"{0}\"")] 9 | LoginFailed(String), 10 | 11 | /// IOError 12 | #[error(transparent)] 13 | IoError(#[from] std::io::Error), 14 | 15 | /// Reqwest Errors 16 | #[error("Request timed out: \"{0}\"")] 17 | Timeout(String), 18 | #[error("Request returned non-200 status: \"{0}\"")] 19 | StatusCode(String), 20 | #[error("Error executing request: \"{0}\"")] 21 | RequestFailed(String), 22 | #[error("Error parsing body as json: \"{0}\"")] 23 | ParsingFailed(String), 24 | /// Catch-all reqwest errors 25 | #[error("HTTP Client Error: \"{0}\"")] 26 | HttpClient(String), 27 | /* /// Tauri Errors 28 | #[error("Tauri error: {0}")] 29 | TauriError(String), */ 30 | #[error("Invalid Input: \"{0}\"")] 31 | InvalidInput(String), 32 | 33 | #[error("{0}")] 34 | Other(String), 35 | } 36 | 37 | impl Error { 38 | #[must_use] 39 | pub const fn code(&self) -> &'static str { 40 | match self { 41 | Error::MediaWikiApi(_) => "MediaWikiaApi", 42 | Error::TokenNotFound(_) => "TokenNotFound", 43 | Error::LoginFailed(_) => "LoginFailed", 44 | Error::IoError(_) => "IoError", 45 | Error::Timeout(_) => "Timeout", 46 | Error::StatusCode(_) => "StatusCode", 47 | Error::RequestFailed(_) => "RequestFailed", 48 | Error::ParsingFailed(_) => "ParsingFailed", 49 | Error::HttpClient(_) => "HttpClient", 50 | Error::InvalidInput(_) => "InvalidInput", 51 | Error::Other(_) => "Other", 52 | } 53 | } 54 | } 55 | 56 | impl From for Error { 57 | fn from(source: reqwest::Error) -> Self { 58 | if source.is_timeout() { 59 | Self::Timeout(source.to_string()) 60 | } else if source.is_status() { 61 | Self::StatusCode(source.to_string()) 62 | } else if source.is_decode() { 63 | Self::ParsingFailed(source.to_string()) 64 | } else if source.is_request() { 65 | Self::RequestFailed(source.to_string()) 66 | } else { 67 | Self::HttpClient(source.to_string()) 68 | } 69 | } 70 | } 71 | 72 | impl serde::Serialize for Error { 73 | fn serialize(&self, serializer: S) -> Result 74 | where 75 | S: serde::ser::Serializer, 76 | { 77 | use serde::ser::SerializeStruct; 78 | 79 | let mut state = serializer.serialize_struct("Error", 2)?; 80 | state.serialize_field("code", &self.code())?; 81 | state.serialize_field("description", &self.to_string())?; 82 | state.end() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /gui/src/pages/Move/Move.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { invoke } from '@tauri-apps/api/core'; 3 | import { getCache, setCache } from '@/helpers/invoke'; 4 | import { errorToast, successToast } from '@/helpers/toast'; 5 | import { Button, Textarea } from '@/components'; 6 | import cls from './Move.module.css'; 7 | 8 | type Props = { 9 | isOnline: boolean; 10 | setNavDisabled: React.Dispatch>; 11 | }; 12 | 13 | const Move = ({ isOnline, setNavDisabled }: Props) => { 14 | const [isLoading, setIsLoading] = useState(false); 15 | const [areaFrom, setAreaFrom] = useState(''); 16 | const [areaTo, setAreaTo] = useState(''); 17 | 18 | const movePages = () => { 19 | setIsLoading(true); 20 | invoke('rename', { 21 | from: areaFrom.split(/\r?\n/), 22 | to: areaTo.split(/\r?\n/), 23 | }) 24 | .then(() => successToast('Successfully moved pages')) 25 | .catch(errorToast) 26 | .finally(() => setIsLoading(false)); 27 | }; 28 | 29 | useEffect(() => setNavDisabled(isLoading), [isLoading, setNavDisabled]); 30 | 31 | useEffect(() => { 32 | getCache('move-cache-from').then((cache) => { 33 | if (cache) setAreaFrom(cache); 34 | }); 35 | getCache('move-cache-to').then((cache) => { 36 | if (cache) setAreaTo(cache); 37 | }); 38 | }, []); 39 | 40 | return ( 41 |
42 |
43 |