├── .github └── workflows │ └── main.yml ├── .gitignore ├── .prettierrc ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── biome.json ├── components.json ├── index.html ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public └── favicon.ico ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── src │ └── main.rs └── tauri.conf.json ├── src ├── App.tsx ├── components │ ├── file │ │ ├── file-item.tsx │ │ ├── files-panel.tauri.tsx │ │ ├── files-panel.tsx │ │ └── files-panel.web.tsx │ ├── global │ │ ├── global-alert.tsx │ │ └── global-dialog.tsx │ ├── profile │ │ ├── profile-nav-list.tsx │ │ └── profile-nav.tsx │ ├── rule │ │ ├── rule-edit-dialog.tsx │ │ ├── rule-edit-panel.tsx │ │ ├── rule-form-render.tsx │ │ ├── rule-item.tsx │ │ ├── rule-type-froms │ │ │ ├── rule-delete-form.tsx │ │ │ ├── rule-format-form.tsx │ │ │ ├── rule-insert-form.tsx │ │ │ ├── rule-replace-form.tsx │ │ │ ├── rule-script-form-worker.ts │ │ │ ├── rule-script-form.module.css │ │ │ ├── rule-script-form.tsx │ │ │ └── rule-template-form.tsx │ │ └── rules-panel.tsx │ └── ui │ │ ├── alert-dialog.tsx │ │ ├── button.tsx │ │ ├── checkbox.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── radio-group.tsx │ │ ├── scroll-area.tsx │ │ └── switch.tsx ├── lib │ ├── atoms │ │ └── index.ts │ ├── file │ │ ├── file.tauri.ts │ │ ├── file.web.ts │ │ ├── index.ts │ │ └── type.ts │ ├── profile.ts │ ├── queries │ │ ├── file.ts │ │ └── profile.ts │ ├── query.ts │ ├── rule.ts │ ├── rules │ │ ├── base.ts │ │ ├── delete.ts │ │ ├── format.ts │ │ ├── index.ts │ │ ├── insert.ts │ │ ├── replace.ts │ │ ├── script.ts │ │ └── template.ts │ ├── store.ts │ ├── ui.tsx │ └── utils.ts ├── main.tsx ├── routeTree.gen.ts ├── routes │ ├── __root.tsx │ ├── index.tsx │ └── profile │ │ ├── $profileId.tsx │ │ └── route.tsx ├── styles.css └── vite-env.d.ts ├── static ├── preview.png └── web-preview.png ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: 'publish' 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | 9 | # This workflow will trigger on each push to the `release` branch to create or update a GitHub release, build your app, and upload the artifacts to the release. 10 | 11 | jobs: 12 | publish-tauri: 13 | permissions: 14 | contents: write 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | include: 19 | - platform: 'macos-latest' # for Arm based macs (M1 and above). 20 | args: '--target aarch64-apple-darwin' 21 | - platform: 'macos-latest' # for Intel based macs. 22 | args: '--target x86_64-apple-darwin' 23 | - platform: 'ubuntu-22.04' # for Tauri v1 you could replace this with ubuntu-20.04. 24 | args: '' 25 | - platform: 'windows-latest' 26 | args: '' 27 | 28 | runs-on: ${{ matrix.platform }} 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: pnpm/action-setup@v4 32 | with: 33 | version: 9 34 | - name: setup node 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version: '20' 38 | cache: 'pnpm' 39 | 40 | - name: install Rust stable 41 | uses: dtolnay/rust-toolchain@stable 42 | with: 43 | # Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds. 44 | targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} 45 | 46 | - name: install dependencies (ubuntu only) 47 | if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above. 48 | run: | 49 | sudo apt-get update 50 | sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf 51 | # webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2. 52 | # You can remove the one that doesn't apply to your app to speed up the workflow a bit. 53 | 54 | - name: install frontend dependencies 55 | run: pnpm install # change this to npm, pnpm or bun depending on which one you use. 56 | 57 | - uses: tauri-apps/tauri-action@v0 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | with: 61 | tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version. 62 | releaseName: 'v__VERSION__' 63 | releaseBody: '在assets下选择对应平台的程序' 64 | releaseDraft: true 65 | prerelease: false 66 | args: ${{ matrix.args }} 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "plugins": ["prettier-plugin-tailwindcss"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 2024 cyhuajuan 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 |

4 | 5 | FreeReNamer 6 | 7 |
8 | FreeReNamer 9 |

10 |

功能强大又易用的文件批量重命名软件

11 |

网页版在线体验:https://renamer.cyhuajuan.site

12 |

客户端下载:release

13 |

14 |
15 | 16 | ## 17 | 18 | ## 桌面版预览 19 | 20 | ![image](static/preview.png) 21 | 22 | ## 网页版预览 23 | 24 | ![image](static/web-preview.png) 25 | 26 | ## 网页端和桌面端的一些区别 27 | 28 | - 网页端使用的FileSystemHandle的move api,兼容性较差,推荐使用最新版本的chrome浏览器。客户端是原生API,兼容性没问题。 29 | - 网页端会有额外的操作权限确认,这是浏览器本身的安全处理。客户端没有。 30 | - 配置数据网页端是存储在indexedDB,桌面端是存储在文件 31 | 32 | ## 功能 33 | 34 | - 支持拖拽添加文件和文件夹 35 | - 支持创建多个配置 36 | - 单个配置内支持多个规则 37 | - 支持js脚本,内置Monaco Editor。 38 | - 跨平台,支持windows,macos,linux -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.7.3/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "linter": { 7 | "enabled": true, 8 | "rules": { 9 | "recommended": true, 10 | "nursery": { 11 | "useSortedClasses": "warn" 12 | }, 13 | "a11y": { 14 | "useButtonType": "off", 15 | "useKeyWithClickEvents": "off" 16 | }, 17 | "style": { 18 | "noNonNullAssertion": "off" 19 | }, 20 | "correctness": { 21 | "noUnusedImports": "error" 22 | } 23 | } 24 | }, 25 | "formatter": { 26 | "indentWidth": 2, 27 | "indentStyle": "space" 28 | }, 29 | "javascript": { 30 | "formatter": { 31 | "quoteStyle": "single" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/styles.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | FreeReNamer 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "free-renamer", 3 | "private": true, 4 | "version": "0.5.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "cross-env PLATFORM=web vite", 8 | "build": "cross-env-shell PLATFORM=web \"tsc && vite build\"", 9 | "preview": "vite preview", 10 | "tauri": "tauri", 11 | "dev-in-tauri": "cross-env PLATFORM=tauri vite", 12 | "build-in-tauri": "cross-env-shell PLATFORM=tauri \"tsc && vite build\"" 13 | }, 14 | "dependencies": { 15 | "@hookform/resolvers": "^3.3.4", 16 | "@radix-ui/react-alert-dialog": "^1.0.5", 17 | "@radix-ui/react-checkbox": "^1.0.4", 18 | "@radix-ui/react-context-menu": "^2.1.5", 19 | "@radix-ui/react-dialog": "^1.0.5", 20 | "@radix-ui/react-icons": "^1.3.0", 21 | "@radix-ui/react-label": "^2.0.2", 22 | "@radix-ui/react-radio-group": "^1.1.3", 23 | "@radix-ui/react-scroll-area": "^1.0.5", 24 | "@radix-ui/react-slot": "^1.0.2", 25 | "@radix-ui/react-switch": "^1.0.3", 26 | "@react-spring/web": "^9.7.3", 27 | "@tabler/icons-react": "^3.3.0", 28 | "@tanstack/react-query": "^5.35.1", 29 | "@tanstack/react-router": "^1.31.22", 30 | "@tauri-apps/api": "^1", 31 | "class-variance-authority": "^0.7.0", 32 | "clsx": "^2.1.1", 33 | "idb-keyval": "^6.2.1", 34 | "jotai": "^2.8.0", 35 | "lodash-es": "^4.17.21", 36 | "monaco-editor": "^0.48.0", 37 | "nanoid": "^5.0.7", 38 | "path-browserify": "^1.0.1", 39 | "react": "^18.2.0", 40 | "react-dom": "^18.2.0", 41 | "react-hook-form": "^7.51.4", 42 | "react-use-measure": "^2.1.1", 43 | "tailwind-merge": "^2.3.0", 44 | "tailwindcss-animate": "^1.0.7", 45 | "tauri-plugin-store-api": "github:tauri-apps/tauri-plugin-store#v1", 46 | "zod": "^3.23.8" 47 | }, 48 | "devDependencies": { 49 | "@biomejs/biome": "1.7.3", 50 | "@tanstack/router-devtools": "^1.31.22", 51 | "@tanstack/router-vite-plugin": "^1.31.18", 52 | "@tauri-apps/cli": "^1", 53 | "@types/lodash-es": "^4.17.12", 54 | "@types/node": "^20.12.10", 55 | "@types/path-browserify": "^1.0.2", 56 | "@types/react": "^18.2.15", 57 | "@types/react-dom": "^18.2.7", 58 | "@vitejs/plugin-react": "^4.2.1", 59 | "autoprefixer": "^10.4.19", 60 | "cross-env": "^7.0.3", 61 | "postcss": "^8.4.38", 62 | "tailwindcss": "^3.4.3", 63 | "typescript": "^5.0.2", 64 | "vite": "^5.0.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyhuajuan/FreeReNamer/79ffe88c290e4a5461f7c9e952a86e154d925306/public/favicon.ico -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Generated by Tauri 6 | # will have schema files for capabilities auto-completion 7 | /gen/schemas 8 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "free-renamer" 3 | version = "0.5.1" 4 | description = "A Tauri App" 5 | authors = ["cyhuajuan"] 6 | edition = "2021" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [build-dependencies] 11 | tauri-build = { version = "1", features = [] } 12 | 13 | [dependencies] 14 | tauri = { version = "1", features = [ "dialog-all", "path-all", "fs-all", "shell-open"] } 15 | serde = { version = "1", features = ["derive"] } 16 | serde_json = "1" 17 | tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } 18 | walkdir = "2.5.0" 19 | 20 | [features] 21 | # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! 22 | custom-protocol = ["tauri/custom-protocol"] 23 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyhuajuan/FreeReNamer/79ffe88c290e4a5461f7c9e952a86e154d925306/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyhuajuan/FreeReNamer/79ffe88c290e4a5461f7c9e952a86e154d925306/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyhuajuan/FreeReNamer/79ffe88c290e4a5461f7c9e952a86e154d925306/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyhuajuan/FreeReNamer/79ffe88c290e4a5461f7c9e952a86e154d925306/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyhuajuan/FreeReNamer/79ffe88c290e4a5461f7c9e952a86e154d925306/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyhuajuan/FreeReNamer/79ffe88c290e4a5461f7c9e952a86e154d925306/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyhuajuan/FreeReNamer/79ffe88c290e4a5461f7c9e952a86e154d925306/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyhuajuan/FreeReNamer/79ffe88c290e4a5461f7c9e952a86e154d925306/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyhuajuan/FreeReNamer/79ffe88c290e4a5461f7c9e952a86e154d925306/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyhuajuan/FreeReNamer/79ffe88c290e4a5461f7c9e952a86e154d925306/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyhuajuan/FreeReNamer/79ffe88c290e4a5461f7c9e952a86e154d925306/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyhuajuan/FreeReNamer/79ffe88c290e4a5461f7c9e952a86e154d925306/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyhuajuan/FreeReNamer/79ffe88c290e4a5461f7c9e952a86e154d925306/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyhuajuan/FreeReNamer/79ffe88c290e4a5461f7c9e952a86e154d925306/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyhuajuan/FreeReNamer/79ffe88c290e4a5461f7c9e952a86e154d925306/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyhuajuan/FreeReNamer/79ffe88c290e4a5461f7c9e952a86e154d925306/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | use std::{ffi::OsStr, fs}; 5 | use walkdir::WalkDir; 6 | 7 | // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command 8 | #[tauri::command] 9 | fn rename(old: &str, new: &str) -> Result<(), String> { 10 | fs::rename(old, new).map_err(|err| err.to_string())?; 11 | 12 | Ok(()) 13 | } 14 | 15 | #[tauri::command] 16 | fn exists(path: &str) -> bool { 17 | fs::metadata(path).is_ok() 18 | } 19 | 20 | #[tauri::command] 21 | fn is_file(path: &str) -> bool { 22 | exists(path) && fs::metadata(path).unwrap().is_file() 23 | } 24 | 25 | #[tauri::command] 26 | fn read_dir(path: &str) -> Result, String> { 27 | let mut files = Vec::new(); 28 | 29 | for entry in WalkDir::new(path) { 30 | let entry = entry.map_err(|err| err.to_string())?; 31 | 32 | if entry.file_type().is_file() { 33 | let file_path = match entry.path().to_str() { 34 | Some(path) => path, 35 | None => continue, 36 | }; 37 | 38 | files.push(file_path.to_string()); 39 | } 40 | } 41 | 42 | Ok(files) 43 | } 44 | 45 | #[tauri::command] 46 | fn basename(path: &str) -> String { 47 | let path = std::path::Path::new(path) 48 | .file_stem() 49 | .unwrap_or(OsStr::new("")); 50 | 51 | path.to_string_lossy().to_string() 52 | } 53 | 54 | fn main() { 55 | tauri::Builder::default() 56 | .invoke_handler(tauri::generate_handler![ 57 | rename, exists, is_file, read_dir, basename 58 | ]) 59 | .plugin(tauri_plugin_store::Builder::default().build()) 60 | .run(tauri::generate_context!()) 61 | .expect("error while running tauri application"); 62 | } 63 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "beforeDevCommand": "pnpm dev-in-tauri", 4 | "beforeBuildCommand": "pnpm build-in-tauri", 5 | "devPath": "http://localhost:1420", 6 | "distDir": "../dist" 7 | }, 8 | "package": { 9 | "productName": "FreeReNamer", 10 | "version": "0.5.1" 11 | }, 12 | "tauri": { 13 | "allowlist": { 14 | "all": false, 15 | "shell": { 16 | "all": false, 17 | "open": true 18 | }, 19 | "fs": { 20 | "all": true 21 | }, 22 | "path": { 23 | "all": true 24 | }, 25 | "dialog": { 26 | "all": true 27 | } 28 | }, 29 | "windows": [ 30 | { 31 | "title": "FreeReNamer", 32 | "width": 800, 33 | "height": 600 34 | } 35 | ], 36 | "security": { 37 | "csp": null 38 | }, 39 | "bundle": { 40 | "active": true, 41 | "targets": "all", 42 | "identifier": "com.cyhuajuan.free-renamer", 43 | "icon": [ 44 | "icons/32x32.png", 45 | "icons/128x128.png", 46 | "icons/128x128@2x.png", 47 | "icons/icon.icns", 48 | "icons/icon.ico" 49 | ] 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | 3 | export const App: FC = () => { 4 | return
App
; 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/file/file-item.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { useMemo, type FC } from 'react'; 3 | import { fileItemInfoQueryOptions } from '@/lib/queries/file'; 4 | import { atomStore, selectedFilesAtom } from '@/lib/atoms'; 5 | import { Checkbox } from '../ui/checkbox'; 6 | import { useAtomValue } from 'jotai'; 7 | 8 | export interface FileItemProps { 9 | file: string; 10 | profileId: string; 11 | index: number; 12 | } 13 | 14 | export const FileItem: FC = ({ file, profileId, index }) => { 15 | const { 16 | data: fileItemInfo, 17 | error, 18 | isError, 19 | } = useQuery(fileItemInfoQueryOptions(profileId, file, index)); 20 | 21 | const selectedFiles = useAtomValue(selectedFilesAtom); 22 | const selected = useMemo( 23 | () => selectedFiles.includes(file), 24 | [selectedFiles, file], 25 | ); 26 | 27 | function onCheckedChange(checked: boolean) { 28 | atomStore.set(selectedFilesAtom, (prev) => { 29 | if (checked) { 30 | return [...prev, file]; 31 | } 32 | 33 | return prev.filter((item) => item !== file); 34 | }); 35 | } 36 | 37 | if (isError) { 38 | return ( 39 |
40 |
41 | {error as unknown as string} 42 |
43 |
44 | ); 45 | } 46 | 47 | if (!fileItemInfo) { 48 | return null; 49 | } 50 | 51 | return ( 52 |
53 |
54 | 55 |
56 | 57 | {index + 1} 58 | 59 | 60 | {fileItemInfo.fileInfo.fullName} 61 | 62 | 63 | {fileItemInfo.preview} 64 | 65 |
66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /src/components/file/files-panel.tauri.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | atomStore, 3 | filesAtom, 4 | selectedFilesAtom, 5 | type FilesAtomTauri, 6 | } from '@/lib/atoms'; 7 | import { listen } from '@tauri-apps/api/event'; 8 | import { useAtomValue } from 'jotai'; 9 | import { useEffect, useMemo, type FC } from 'react'; 10 | import { FileItem } from './file-item'; 11 | import { Button } from '../ui/button'; 12 | import { ScrollArea } from '../ui/scroll-area'; 13 | import { open } from '@tauri-apps/api/dialog'; 14 | import { invoke } from '@tauri-apps/api'; 15 | import { Checkbox } from '../ui/checkbox'; 16 | 17 | export interface FilesPanelProps { 18 | profileId: string; 19 | } 20 | 21 | const FilesPanel: FC = ({ profileId }) => { 22 | const files = useAtomValue(filesAtom as FilesAtomTauri); 23 | const selectedFiles = useAtomValue(selectedFilesAtom); 24 | 25 | const checked = useMemo( 26 | () => files.length > 0 && selectedFiles.length === files.length, 27 | [selectedFiles, files], 28 | ); 29 | 30 | async function onAddFile() { 31 | const openFiles = await open({ multiple: true, directory: false }); 32 | window.showOpenFilePicker(); 33 | 34 | if (!Array.isArray(openFiles)) { 35 | return; 36 | } 37 | 38 | atomStore.set(filesAtom as FilesAtomTauri, (prevFiles) => [ 39 | ...new Set([...prevFiles, ...openFiles]), 40 | ]); 41 | } 42 | 43 | async function onAddDir() { 44 | const openDir = await open({ directory: true }); 45 | 46 | if (typeof openDir !== 'string') { 47 | return; 48 | } 49 | 50 | const files = await invoke('read_dir', { path: openDir }); 51 | 52 | atomStore.set(filesAtom as FilesAtomTauri, (prevFiles) => [ 53 | ...new Set([...prevFiles, ...files]), 54 | ]); 55 | } 56 | 57 | function onCheckedChange(checked: boolean) { 58 | atomStore.set(selectedFilesAtom as FilesAtomTauri, () => { 59 | if (checked) { 60 | return files.slice(); 61 | } 62 | 63 | return []; 64 | }); 65 | } 66 | 67 | function onRemove() { 68 | atomStore.set(filesAtom as FilesAtomTauri, (prevFiles) => 69 | prevFiles.filter((file) => !selectedFiles.includes(file)), 70 | ); 71 | atomStore.set(selectedFilesAtom, []); 72 | } 73 | 74 | useEffect(() => { 75 | let unlisten: (() => void) | undefined; 76 | 77 | listen('tauri://file-drop', async (e) => { 78 | if (!Array.isArray(e.payload)) { 79 | return; 80 | } 81 | 82 | const dropFiles: string[] = []; 83 | 84 | for (const item of e.payload as string[]) { 85 | const isFile = await invoke('is_file', { path: item }); 86 | 87 | if (isFile) { 88 | dropFiles.push(item); 89 | continue; 90 | } 91 | 92 | const files = await invoke('read_dir', { path: item }); 93 | 94 | dropFiles.push(...files); 95 | } 96 | 97 | atomStore.set(filesAtom as FilesAtomTauri, (prevFiles) => [ 98 | ...new Set([...prevFiles, ...dropFiles]), 99 | ]); 100 | }).then((unlistenFn) => { 101 | unlisten = unlistenFn; 102 | }); 103 | 104 | return () => { 105 | unlisten?.(); 106 | }; 107 | }, []); 108 | 109 | return ( 110 |
111 |
112 |
113 | 116 | 119 |
120 |
121 | {selectedFiles.length > 0 && ( 122 | 125 | )} 126 |
127 |
128 |
129 |
130 | 131 |
132 | 133 | 序号 134 | 135 | 文件名 136 | 预览 137 |
138 | 139 |
140 | {files.map((file, i) => { 141 | return ( 142 | 148 | ); 149 | })} 150 |
151 |
152 |
153 | ); 154 | }; 155 | 156 | export default FilesPanel; 157 | -------------------------------------------------------------------------------- /src/components/file/files-panel.tsx: -------------------------------------------------------------------------------- 1 | import { lazy, type FC } from 'react'; 2 | 3 | const FilesPanelTauri = lazy(() => import('./files-panel.tauri')); 4 | const FilesPanelWeb = lazy(() => import('./files-panel.web')); 5 | 6 | export interface FilesPanelProps { 7 | profileId: string; 8 | } 9 | 10 | export const FilesPanel: FC = ({ profileId }) => { 11 | if (__PLATFORM__ === __PLATFORM_TAURI__) { 12 | return ; 13 | } 14 | 15 | if (__PLATFORM__ === __PLATFORM_WEB__) { 16 | return ; 17 | } 18 | 19 | return null; 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/file/files-panel.web.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | atomStore, 3 | filesAtom, 4 | selectedFilesAtom, 5 | type FilesAtomWeb, 6 | } from '@/lib/atoms'; 7 | import { useAtomValue } from 'jotai'; 8 | import { useMemo, type FC } from 'react'; 9 | import { FileItem } from './file-item'; 10 | import { Button } from '../ui/button'; 11 | import { ScrollArea } from '../ui/scroll-area'; 12 | import { Checkbox } from '../ui/checkbox'; 13 | import { uniqBy } from 'lodash-es'; 14 | 15 | async function getAllFiles(directoryHandle: FileSystemDirectoryHandle) { 16 | const fileHandles: FileSystemFileHandle[] = []; 17 | 18 | for await (const fileHandle of directoryHandle.values()) { 19 | if (fileHandle.kind === 'file') { 20 | fileHandles.push(fileHandle); 21 | } else if (fileHandle.kind === 'directory') { 22 | fileHandles.push(...(await getAllFiles(fileHandle))); 23 | } 24 | } 25 | 26 | return fileHandles; 27 | } 28 | 29 | export interface FilesPanelProps { 30 | profileId: string; 31 | } 32 | 33 | const FilesPanel: FC = ({ profileId }) => { 34 | const files = useAtomValue(filesAtom as FilesAtomWeb); 35 | const selectedFiles = useAtomValue(selectedFilesAtom); 36 | 37 | const checked = useMemo( 38 | () => files.length > 0 && selectedFiles.length === files.length, 39 | [selectedFiles, files], 40 | ); 41 | 42 | async function onAddFile() { 43 | try { 44 | const result = await window.showOpenFilePicker({ multiple: true }); 45 | 46 | atomStore.set(filesAtom as FilesAtomWeb, (prevFile) => 47 | uniqBy([...prevFile, ...result], 'name'), 48 | ); 49 | } catch (err) {} 50 | } 51 | 52 | async function onAddDir() { 53 | try { 54 | const result = await window.showDirectoryPicker(); 55 | const files = await getAllFiles(result); 56 | 57 | atomStore.set(filesAtom as FilesAtomWeb, (prevFile) => 58 | uniqBy([...prevFile, ...files], 'name'), 59 | ); 60 | } catch (err) {} 61 | } 62 | 63 | function onCheckedChange(checked: boolean) { 64 | atomStore.set(selectedFilesAtom, () => { 65 | if (checked) { 66 | return files.slice().map((f) => f.name); 67 | } 68 | return []; 69 | }); 70 | } 71 | 72 | function onRemove() { 73 | atomStore.set(filesAtom as FilesAtomWeb, (prevFiles) => 74 | prevFiles.filter((file) => !selectedFiles.includes(file.name)), 75 | ); 76 | atomStore.set(selectedFilesAtom, []); 77 | } 78 | 79 | function preventDefault(e: React.DragEvent) { 80 | e.preventDefault(); 81 | e.stopPropagation(); 82 | } 83 | 84 | async function handleDrop(e: React.DragEvent) { 85 | try { 86 | preventDefault(e); 87 | 88 | const items = await Promise.all( 89 | [...e.dataTransfer.items].map((item) => item.getAsFileSystemHandle()), 90 | ); 91 | const files = ( 92 | await Promise.all( 93 | items.map((item) => { 94 | if (item?.kind === 'file') { 95 | return item; 96 | } 97 | 98 | if (item?.kind === 'directory') { 99 | return getAllFiles(item); 100 | } 101 | 102 | return null; 103 | }), 104 | ) 105 | ) 106 | .flat() 107 | .filter(Boolean) as FileSystemFileHandle[]; 108 | 109 | atomStore.set(filesAtom as FilesAtomWeb, (prevFile) => 110 | uniqBy([...prevFile, ...files], 'name'), 111 | ); 112 | } catch (err) {} 113 | } 114 | 115 | return ( 116 |
123 |
124 |
125 | 128 | 131 |
132 |
133 | {selectedFiles.length > 0 && ( 134 | 137 | )} 138 |
139 |
140 |
141 |
142 | 143 |
144 | 145 | 序号 146 | 147 | 文件名 148 | 预览 149 |
150 | 151 |
152 | {files.map((file, i) => { 153 | return ( 154 | 160 | ); 161 | })} 162 |
163 |
164 |
165 | ); 166 | }; 167 | 168 | export default FilesPanel; 169 | -------------------------------------------------------------------------------- /src/components/global/global-alert.tsx: -------------------------------------------------------------------------------- 1 | import { globalAlertInfoAtom } from '@/lib/atoms'; 2 | import { useAtom } from 'jotai'; 3 | import type { FC } from 'react'; 4 | import { 5 | AlertDialog, 6 | AlertDialogContent, 7 | AlertDialogDescription, 8 | AlertDialogFooter, 9 | AlertDialogHeader, 10 | AlertDialogTitle, 11 | } from '../ui/alert-dialog'; 12 | 13 | export const GlobalAlert: FC = () => { 14 | const [alertInfo, setAlertInfo] = useAtom(globalAlertInfoAtom); 15 | 16 | function onOpenChange(opened: boolean) { 17 | setAlertInfo((prev) => ({ ...prev, opened })); 18 | } 19 | 20 | return ( 21 | 22 | 23 | 24 | {alertInfo.title && ( 25 | {alertInfo.title} 26 | )} 27 | {alertInfo.description && ( 28 | 29 | {alertInfo.description} 30 | 31 | )} 32 | 33 | {alertInfo.footer && ( 34 | {alertInfo.footer} 35 | )} 36 | 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/global/global-dialog.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogDescription, 6 | DialogTitle, 7 | } from '../ui/dialog'; 8 | import { useAtom } from 'jotai'; 9 | import { globalDialogInfoAtom } from '@/lib/atoms'; 10 | 11 | export const GlobalDialog: FC = () => { 12 | const [dialogInfo, setDialogInfo] = useAtom(globalDialogInfoAtom); 13 | 14 | function onOpenChange(opened: boolean) { 15 | setDialogInfo((prev) => ({ ...prev, opened })); 16 | } 17 | 18 | return ( 19 | 20 | 21 | {dialogInfo.title && {dialogInfo.title}} 22 | {dialogInfo.description && ( 23 | {dialogInfo.description} 24 | )} 25 | {dialogInfo.children} 26 | 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/profile/profile-nav-list.tsx: -------------------------------------------------------------------------------- 1 | import { profileIdsQueryOptions } from '@/lib/queries/profile'; 2 | import { useQuery } from '@tanstack/react-query'; 3 | import type { FC } from 'react'; 4 | import { ProfileNav } from './profile-nav'; 5 | 6 | export const ProfileNavList: FC = () => { 7 | const { data: profileIds = [] } = useQuery(profileIdsQueryOptions); 8 | 9 | return ( 10 |
11 | {profileIds.map((profileId) => { 12 | return ( 13 | 18 | ); 19 | })} 20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/profile/profile-nav.tsx: -------------------------------------------------------------------------------- 1 | import { profileQueryOptions } from '@/lib/queries/profile'; 2 | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; 3 | import { Link, useNavigate, useParams } from '@tanstack/react-router'; 4 | import type { FC } from 'react'; 5 | import { 6 | ContextMenu, 7 | ContextMenuContent, 8 | ContextMenuItem, 9 | ContextMenuTrigger, 10 | } from '../ui/context-menu'; 11 | import { showConfirm, showRenameDialog } from '@/lib/ui'; 12 | import { delProfile, updateProfile } from '@/lib/profile'; 13 | import { QueryType } from '@/lib/query'; 14 | 15 | export interface ProfileNavProps { 16 | id: string; 17 | disableDel?: boolean; 18 | } 19 | 20 | export const ProfileNav: FC = ({ id, disableDel = false }) => { 21 | const queryClient = useQueryClient(); 22 | const params = useParams({ from: '/profile/$profileId' }); 23 | const navigate = useNavigate(); 24 | const { data: profile } = useQuery(profileQueryOptions(id)); 25 | const { mutate: rename } = useMutation({ 26 | mutationFn: async (name: string) => { 27 | return updateProfile(id, { name }); 28 | }, 29 | 30 | onSuccess: async () => { 31 | queryClient.invalidateQueries({ queryKey: [QueryType.Profile, { id }] }); 32 | }, 33 | }); 34 | const { mutate: del } = useMutation({ 35 | mutationFn: async () => { 36 | return delProfile(id); 37 | }, 38 | onSuccess: async () => { 39 | await queryClient.invalidateQueries({ queryKey: [QueryType.ProfileIds] }); 40 | await queryClient.invalidateQueries({ 41 | queryKey: [QueryType.Profile, { id }], 42 | }); 43 | 44 | if (params.profileId === id) { 45 | navigate({ to: '/' }); 46 | } 47 | }, 48 | }); 49 | 50 | function onRename() { 51 | showRenameDialog((name) => { 52 | rename(name); 53 | }); 54 | } 55 | 56 | function onDel() { 57 | showConfirm({ 58 | title: '确定删除?', 59 | description: '删除后数据无法恢复', 60 | onOk: () => { 61 | del(); 62 | }, 63 | }); 64 | } 65 | 66 | if (!profile) { 67 | return null; 68 | } 69 | 70 | return ( 71 | 72 | 73 | 78 | {profile.name} 79 | 80 | 81 | 82 | 重命名 83 | {!disableDel && 删除} 84 | 85 | 86 | ); 87 | }; 88 | -------------------------------------------------------------------------------- /src/components/rule/rule-edit-dialog.tsx: -------------------------------------------------------------------------------- 1 | import type { Rule } from '@/lib/rule'; 2 | import { 3 | Dialog, 4 | DialogClose, 5 | DialogContent, 6 | DialogHeader, 7 | DialogTitle, 8 | } from '../ui/dialog'; 9 | import type { FC } from 'react'; 10 | import { useForm } from 'react-hook-form'; 11 | import { Button } from '../ui/button'; 12 | import { RuleEditPanel } from './rule-edit-panel'; 13 | import { Form } from '../ui/form'; 14 | 15 | export interface RuleEditDialogContentProps { 16 | rule?: Rule; 17 | onSubmit: (values: Rule) => void; 18 | } 19 | 20 | export const RuleEditDialogContent: FC = ({ 21 | rule, 22 | onSubmit, 23 | }) => { 24 | const form = useForm({ 25 | defaultValues: rule, 26 | }); 27 | 28 | return ( 29 | <> 30 | 31 | 修改规则 32 | 33 |
34 | 39 | 40 |
41 | 42 | 43 | 44 | 45 |
46 | 47 | 48 | 49 | ); 50 | }; 51 | 52 | export interface RuleEditDialogProps { 53 | rule?: Rule; 54 | onSubmit: (values: Rule) => void; 55 | onOpenedChange: (open: boolean) => void; 56 | } 57 | 58 | export const RuleEditDialog: FC = ({ 59 | rule, 60 | onSubmit, 61 | onOpenedChange, 62 | }) => { 63 | const opened = !!rule; 64 | 65 | return ( 66 | 67 | 68 | {rule && } 69 | 70 | 71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /src/components/rule/rule-edit-panel.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | RULE_SCRIPT_TYPE, 3 | getRuleDefines, 4 | getRuleTypeDefaultValue, 5 | type Rule, 6 | } from '@/lib/rule'; 7 | import { useMemo, type FC } from 'react'; 8 | import { RuleFormRender } from './rule-form-render'; 9 | import { useFormContext } from 'react-hook-form'; 10 | import { FormControl, FormField, FormItem } from '../ui/form'; 11 | import { Input } from '../ui/input'; 12 | import { ScrollArea } from '../ui/scroll-area'; 13 | import { cn } from '@/lib/utils'; 14 | 15 | export interface RuleEditPanelProps { 16 | allowChangeType?: boolean; 17 | } 18 | 19 | export const RuleEditPanel: FC = ({ 20 | allowChangeType = true, 21 | }) => { 22 | const form = useFormContext(); 23 | const typeValue = form.watch('type'); 24 | const ruleDefines = useMemo(() => { 25 | return getRuleDefines(); 26 | }, []); 27 | 28 | return ( 29 |
30 | {allowChangeType && ( 31 | 32 |
33 | {ruleDefines.map((ruleDefine) => ( 34 |
37 | form.reset(getRuleTypeDefaultValue(ruleDefine.type)) 38 | } 39 | data-active={typeValue === ruleDefine.type || null} 40 | className="flex h-8 w-full cursor-default items-center justify-center rounded text-sm transition-colors data-[active]:bg-primary hover:bg-accent data-[active]:text-primary-foreground hover:text-accent-foreground" 41 | > 42 | {ruleDefine.label} 43 |
44 | ))} 45 |
46 |
47 | )} 48 |
49 |
50 | 规则名称 51 |
52 | ( 56 | 57 | 58 | 59 | 60 | 61 | )} 62 | /> 63 |
64 |
65 |
71 | 规则配置 72 | {typeValue === RULE_SCRIPT_TYPE ? ( 73 |
74 | 75 |
76 | ) : ( 77 | 78 |
79 | 80 |
81 |
82 | )} 83 |
84 |
85 |
86 | ); 87 | }; 88 | -------------------------------------------------------------------------------- /src/components/rule/rule-form-render.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense, lazy, type FC } from 'react'; 2 | import { RuleReplaceForm } from './rule-type-froms/rule-replace-form'; 3 | import { RuleDeleteForm } from './rule-type-froms/rule-delete-form'; 4 | import { IconLoader2 } from '@tabler/icons-react'; 5 | import { RuleFormatForm } from './rule-type-froms/rule-format-form'; 6 | import { RuleTemplateForm } from './rule-type-froms/rule-template-form'; 7 | import { 8 | RULE_DELETE_TYPE, 9 | RULE_FORMAT_TYPE, 10 | RULE_INSERT_TYPE, 11 | RULE_REPLACE_TYPE, 12 | RULE_SCRIPT_TYPE, 13 | RULE_TEMPLATE_TYPE, 14 | } from '@/lib/rules'; 15 | import { RuleInsertForm } from './rule-type-froms/rule-insert-form'; 16 | 17 | const RuleScriptForm = lazy(() => import('./rule-type-froms/rule-script-form')); 18 | 19 | export interface RuleFormRenderProps { 20 | type: string; 21 | } 22 | 23 | export const RuleFormRender: FC = ({ type }) => { 24 | switch (type) { 25 | case RULE_REPLACE_TYPE: 26 | return ; 27 | 28 | case RULE_DELETE_TYPE: 29 | return ; 30 | 31 | case RULE_FORMAT_TYPE: 32 | return ; 33 | 34 | case RULE_SCRIPT_TYPE: 35 | return ( 36 | 39 | 40 | 正在加载... 41 | 42 | } 43 | > 44 | 45 | 46 | ); 47 | 48 | case RULE_TEMPLATE_TYPE: 49 | return ; 50 | 51 | case RULE_INSERT_TYPE: 52 | return ; 53 | 54 | default: 55 | return null; 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /src/components/rule/rule-item.tsx: -------------------------------------------------------------------------------- 1 | import { getRuleDefine, type Rule } from '@/lib/rule'; 2 | import { useMemo, type FC } from 'react'; 3 | import { 4 | ContextMenu, 5 | ContextMenuContent, 6 | ContextMenuItem, 7 | ContextMenuTrigger, 8 | } from '../ui/context-menu'; 9 | import { Switch } from '../ui/switch'; 10 | 11 | export interface RuleItemProps { 12 | rule: Rule; 13 | onDel?: () => void; 14 | onSwitch?: (checked: boolean) => void; 15 | onEdit?: () => void; 16 | } 17 | 18 | export const RuleItem: FC = ({ 19 | rule, 20 | onDel, 21 | onSwitch, 22 | onEdit, 23 | }) => { 24 | const label = useMemo(() => { 25 | return getRuleDefine(rule.type).label; 26 | }, [rule.type]); 27 | 28 | const description = useMemo(() => { 29 | return getRuleDefine(rule.type).getDescription(rule.info); 30 | }, [rule.type, rule.info]); 31 | 32 | function handleDel() { 33 | onDel?.(); 34 | } 35 | 36 | return ( 37 | 38 | 39 |
40 | 41 | {rule.name} 42 | 43 | 44 | {label} 45 | 46 | 47 | {description} 48 | 49 |
50 | 51 |
52 |
53 |
54 | 55 | 编辑 56 | 删除 57 | 58 |
59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /src/components/rule/rule-type-froms/rule-delete-form.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox } from '@/components/ui/checkbox'; 2 | import { 3 | FormField, 4 | FormItem, 5 | FormLabel, 6 | FormControl, 7 | } from '@/components/ui/form'; 8 | import { Input } from '@/components/ui/input'; 9 | import type { RULE_DELETE_TYPE, Rule, RuleDeleteInfo } from '@/lib/rules'; 10 | import type { FC } from 'react'; 11 | import { useFormContext } from 'react-hook-form'; 12 | 13 | export const RuleDeleteForm: FC = () => { 14 | const form = useFormContext>(); 15 | 16 | return ( 17 |
18 |
19 | ( 23 | 24 | 查找 25 | 26 | 27 | 28 | 29 | )} 30 | /> 31 |
32 |
33 | ( 37 | 38 | 39 | 43 | 44 | 使用正则表达式 45 | 46 | )} 47 | /> 48 | ( 52 | 53 | 54 | 58 | 59 | 区分大小写 60 | 61 | )} 62 | /> 63 | ( 67 | 68 | 69 | 73 | 74 | 匹配所有符合项 75 | 76 | )} 77 | /> 78 | ( 82 | 83 | 84 | 88 | 89 | 包含扩展名 90 | 91 | )} 92 | /> 93 |
94 |
95 | ); 96 | }; 97 | -------------------------------------------------------------------------------- /src/components/rule/rule-type-froms/rule-format-form.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FormControl, 3 | FormField, 4 | FormItem, 5 | FormLabel, 6 | } from '@/components/ui/form'; 7 | import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; 8 | import { 9 | type RULE_FORMAT_TYPE, 10 | RULE_FORMAT_TYPES, 11 | RULE_FORMAT_TYPE_LABELS, 12 | type Rule, 13 | type RuleFormatInfo, 14 | } from '@/lib/rule'; 15 | import type { FC } from 'react'; 16 | import { useFormContext } from 'react-hook-form'; 17 | 18 | export const RuleFormatForm: FC = () => { 19 | const form = useFormContext>(); 20 | 21 | return ( 22 |
23 | ( 27 | 28 | 格式化类型 29 | 30 | 34 | {RULE_FORMAT_TYPES.map((formatType) => ( 35 | 39 | 40 | 41 | 42 | {RULE_FORMAT_TYPE_LABELS[formatType]} 43 | 44 | ))} 45 | 46 | 47 | 48 | )} 49 | /> 50 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /src/components/rule/rule-type-froms/rule-insert-form.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox } from '@/components/ui/checkbox'; 2 | import { 3 | FormField, 4 | FormItem, 5 | FormLabel, 6 | FormControl, 7 | } from '@/components/ui/form'; 8 | import { Input } from '@/components/ui/input'; 9 | import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; 10 | import { 11 | RULE_INSERT_TYPES, 12 | RULE_INSET_TYPE_LABELS, 13 | type RULE_INSERT_TYPE, 14 | type Rule, 15 | type RuleInsertInfo, 16 | } from '@/lib/rules'; 17 | import type { FC } from 'react'; 18 | import { useFormContext } from 'react-hook-form'; 19 | 20 | export const RuleInsertForm: FC = () => { 21 | const form = useFormContext>(); 22 | 23 | return ( 24 |
25 | ( 29 | 30 | 内容 31 | 32 | 37 | 38 | 39 | )} 40 | /> 41 | ( 45 | 46 | 插入类型 47 | 48 | 52 | {RULE_INSERT_TYPES.map((insertType) => ( 53 | 57 | 58 | 59 | 60 | {RULE_INSET_TYPE_LABELS[insertType]} 61 | 62 | ))} 63 | 64 | 65 | 66 | )} 67 | /> 68 | ( 72 | 73 | 74 | 78 | 79 | 包含扩展名 80 | 81 | )} 82 | /> 83 |
84 | ); 85 | }; 86 | -------------------------------------------------------------------------------- /src/components/rule/rule-type-froms/rule-replace-form.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox } from '@/components/ui/checkbox'; 2 | import { 3 | FormControl, 4 | FormField, 5 | FormItem, 6 | FormLabel, 7 | } from '@/components/ui/form'; 8 | import { Input } from '@/components/ui/input'; 9 | import type { RULE_REPLACE_TYPE, Rule, RuleReplaceInfo } from '@/lib/rule'; 10 | import type { FC } from 'react'; 11 | import { useFormContext } from 'react-hook-form'; 12 | 13 | export const RuleReplaceForm: FC = () => { 14 | const form = 15 | useFormContext>(); 16 | 17 | return ( 18 |
19 |
20 | ( 24 | 25 | 查找 26 | 27 | 28 | 29 | 30 | )} 31 | /> 32 | ( 36 | 37 | 替换 38 | 39 | 40 | 41 | 42 | )} 43 | /> 44 |
45 |
46 | ( 50 | 51 | 52 | 56 | 57 | 使用正则表达式 58 | 59 | )} 60 | /> 61 | ( 65 | 66 | 67 | 71 | 72 | 区分大小写 73 | 74 | )} 75 | /> 76 | ( 80 | 81 | 82 | 86 | 87 | 匹配所有符合项 88 | 89 | )} 90 | /> 91 | ( 95 | 96 | 97 | 101 | 102 | 包含扩展名 103 | 104 | )} 105 | /> 106 |
107 |
108 | ); 109 | }; 110 | -------------------------------------------------------------------------------- /src/components/rule/rule-type-froms/rule-script-form-worker.ts: -------------------------------------------------------------------------------- 1 | import * as monaco from 'monaco-editor'; 2 | import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; 3 | import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'; 4 | 5 | // @ts-ignore 6 | self.MonacoEnvironment = { 7 | getWorker(_: unknown, label: string) { 8 | if (label === 'typescript' || label === 'javascript') { 9 | return new tsWorker(); 10 | } 11 | return new editorWorker(); 12 | }, 13 | }; 14 | 15 | monaco.languages.typescript.javascriptDefaults.addExtraLib( 16 | ` 17 | var args: { fileInfo: { name: string; ext: string; fullName: string; }, index: number }; 18 | `, 19 | ); 20 | monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true); 21 | -------------------------------------------------------------------------------- /src/components/rule/rule-type-froms/rule-script-form.module.css: -------------------------------------------------------------------------------- 1 | .editor { 2 | width: 100%; 3 | height: 100%; 4 | 5 | &> div { 6 | outline: none !important; 7 | } 8 | } -------------------------------------------------------------------------------- /src/components/rule/rule-type-froms/rule-script-form.tsx: -------------------------------------------------------------------------------- 1 | import type { RULE_SCRIPT_TYPE, Rule, RuleScriptInfo } from '@/lib/rule'; 2 | import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; 3 | import { useEffect, useRef, type FC } from 'react'; 4 | import { useFormContext } from 'react-hook-form'; 5 | import './rule-script-form-worker'; 6 | import classes from './rule-script-form.module.css'; 7 | 8 | const RuleScriptForm: FC = () => { 9 | const form = useFormContext>(); 10 | const editorRef = useRef(); 11 | const monacoEl = useRef(null); 12 | const initValue = form.getValues('info.script'); 13 | 14 | // biome-ignore lint/correctness/useExhaustiveDependencies: 15 | useEffect(() => { 16 | editorRef.current = monaco.editor.create(monacoEl.current!, { 17 | value: initValue, 18 | language: 'javascript', 19 | }); 20 | 21 | return () => { 22 | editorRef.current?.dispose(); 23 | }; 24 | }, []); 25 | 26 | useEffect(() => { 27 | const listener = editorRef.current?.onDidChangeModelContent(() => { 28 | const value = editorRef.current?.getValue(); 29 | 30 | if (value) { 31 | form.setValue('info.script', value); 32 | } 33 | }); 34 | 35 | return () => { 36 | listener?.dispose(); 37 | }; 38 | }, [form.setValue]); 39 | 40 | return ( 41 |
42 |
43 |
44 | ); 45 | }; 46 | 47 | export default RuleScriptForm; 48 | -------------------------------------------------------------------------------- /src/components/rule/rule-type-froms/rule-template-form.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox } from '@/components/ui/checkbox'; 2 | import { 3 | FormField, 4 | FormItem, 5 | FormLabel, 6 | FormControl, 7 | } from '@/components/ui/form'; 8 | import { Input } from '@/components/ui/input'; 9 | import type { RULE_TEMPLATE_TYPE, Rule, RuleTemplateInfo } from '@/lib/rules'; 10 | import type { FC } from 'react'; 11 | import { useFormContext } from 'react-hook-form'; 12 | 13 | export const RuleTemplateForm: FC = () => { 14 | const form = 15 | useFormContext>(); 16 | 17 | return ( 18 |
19 | ( 23 | 24 | 模板 25 | 26 | 27 | 28 |

29 | 示例:文件:[xxx.mp4] 模板: 30 | {'[File-${fileInfo.name}] 输出:File-xxx.mp4'} 31 |

32 |

33 | 支持的变量:fileInfo.name、fileInfo.fullName、fileInfo.ext、index 34 |

35 |

36 | 支持js逻辑运算,比如:{'${index + 1}'} 37 |

38 |
39 | )} 40 | /> 41 | ( 45 | 46 | 47 | 51 | 52 | 包含扩展名 53 | 54 | )} 55 | /> 56 |
57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /src/components/rule/rules-panel.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, type FC } from 'react'; 2 | import { Button } from '../ui/button'; 3 | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; 4 | import { profileQueryOptions } from '@/lib/queries/profile'; 5 | import { RuleItem } from './rule-item'; 6 | import { 7 | Dialog, 8 | DialogClose, 9 | DialogContent, 10 | DialogHeader, 11 | DialogTitle, 12 | } from '../ui/dialog'; 13 | import { RuleEditPanel } from './rule-edit-panel'; 14 | import { useForm } from 'react-hook-form'; 15 | import { Form } from '../ui/form'; 16 | import { 17 | getRuleTypeDefaultValue, 18 | type Rule, 19 | RULE_REPLACE_TYPE, 20 | } from '@/lib/rule'; 21 | import { updateProfile } from '@/lib/profile'; 22 | import { QueryType } from '@/lib/query'; 23 | import { ScrollArea } from '../ui/scroll-area'; 24 | import { RuleEditDialog } from './rule-edit-dialog'; 25 | 26 | export interface RulesPanelProps { 27 | profileId: string; 28 | } 29 | 30 | export const RulesPanel: FC = ({ profileId }) => { 31 | const queryClient = useQueryClient(); 32 | const { data: profile } = useQuery(profileQueryOptions(profileId)); 33 | const [addRuleDialogOpened, setAddRuleDialogOpened] = useState(false); 34 | const [targetEditRule, setTargetEditRule] = useState(); 35 | 36 | const { mutate: addRule } = useMutation({ 37 | mutationFn: async (rule: Rule) => { 38 | if (!profile) { 39 | return; 40 | } 41 | 42 | return updateProfile(profileId, { 43 | ...profile, 44 | rules: [...profile.rules, rule], 45 | }); 46 | }, 47 | onSuccess: async () => { 48 | queryClient.invalidateQueries({ 49 | queryKey: [QueryType.Profile, { id: profileId }], 50 | }); 51 | queryClient.invalidateQueries({ 52 | queryKey: [QueryType.FileItemInfo, { profileId }], 53 | }); 54 | }, 55 | }); 56 | 57 | const { mutate: deleteRule } = useMutation({ 58 | mutationFn: async (ruleId: string) => { 59 | if (!profile) { 60 | return; 61 | } 62 | 63 | return updateProfile(profileId, { 64 | ...profile, 65 | rules: profile.rules.filter((rule) => rule.id !== ruleId), 66 | }); 67 | }, 68 | onSuccess: async () => { 69 | queryClient.invalidateQueries({ 70 | queryKey: [QueryType.Profile, { id: profileId }], 71 | }); 72 | queryClient.invalidateQueries({ 73 | queryKey: [QueryType.FileItemInfo, { profileId }], 74 | }); 75 | }, 76 | }); 77 | 78 | const { mutate: updateRuleChecked } = useMutation({ 79 | mutationFn: async ({ 80 | ruleId, 81 | checked, 82 | }: { ruleId: string; checked: boolean }) => { 83 | if (!profile) { 84 | return; 85 | } 86 | 87 | return updateProfile(profileId, { 88 | ...profile, 89 | rules: profile.rules.map((rule) => { 90 | if (rule.id === ruleId) { 91 | return { 92 | ...rule, 93 | enabled: checked, 94 | }; 95 | } 96 | 97 | return rule; 98 | }), 99 | }); 100 | }, 101 | 102 | onSuccess: async () => { 103 | queryClient.invalidateQueries({ 104 | queryKey: [QueryType.Profile, { id: profileId }], 105 | }); 106 | queryClient.invalidateQueries({ 107 | queryKey: [QueryType.FileItemInfo, { profileId }], 108 | }); 109 | }, 110 | }); 111 | 112 | const { mutate: updateRule } = useMutation({ 113 | mutationFn: async (rule: Rule) => { 114 | if (!profile) { 115 | return; 116 | } 117 | 118 | return updateProfile(profileId, { 119 | ...profile, 120 | rules: profile.rules.map((r) => { 121 | if (rule.id === r.id) { 122 | return { 123 | ...r, 124 | ...rule, 125 | }; 126 | } 127 | 128 | return r; 129 | }), 130 | }); 131 | }, 132 | onSuccess: async () => { 133 | queryClient.invalidateQueries({ 134 | queryKey: [QueryType.Profile, { id: profileId }], 135 | }); 136 | queryClient.invalidateQueries({ 137 | queryKey: [QueryType.FileItemInfo, { profileId }], 138 | }); 139 | }, 140 | }); 141 | 142 | const form = useForm({ 143 | defaultValues: getRuleTypeDefaultValue(RULE_REPLACE_TYPE), 144 | }); 145 | 146 | function handleAddRule() { 147 | setAddRuleDialogOpened(true); 148 | } 149 | 150 | function onSubmit(values: Rule) { 151 | addRule(values); 152 | 153 | setAddRuleDialogOpened(false); 154 | } 155 | 156 | function onUpdateRule(values: Rule) { 157 | updateRule(values); 158 | 159 | setTargetEditRule(undefined); 160 | } 161 | 162 | function onCloseRuleEditDialog() { 163 | setTargetEditRule(undefined); 164 | } 165 | 166 | useEffect(() => { 167 | if (!addRuleDialogOpened) { 168 | form.reset(getRuleTypeDefaultValue(RULE_REPLACE_TYPE)); 169 | } 170 | }, [addRuleDialogOpened, form.reset]); 171 | 172 | return ( 173 | <> 174 |
175 |
176 | 179 |
180 |
181 | 名称 182 | 183 | 规则 184 | 185 | 说明 186 |
187 |
188 | 189 |
190 | {profile?.rules?.map((rule) => { 191 | return ( 192 | deleteRule(rule.id)} 196 | onSwitch={(checked) => 197 | updateRuleChecked({ ruleId: rule.id, checked }) 198 | } 199 | onEdit={() => setTargetEditRule(rule)} 200 | /> 201 | ); 202 | })} 203 |
204 |
205 |
206 | 207 | e.preventDefault()} 209 | className="grid h-[70vh] w-full grid-cols-1 grid-rows-[max-content_1fr]" 210 | > 211 | 212 | 添加规则 213 | 214 |
215 | 220 | 221 |
222 | 223 | 224 | 225 | 226 |
227 | 228 | 229 |
230 |
231 | 236 | 237 | ); 238 | }; 239 | -------------------------------------------------------------------------------- /src/components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" 3 | 4 | import { cn } from "@/lib/utils" 5 | import { buttonVariants } from "@/components/ui/button" 6 | 7 | const AlertDialog = AlertDialogPrimitive.Root 8 | 9 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger 10 | 11 | const AlertDialogPortal = AlertDialogPrimitive.Portal 12 | 13 | const AlertDialogOverlay = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef 16 | >(({ className, ...props }, ref) => ( 17 | 25 | )) 26 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName 27 | 28 | const AlertDialogContent = React.forwardRef< 29 | React.ElementRef, 30 | React.ComponentPropsWithoutRef 31 | >(({ className, ...props }, ref) => ( 32 | 33 | 34 | 42 | 43 | )) 44 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName 45 | 46 | const AlertDialogHeader = ({ 47 | className, 48 | ...props 49 | }: React.HTMLAttributes) => ( 50 |
57 | ) 58 | AlertDialogHeader.displayName = "AlertDialogHeader" 59 | 60 | const AlertDialogFooter = ({ 61 | className, 62 | ...props 63 | }: React.HTMLAttributes) => ( 64 |
71 | ) 72 | AlertDialogFooter.displayName = "AlertDialogFooter" 73 | 74 | const AlertDialogTitle = React.forwardRef< 75 | React.ElementRef, 76 | React.ComponentPropsWithoutRef 77 | >(({ className, ...props }, ref) => ( 78 | 83 | )) 84 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName 85 | 86 | const AlertDialogDescription = React.forwardRef< 87 | React.ElementRef, 88 | React.ComponentPropsWithoutRef 89 | >(({ className, ...props }, ref) => ( 90 | 95 | )) 96 | AlertDialogDescription.displayName = 97 | AlertDialogPrimitive.Description.displayName 98 | 99 | const AlertDialogAction = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName 110 | 111 | const AlertDialogCancel = React.forwardRef< 112 | React.ElementRef, 113 | React.ComponentPropsWithoutRef 114 | >(({ className, ...props }, ref) => ( 115 | 124 | )) 125 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName 126 | 127 | export { 128 | AlertDialog, 129 | AlertDialogPortal, 130 | AlertDialogOverlay, 131 | AlertDialogTrigger, 132 | AlertDialogContent, 133 | AlertDialogHeader, 134 | AlertDialogFooter, 135 | AlertDialogTitle, 136 | AlertDialogDescription, 137 | AlertDialogAction, 138 | AlertDialogCancel, 139 | } 140 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 3 | import { CheckIcon } from "@radix-ui/react-icons" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Checkbox = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | 22 | 23 | 24 | 25 | )) 26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 27 | 28 | export { Checkbox } 29 | -------------------------------------------------------------------------------- /src/components/ui/context-menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" 3 | import { 4 | CheckIcon, 5 | ChevronRightIcon, 6 | DotFilledIcon, 7 | } from "@radix-ui/react-icons" 8 | 9 | import { cn } from "@/lib/utils" 10 | 11 | const ContextMenu = ContextMenuPrimitive.Root 12 | 13 | const ContextMenuTrigger = ContextMenuPrimitive.Trigger 14 | 15 | const ContextMenuGroup = ContextMenuPrimitive.Group 16 | 17 | const ContextMenuPortal = ContextMenuPrimitive.Portal 18 | 19 | const ContextMenuSub = ContextMenuPrimitive.Sub 20 | 21 | const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup 22 | 23 | const ContextMenuSubTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef & { 26 | inset?: boolean 27 | } 28 | >(({ className, inset, children, ...props }, ref) => ( 29 | 38 | {children} 39 | 40 | 41 | )) 42 | ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName 43 | 44 | const ContextMenuSubContent = React.forwardRef< 45 | React.ElementRef, 46 | React.ComponentPropsWithoutRef 47 | >(({ className, ...props }, ref) => ( 48 | 56 | )) 57 | ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName 58 | 59 | const ContextMenuContent = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, ...props }, ref) => ( 63 | 64 | 72 | 73 | )) 74 | ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName 75 | 76 | const ContextMenuItem = React.forwardRef< 77 | React.ElementRef, 78 | React.ComponentPropsWithoutRef & { 79 | inset?: boolean 80 | } 81 | >(({ className, inset, ...props }, ref) => ( 82 | 91 | )) 92 | ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName 93 | 94 | const ContextMenuCheckboxItem = React.forwardRef< 95 | React.ElementRef, 96 | React.ComponentPropsWithoutRef 97 | >(({ className, children, checked, ...props }, ref) => ( 98 | 107 | 108 | 109 | 110 | 111 | 112 | {children} 113 | 114 | )) 115 | ContextMenuCheckboxItem.displayName = 116 | ContextMenuPrimitive.CheckboxItem.displayName 117 | 118 | const ContextMenuRadioItem = React.forwardRef< 119 | React.ElementRef, 120 | React.ComponentPropsWithoutRef 121 | >(({ className, children, ...props }, ref) => ( 122 | 130 | 131 | 132 | 133 | 134 | 135 | {children} 136 | 137 | )) 138 | ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName 139 | 140 | const ContextMenuLabel = React.forwardRef< 141 | React.ElementRef, 142 | React.ComponentPropsWithoutRef & { 143 | inset?: boolean 144 | } 145 | >(({ className, inset, ...props }, ref) => ( 146 | 155 | )) 156 | ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName 157 | 158 | const ContextMenuSeparator = React.forwardRef< 159 | React.ElementRef, 160 | React.ComponentPropsWithoutRef 161 | >(({ className, ...props }, ref) => ( 162 | 167 | )) 168 | ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName 169 | 170 | const ContextMenuShortcut = ({ 171 | className, 172 | ...props 173 | }: React.HTMLAttributes) => { 174 | return ( 175 | 182 | ) 183 | } 184 | ContextMenuShortcut.displayName = "ContextMenuShortcut" 185 | 186 | export { 187 | ContextMenu, 188 | ContextMenuTrigger, 189 | ContextMenuContent, 190 | ContextMenuItem, 191 | ContextMenuCheckboxItem, 192 | ContextMenuRadioItem, 193 | ContextMenuLabel, 194 | ContextMenuSeparator, 195 | ContextMenuShortcut, 196 | ContextMenuGroup, 197 | ContextMenuPortal, 198 | ContextMenuSub, 199 | ContextMenuSubContent, 200 | ContextMenuSubTrigger, 201 | ContextMenuRadioGroup, 202 | } 203 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as DialogPrimitive from '@radix-ui/react-dialog'; 3 | import { Cross2Icon } from '@radix-ui/react-icons'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | 7 | const Dialog = DialogPrimitive.Root; 8 | 9 | const DialogTrigger = DialogPrimitive.Trigger; 10 | 11 | const DialogPortal = DialogPrimitive.Portal; 12 | 13 | const DialogClose = DialogPrimitive.Close; 14 | 15 | const DialogOverlay = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => ( 19 | 27 | )); 28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 29 | 30 | const DialogContent = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, children, ...props }, ref) => ( 34 | 35 | 36 | 44 | {children} 45 | 46 | 47 | Close 48 | 49 | 50 | 51 | )); 52 | DialogContent.displayName = DialogPrimitive.Content.displayName; 53 | 54 | const DialogHeader = ({ 55 | className, 56 | ...props 57 | }: React.HTMLAttributes) => ( 58 |
65 | ); 66 | DialogHeader.displayName = 'DialogHeader'; 67 | 68 | const DialogFooter = ({ 69 | className, 70 | ...props 71 | }: React.HTMLAttributes) => ( 72 |
79 | ); 80 | DialogFooter.displayName = 'DialogFooter'; 81 | 82 | const DialogTitle = React.forwardRef< 83 | React.ElementRef, 84 | React.ComponentPropsWithoutRef 85 | >(({ className, ...props }, ref) => ( 86 | 94 | )); 95 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 96 | 97 | const DialogDescription = React.forwardRef< 98 | React.ElementRef, 99 | React.ComponentPropsWithoutRef 100 | >(({ className, ...props }, ref) => ( 101 | 106 | )); 107 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 108 | 109 | export { 110 | Dialog, 111 | DialogPortal, 112 | DialogOverlay, 113 | DialogTrigger, 114 | DialogClose, 115 | DialogContent, 116 | DialogHeader, 117 | DialogFooter, 118 | DialogTitle, 119 | DialogDescription, 120 | }; 121 | -------------------------------------------------------------------------------- /src/components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import type * as LabelPrimitive from '@radix-ui/react-label'; 3 | import { Slot } from '@radix-ui/react-slot'; 4 | import { 5 | Controller, 6 | type ControllerProps, 7 | type FieldPath, 8 | type FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | } from 'react-hook-form'; 12 | 13 | import { cn } from '@/lib/utils'; 14 | import { Label } from '@/components/ui/label'; 15 | 16 | const Form = FormProvider; 17 | 18 | type FormFieldContextValue< 19 | TFieldValues extends FieldValues = FieldValues, 20 | TName extends FieldPath = FieldPath, 21 | > = { 22 | name: TName; 23 | }; 24 | 25 | const FormFieldContext = React.createContext( 26 | {} as FormFieldContextValue, 27 | ); 28 | 29 | const FormField = < 30 | TFieldValues extends FieldValues = FieldValues, 31 | TName extends FieldPath = FieldPath, 32 | >({ 33 | ...props 34 | }: ControllerProps) => { 35 | return ( 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | const useFormField = () => { 43 | const fieldContext = React.useContext(FormFieldContext); 44 | const itemContext = React.useContext(FormItemContext); 45 | const { getFieldState, formState } = useFormContext(); 46 | 47 | const fieldState = getFieldState(fieldContext.name, formState); 48 | 49 | if (!fieldContext) { 50 | throw new Error('useFormField should be used within '); 51 | } 52 | 53 | const { id } = itemContext; 54 | 55 | return { 56 | id, 57 | name: fieldContext.name, 58 | formItemId: `${id}-form-item`, 59 | formDescriptionId: `${id}-form-item-description`, 60 | formMessageId: `${id}-form-item-message`, 61 | ...fieldState, 62 | }; 63 | }; 64 | 65 | type FormItemContextValue = { 66 | id: string; 67 | }; 68 | 69 | const FormItemContext = React.createContext( 70 | {} as FormItemContextValue, 71 | ); 72 | 73 | const FormItem = React.forwardRef< 74 | HTMLDivElement, 75 | React.HTMLAttributes 76 | >(({ className, ...props }, ref) => { 77 | const id = React.useId(); 78 | 79 | return ( 80 | 81 |
82 | 83 | ); 84 | }); 85 | FormItem.displayName = 'FormItem'; 86 | 87 | const FormLabel = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => { 91 | const { error, formItemId } = useFormField(); 92 | 93 | return ( 94 |