├── Trunk.toml ├── .gitignore ├── assets ├── cpu.ico ├── cpu.png ├── favicon.ico ├── icon-1024.png ├── icon-256.png ├── settings.png ├── screenshot.png ├── icon_ios_touch_192.png ├── maskable_icon_x512.png ├── sw.js ├── manifest.json └── cpu.svg ├── src ├── lib.rs ├── main.rs └── app.rs ├── TODO.md ├── CHANGELOG.md ├── rust-toolchain ├── check.sh ├── .cargo └── config.toml ├── Cargo.toml ├── README.md ├── .github └── workflows │ ├── rust.yml │ └── release.yml └── index.html /Trunk.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /dist 3 | -------------------------------------------------------------------------------- /assets/cpu.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markusdd/EasyEDA_to_KiCAD_Lib_UI/HEAD/assets/cpu.ico -------------------------------------------------------------------------------- /assets/cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markusdd/EasyEDA_to_KiCAD_Lib_UI/HEAD/assets/cpu.png -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all, rust_2018_idioms)] 2 | 3 | mod app; 4 | pub use app::MyApp; 5 | -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markusdd/EasyEDA_to_KiCAD_Lib_UI/HEAD/assets/favicon.ico -------------------------------------------------------------------------------- /assets/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markusdd/EasyEDA_to_KiCAD_Lib_UI/HEAD/assets/icon-1024.png -------------------------------------------------------------------------------- /assets/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markusdd/EasyEDA_to_KiCAD_Lib_UI/HEAD/assets/icon-256.png -------------------------------------------------------------------------------- /assets/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markusdd/EasyEDA_to_KiCAD_Lib_UI/HEAD/assets/settings.png -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markusdd/EasyEDA_to_KiCAD_Lib_UI/HEAD/assets/screenshot.png -------------------------------------------------------------------------------- /assets/icon_ios_touch_192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markusdd/EasyEDA_to_KiCAD_Lib_UI/HEAD/assets/icon_ios_touch_192.png -------------------------------------------------------------------------------- /assets/maskable_icon_x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markusdd/EasyEDA_to_KiCAD_Lib_UI/HEAD/assets/maskable_icon_x512.png -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # remove this if present in the resulting symbol file 2 | # EasyEDA libs can hide single pin names, in KiCAD this translates to hiding them all, 3 | # which makes no sense 4 | 5 | (pin_names hide) -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.4.0 4 | 5 | - WIP 6 | 7 | ## 1.3.9 8 | 9 | - fix and rework datasheet download to remove openssl dependency completely 10 | 11 | ## 1.3.8 12 | 13 | - offer universal DMG for macOS 14 | - build Linux release against older GLIBC to support more distributions 15 | 16 | ## 1.3.7 17 | 18 | - bump dependencies 19 | - fix Windows executable icon 20 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | # If you see this, run "rustup self update" to get rustup 1.23 or newer. 2 | 3 | # NOTE: above comment is for older `rustup` (before TOML support was added), 4 | # which will treat the first line as the toolchain name, and therefore show it 5 | # to the user in the error, instead of "error: invalid channel name '[toolchain]'". 6 | 7 | [toolchain] 8 | channel = "1.82.0" 9 | components = ["rustfmt", "clippy"] 10 | targets = ["wasm32-unknown-unknown"] 11 | -------------------------------------------------------------------------------- /check.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This scripts runs various CI-like checks in a convenient way. 3 | set -eux 4 | 5 | cargo check --quiet --workspace --all-targets 6 | cargo check --quiet --workspace --all-features --lib --target wasm32-unknown-unknown 7 | cargo fmt --all -- --check 8 | cargo clippy --quiet --workspace --all-targets --all-features -- -D warnings -W clippy::all 9 | cargo test --quiet --workspace --all-targets --all-features 10 | cargo test --quiet --workspace --doc 11 | trunk build 12 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # clipboard api is still unstable, so web-sys requires the below flag to be passed for copy (ctrl + c) to work 2 | # https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html 3 | # check status at https://developer.mozilla.org/en-US/docs/Web/API/Clipboard#browser_compatibility 4 | # we don't use `[build]` because of rust analyzer's build cache invalidation https://github.com/emilk/eframe_template/issues/93 5 | [target.wasm32-unknown-unknown] 6 | rustflags = ["--cfg=web_sys_unstable_apis"] -------------------------------------------------------------------------------- /assets/sw.js: -------------------------------------------------------------------------------- 1 | var cacheName = 'egui-template-pwa'; 2 | var filesToCache = [ 3 | './', 4 | './index.html', 5 | './eframe_template.js', 6 | './eframe_template_bg.wasm', 7 | ]; 8 | 9 | /* Start the service worker and cache all of the app's content */ 10 | self.addEventListener('install', function (e) { 11 | e.waitUntil( 12 | caches.open(cacheName).then(function (cache) { 13 | return cache.addAll(filesToCache); 14 | }) 15 | ); 16 | }); 17 | 18 | /* Serve cached content when offline */ 19 | self.addEventListener('fetch', function (e) { 20 | e.respondWith( 21 | caches.match(e.request).then(function (response) { 22 | return response || fetch(e.request); 23 | }) 24 | ); 25 | }); 26 | -------------------------------------------------------------------------------- /assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "egui Template PWA", 3 | "short_name": "egui-template-pwa", 4 | "icons": [ 5 | { 6 | "src": "./icon-256.png", 7 | "sizes": "256x256", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "./maskable_icon_x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png", 14 | "purpose": "any maskable" 15 | }, 16 | { 17 | "src": "./icon-1024.png", 18 | "sizes": "1024x1024", 19 | "type": "image/png" 20 | } 21 | ], 22 | "lang": "en-US", 23 | "id": "/index.html", 24 | "start_url": "./index.html", 25 | "display": "standalone", 26 | "background_color": "white", 27 | "theme_color": "white" 28 | } 29 | -------------------------------------------------------------------------------- /assets/cpu.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 26 | 32 | 38 | 44 | 50 | 56 | 62 | 68 | 74 | 80 | 81 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "easyeda_to_kicad_lib_ui" 3 | version = "1.3.8" 4 | authors = ["Markus Krause "] 5 | edition = "2021" 6 | rust-version = "1.82" 7 | 8 | 9 | [dependencies] 10 | reqwest = { version = "0.12.23", default-features = false, features = ["blocking", "rustls-tls"] } 11 | egui = "0.31.1" 12 | eframe = { version = "0.31.1", default-features = false, features = [ 13 | "accesskit", # Make egui comptaible with screen readers. NOTE: adds a lot of dependencies. 14 | "default_fonts", # Embed the default egui fonts. 15 | "glow", # Use the glow rendering backend. Alternative: "wgpu". 16 | "persistence", # Enable restoring app state when restarting the app. 17 | "x11", # for Linux 18 | "wayland", # for Linux 19 | ] } 20 | egui_extras = { version = "0.31.1", features = ["all_loaders"] } 21 | image = { version = "0.25.6", features = ["jpeg", "png"] } 22 | egui-dropdown = "0.13.0" 23 | log = "0.4.27" 24 | urlencoding = "2.1.3" 25 | subprocess = "0.2.9" 26 | serde_json = "1.0.140" 27 | regex = "1.11.1" 28 | indexmap = "2.9.0" 29 | tempfile = "3.19.1" 30 | glob = "0.3.2" 31 | arboard = "3.5.0" 32 | # this is needed to avoid edition2024 errors 33 | mime = "0.3.17" 34 | mime_guess2 = "=2.0.5" 35 | 36 | # You only need serde if you want app persistence: 37 | serde = { version = "1.0.219", features = ["derive"] } 38 | 39 | [build-dependencies] 40 | # for windows icon embedding 41 | winres = "0.1" 42 | 43 | # native: 44 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 45 | env_logger = "0.11.8" 46 | 47 | # web: 48 | [target.'cfg(target_arch = "wasm32")'.dependencies] 49 | wasm-bindgen-futures = "0.4.50" 50 | 51 | 52 | # [profile.release] 53 | # opt-level = 2 # fast and small wasm 54 | 55 | [profile.release] 56 | opt-level = 3 # Optimize for speed without exploding size 57 | lto = true # Enable Link Time Optimization 58 | codegen-units = 1 # Reduce number of codegen units to increase optimizations. 59 | panic = 'abort' # Abort on panic 60 | strip = true # Strip symbols from binary* 61 | 62 | # Optimize all dependencies even in debug builds: 63 | [profile.dev.package."*"] 64 | opt-level = 2 65 | 66 | 67 | [patch.crates-io] 68 | 69 | # If you want to use the bleeding edge version of egui and eframe: 70 | # egui = { git = "https://github.com/emilk/egui", branch = "master" } 71 | # eframe = { git = "https://github.com/emilk/egui", branch = "master" } 72 | 73 | # If you fork https://github.com/emilk/egui you can test with: 74 | # egui = { path = "../egui/crates/egui" } 75 | # eframe = { path = "../egui/crates/eframe" } 76 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all, rust_2018_idioms)] 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release 3 | 4 | // When compiling natively: 5 | #[cfg(not(target_arch = "wasm32"))] 6 | fn main() -> eframe::Result<()> { 7 | env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). 8 | 9 | let native_options = eframe::NativeOptions { 10 | viewport: egui::ViewportBuilder::default() 11 | .with_inner_size([1024.0, 1000.0]) 12 | .with_min_inner_size([300.0, 220.0]) 13 | .with_title("EasyEDA to KiCAD Library UI") 14 | .with_icon(load_icon()), 15 | ..Default::default() 16 | }; 17 | eframe::run_native( 18 | "EasyEDA_to_KiCAD_Lib_UI", 19 | native_options, 20 | Box::new(|cc| { 21 | // This gives us image support: 22 | egui_extras::install_image_loaders(&cc.egui_ctx); 23 | 24 | Ok(Box::new(easyeda_to_kicad_lib_ui::MyApp::new(cc))) 25 | }), 26 | ) 27 | } 28 | 29 | // When compiling to web using trunk: 30 | #[cfg(target_arch = "wasm32")] 31 | fn main() { 32 | // Redirect `log` message to `console.log` and friends: 33 | eframe::WebLogger::init(log::LevelFilter::Debug).ok(); 34 | 35 | let web_options = eframe::WebOptions::default(); 36 | 37 | wasm_bindgen_futures::spawn_local(async { 38 | eframe::WebRunner::new() 39 | .start( 40 | "the_canvas_id", // hardcode it 41 | web_options, 42 | Box::new(|cc| Box::new(easyeda_to_kicad_lib_ui::MyApp::new(cc))), 43 | ) 44 | .await 45 | .expect("failed to start eframe"); 46 | }); 47 | } 48 | 49 | // Function to load the icon (supports both Windows and others) 50 | fn load_icon() -> egui::viewport::IconData { 51 | #[cfg(target_os = "windows")] 52 | { 53 | // Embed ICO for Windows 54 | let ico_bytes = include_bytes!("../assets/cpu.ico"); 55 | let image = image::load_from_memory(ico_bytes) 56 | .expect("Failed to load icon from memory") 57 | .to_rgba8(); 58 | let (width, height) = image.dimensions(); 59 | egui::viewport::IconData { 60 | rgba: image.into_raw(), 61 | width, 62 | height, 63 | } 64 | } 65 | #[cfg(not(target_os = "windows"))] 66 | { 67 | // Keep PNG for macOS/Linux 68 | eframe::icon_data::from_png_bytes(&include_bytes!("../assets/cpu.png")[..]) 69 | .expect("Failed to load icon") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EasyEDA to KiCAD Library UI 2 | 3 | If you like this, a small donation is appreciated: 4 | 5 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R8DQO8C) 6 | 7 | ![screenshot](assets/screenshot.png) 8 | 9 | This is a GUI application written in Rust powered by the awesome egui framework to turn 10 | EasyEDA PCB library components into KiCAD library elements (compatible with KiCAD 6 and upwards). 11 | 12 | In the background it builds on the amazing work of the JLC2KICad_lib project to generate 13 | the actual library files. 14 | 15 | What it adds on top is a convenient UI to save your settings. Also, you can provide the Cxxxxx 16 | number from JLCPCB/LCSC directly, or you can drop in either URL from their parts detail pages and the 17 | tool will extract the part number for you. 18 | 19 | It also gives you a pretty parts overview to make sure it is what you wanted, and it provides thumbnails 20 | of the pictures LCSC provides of the parts. If you hover over them, you get the full size view. 21 | 22 | And it gives you the option to directly open the parts pages, access the datasheet URL (if there is one) and 23 | also save the datasheet in addition to the library conversion. 24 | 25 | When using the 'Copy Footprint' button it will download just the footprint and 3D model for the part into a 26 | temporary directory and send it to the clipboard so you can just paste it via Ctrl+V into the KiCAD PCB editor 27 | to check it out and also view the 3d model. The temporary folder will vanish once the application is closed, 28 | so if you commit to using the part use the "Add to Library" function to permanently add it. 29 | 30 | ## How to get going 31 | 32 | You can clone this repository and just run `cargo build --release`, provided you have rust installed (use `rustup`, it's easy). 33 | The Releases section has automatically built releases for Mac ARM64 and x86_64, Windows x86_64 and Linux x86_64. 34 | 35 | Also, you need https://github.com/TousstNicolas/JLC2KiCad_lib 36 | installed on your machine. Install instructions are provided at the linked repo, easiest option is probably via `pip` if Python is already installed. 37 | 38 | After you have the prerequisites, launch the application and adjust the settings to your liking, most importantly, provide a valid path to the JLC2KiCad_lib application, either by using an absolute path or making sure it is in your systems $PATH variable. 39 | 40 | ![settings](assets/settings.png) 41 | 42 | After entering everything close the program once to save everything. 43 | The application leverages the save state mechanism of egui to persist your settings. 44 | 45 | ## What this is NOT 46 | 47 | - a full featured LCSC library/component browser 48 | - a savior which prevents you from overriding stuff you did not want to override 49 | - a checker that the resulting lib components are actually correct 50 | 51 | So, use it as a convenience, but as always: Make sure the output and settings are what you intended and what you need! 52 | 53 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI 4 | 5 | env: 6 | # This is required to enable the web_sys clipboard API which egui_web uses 7 | # https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html 8 | # https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html 9 | RUSTFLAGS: --cfg=web_sys_unstable_apis 10 | 11 | jobs: 12 | check: 13 | name: Check 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions-rs/toolchain@v1 18 | with: 19 | profile: minimal 20 | toolchain: stable 21 | override: true 22 | - uses: actions-rs/cargo@v1 23 | with: 24 | command: check 25 | args: --all-features 26 | 27 | # check_wasm: 28 | # name: Check wasm32 29 | # runs-on: ubuntu-latest 30 | # steps: 31 | # - uses: actions/checkout@v2 32 | # - uses: actions-rs/toolchain@v1 33 | # with: 34 | # profile: minimal 35 | # toolchain: stable 36 | # target: wasm32-unknown-unknown 37 | # override: true 38 | # - uses: actions-rs/cargo@v1 39 | # with: 40 | # command: check 41 | # args: --all-features --lib --target wasm32-unknown-unknown 42 | 43 | test: 44 | name: Test Suite 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v2 48 | - uses: actions-rs/toolchain@v1 49 | with: 50 | profile: minimal 51 | toolchain: stable 52 | override: true 53 | - run: sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev 54 | - uses: actions-rs/cargo@v1 55 | with: 56 | command: test 57 | args: --lib 58 | 59 | fmt: 60 | name: Rustfmt 61 | runs-on: ubuntu-latest 62 | steps: 63 | - uses: actions/checkout@v2 64 | - uses: actions-rs/toolchain@v1 65 | with: 66 | profile: minimal 67 | toolchain: stable 68 | override: true 69 | components: rustfmt 70 | - uses: actions-rs/cargo@v1 71 | with: 72 | command: fmt 73 | args: --all -- --check 74 | 75 | clippy: 76 | name: Clippy 77 | runs-on: ubuntu-latest 78 | steps: 79 | - uses: actions/checkout@v2 80 | - uses: actions-rs/toolchain@v1 81 | with: 82 | profile: minimal 83 | toolchain: stable 84 | override: true 85 | components: clippy 86 | - uses: actions-rs/cargo@v1 87 | with: 88 | command: clippy 89 | args: -- -D warnings 90 | 91 | # trunk: 92 | # name: trunk 93 | # runs-on: ubuntu-latest 94 | # steps: 95 | # - uses: actions/checkout@v2 96 | # - uses: actions-rs/toolchain@v1 97 | # with: 98 | # profile: minimal 99 | # toolchain: 1.72.0 100 | # target: wasm32-unknown-unknown 101 | # override: true 102 | # - name: Download and install Trunk binary 103 | # run: wget -qO- https://github.com/thedodd/trunk/releases/latest/download/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf- 104 | # - name: Build 105 | # run: ./trunk build 106 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | eframe template 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - 'v*' 5 | 6 | jobs: 7 | version: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@master 11 | with: 12 | lfs: true 13 | - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* 14 | - run: mkdir -p ./version 15 | - run: git describe --tags "$(git rev-list --tags --max-count=1)" > ./version/version 16 | - uses: actions/upload-artifact@master 17 | with: 18 | name: version 19 | path: ./version/version 20 | 21 | build: 22 | needs: 23 | - version 24 | runs-on: ${{ matrix.os }} 25 | strategy: 26 | matrix: 27 | include: 28 | - os: macos-latest 29 | target: universal-apple-darwin 30 | suffix: .dmg 31 | use_docker: false 32 | - os: ubuntu-latest 33 | target: x86_64-unknown-linux-gnu 34 | suffix: '' 35 | use_docker: true 36 | - os: windows-latest 37 | target: x86_64-pc-windows-msvc 38 | suffix: .exe 39 | use_docker: false 40 | steps: 41 | - uses: actions/checkout@master 42 | with: 43 | lfs: true 44 | - id: get_repository_name 45 | run: echo "REPOSITORY_NAME=$(echo "$GITHUB_REPOSITORY" | awk -F / '{print $2}' | sed -e "s/:refs//")" >> $GITHUB_OUTPUT 46 | shell: bash 47 | - uses: actions/download-artifact@master 48 | with: 49 | name: version 50 | path: ./version 51 | - id: get_version 52 | run: echo "VERSION=$(cat ./version/version)" >> $GITHUB_OUTPUT 53 | shell: bash 54 | - name: Set up Docker for Linux build 55 | if: ${{ matrix.use_docker }} 56 | run: | 57 | echo "Running Linux build in Docker container" 58 | - name: Build Linux binary in Docker 59 | if: ${{ matrix.use_docker }} 60 | run: | 61 | docker run --rm -v $(pwd):/usr/src/easyeda_to_kicad_lib_ui -w /usr/src/easyeda_to_kicad_lib_ui rockylinux:8 /bin/bash -c ' 62 | dnf install -y epel-release && 63 | dnf config-manager --set-enabled powertools && 64 | dnf groupinstall -y "Development Tools" && 65 | dnf install -y curl clang clang-devel libxkbcommon-devel mesa-libGL-devel libX11-devel \ 66 | wayland-devel libpng-devel libjpeg-devel openssl-devel pkgconf-pkg-config && 67 | curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain 1.82.0 && 68 | source $HOME/.cargo/env && 69 | rustc --version && 70 | ls -l /usr/lib64/libclang* && 71 | export LIBCLANG_PATH=/usr/lib64 && 72 | cargo build --release --target x86_64-unknown-linux-gnu && 73 | ldd --version && 74 | objdump -p target/x86_64-unknown-linux-gnu/release/easyeda_to_kicad_lib_ui | grep GLIBC 75 | ' 76 | - name: Install Rust toolchain (non-Docker) 77 | if: ${{ !matrix.use_docker }} 78 | uses: actions-rs/toolchain@v1 79 | with: 80 | profile: minimal 81 | toolchain: stable 82 | override: true 83 | - name: Cache Rust dependencies (non-Docker) 84 | if: ${{ !matrix.use_docker }} 85 | uses: actions/cache@master 86 | with: 87 | path: ~/.cargo/registry 88 | key: '${{ runner.os }}-cargo-registry-${{ hashFiles(''**/Cargo.lock'') }}' 89 | - name: Cache Rust git index (non-Docker) 90 | if: ${{ !matrix.use_docker }} 91 | uses: actions/cache@master 92 | with: 93 | path: ~/.cargo/git 94 | key: '${{ runner.os }}-cargo-index-${{ hashFiles(''**/Cargo.lock'') }}' 95 | - name: Cache build target (non-Docker) 96 | if: ${{ !matrix.use_docker }} 97 | uses: actions/cache@master 98 | with: 99 | path: target 100 | key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} 101 | - name: Install dependencies for macOS DMG 102 | if: ${{ matrix.target == 'universal-apple-darwin' }} 103 | run: | 104 | brew install librsvg libpng create-dmg 105 | npm install -g @electron-forge/cli 106 | - name: Build macOS universal binary 107 | if: ${{ matrix.target == 'universal-apple-darwin' }} 108 | run: | 109 | rustup target add x86_64-apple-darwin aarch64-apple-darwin 110 | cargo build --release --target x86_64-apple-darwin 111 | cargo build --release --target aarch64-apple-darwin 112 | lipo -create -output target/release/easyeda_to_kicad_lib_ui \ 113 | target/x86_64-apple-darwin/release/easyeda_to_kicad_lib_ui \ 114 | target/aarch64-apple-darwin/release/easyeda_to_kicad_lib_ui 115 | chmod +x target/release/easyeda_to_kicad_lib_ui 116 | - name: Convert SVG to ICNS 117 | if: ${{ matrix.target == 'universal-apple-darwin' }} 118 | run: | 119 | mkdir -p icon.iconset 120 | mkdir -p icon-dmg.iconset 121 | # Generate high-quality PNGs for application ICNS 122 | rsvg-convert -w 16 -h 16 assets/cpu.svg -o icon.iconset/icon_16x16.png 123 | rsvg-convert -w 32 -h 32 assets/cpu.svg -o icon.iconset/icon_16x16@2x.png 124 | rsvg-convert -w 32 -h 32 assets/cpu.svg -o icon.iconset/icon_32x32.png 125 | rsvg-convert -w 64 -h 64 assets/cpu.svg -o icon.iconset/icon_32x32@2x.png 126 | rsvg-convert -w 128 -h 128 assets/cpu.svg -o icon.iconset/icon_128x128.png 127 | rsvg-convert -w 256 -h 256 assets/cpu.svg -o icon.iconset/icon_128x128@2x.png 128 | rsvg-convert -w 256 -h 256 assets/cpu.svg -o icon.iconset/icon_256x256.png 129 | rsvg-convert -w 512 -h 512 assets/cpu.svg -o icon.iconset/icon_256x256@2x.png 130 | rsvg-convert -w 512 -h 512 assets/cpu.svg -o icon.iconset/icon_512x512.png 131 | rsvg-convert -w 1024 -h 1024 assets/cpu.svg -o icon.iconset/icon_512x512@2x.png 132 | iconutil -c icns icon.iconset -o assets/EasyEDA_to_KiCAD_Lib_UI.icns 133 | # Generate high-quality PNGs for DMG ICNS 134 | rsvg-convert -w 16 -h 16 assets/cpu.svg -o icon-dmg.iconset/icon_16x16.png 135 | rsvg-convert -w 32 -h 32 assets/cpu.svg -o icon-dmg.iconset/icon_16x16@2x.png 136 | rsvg-convert -w 32 -h 32 assets/cpu.svg -o icon-dmg.iconset/icon_32x32.png 137 | rsvg-convert -w 64 -h 64 assets/cpu.svg -o icon-dmg.iconset/icon_32x32@2x.png 138 | rsvg-convert -w 128 -h 128 assets/cpu.svg -o icon-dmg.iconset/icon_128x128.png 139 | rsvg-convert -w 256 -h 256 assets/cpu.svg -o icon-dmg.iconset/icon_128x128@2x.png 140 | rsvg-convert -w 256 -h 256 assets/cpu.svg -o icon-dmg.iconset/icon_256x256.png 141 | rsvg-convert -w 512 -h 512 assets/cpu.svg -o icon-dmg.iconset/icon_256x256@2x.png 142 | rsvg-convert -w 512 -h 512 assets/cpu.svg -o icon-dmg.iconset/icon_512x512.png 143 | rsvg-convert -w 1024 -h 1024 assets/cpu.svg -o icon-dmg.iconset/icon_512x512@2x.png 144 | iconutil -c icns icon-dmg.iconset -o assets/EasyEDA_to_KiCAD_Lib_UI-dmg.icns 145 | - name: Create macOS app bundle 146 | if: ${{ matrix.target == 'universal-apple-darwin' }} 147 | run: | 148 | mkdir -p EasyEDA_to_KiCAD_Lib_UI.app/Contents/MacOS 149 | mkdir -p EasyEDA_to_KiCAD_Lib_UI.app/Contents/Resources 150 | cp target/release/easyeda_to_kicad_lib_ui EasyEDA_to_KiCAD_Lib_UI.app/Contents/MacOS/EasyEDA_to_KiCAD_Lib_UI 151 | cp assets/EasyEDA_to_KiCAD_Lib_UI.icns EasyEDA_to_KiCAD_Lib_UI.app/Contents/Resources/EasyEDA_to_KiCAD_Lib_UI.icns 152 | cat << EOF > EasyEDA_to_KiCAD_Lib_UI.app/Contents/Info.plist 153 | 154 | 155 | 156 | 157 | CFBundleExecutable 158 | EasyEDA_to_KiCAD_Lib_UI 159 | CFBundleIconFile 160 | EasyEDA_to_KiCAD_Lib_UI 161 | CFBundleIdentifier 162 | com.example.EasyEDA_to_KiCAD_Lib_UI 163 | CFBundleName 164 | EasyEDA_to_KiCAD_Lib_UI 165 | CFBundlePackageType 166 | APPL 167 | CFBundleVersion 168 | ${{ steps.get_version.outputs.VERSION }} 169 | LSMinimumSystemVersion 170 | 10.13 171 | 172 | 173 | EOF 174 | - name: Create DMG 175 | if: ${{ matrix.target == 'universal-apple-darwin' }} 176 | run: | 177 | create-dmg \ 178 | --volname "EasyEDA_to_KiCAD_Lib_UI" \ 179 | --volicon "assets/EasyEDA_to_KiCAD_Lib_UI-dmg.icns" \ 180 | --window-pos 200 120 \ 181 | --window-size 800 400 \ 182 | --icon-size 100 \ 183 | --icon "EasyEDA_to_KiCAD_Lib_UI.app" 200 190 \ 184 | --hide-extension "EasyEDA_to_KiCAD_Lib_UI.app" \ 185 | --app-drop-link 600 185 \ 186 | "EasyEDA_to_KiCAD_Lib_UI-${{ steps.get_version.outputs.VERSION }}.dmg" \ 187 | "EasyEDA_to_KiCAD_Lib_UI.app" 188 | ls -l *.dmg 189 | - name: Build (non-Docker, non-macOS) 190 | if: ${{ !matrix.use_docker && matrix.target != 'universal-apple-darwin' }} 191 | uses: actions-rs/cargo@v1 192 | env: 193 | VERSION: '${{ steps.get_version.outputs.VERSION }}' 194 | REPOSITORY_NAME: '${{ steps.get_repository_name.outputs.REPOSITORY_NAME }}' 195 | with: 196 | command: build 197 | args: '--release' 198 | - name: Upload artifact 199 | uses: actions/upload-artifact@master 200 | with: 201 | name: ${{ steps.get_repository_name.outputs.REPOSITORY_NAME }}-${{ matrix.target }} 202 | path: | 203 | ${{ matrix.target == 'universal-apple-darwin' && format('EasyEDA_to_KiCAD_Lib_UI-{0}.dmg', steps.get_version.outputs.VERSION) || format('./target/{1}release/easyeda_to_kicad_lib_ui{0}', matrix.suffix, matrix.target == 'x86_64-unknown-linux-gnu' && 'x86_64-unknown-linux-gnu/' || '') }} 204 | 205 | release: 206 | needs: 207 | - build 208 | runs-on: ubuntu-latest 209 | steps: 210 | - uses: actions/checkout@master 211 | with: 212 | lfs: true 213 | - id: get_repository_name 214 | run: echo "REPOSITORY_NAME=$(echo "$GITHUB_REPOSITORY" | awk -F / '{print $2}' | sed -e "s/:refs//")" >> $GITHUB_OUTPUT 215 | shell: bash 216 | - uses: actions/download-artifact@master 217 | with: 218 | name: version 219 | path: ./version 220 | - id: get_version 221 | run: echo "VERSION=$(cat ./version/version)" >> $GITHUB_OUTPUT 222 | shell: bash 223 | - uses: actions/download-artifact@master 224 | with: 225 | name: ${{ steps.get_repository_name.outputs.REPOSITORY_NAME }}-x86_64-unknown-linux-gnu 226 | path: ./${{ steps.get_repository_name.outputs.REPOSITORY_NAME }}-x86_64-unknown-linux-gnu/ 227 | - uses: actions/download-artifact@master 228 | with: 229 | name: ${{ steps.get_repository_name.outputs.REPOSITORY_NAME }}-universal-apple-darwin 230 | path: ./${{ steps.get_repository_name.outputs.REPOSITORY_NAME }}-universal-apple-darwin/ 231 | - uses: actions/download-artifact@master 232 | with: 233 | name: ${{ steps.get_repository_name.outputs.REPOSITORY_NAME }}-x86_64-pc-windows-msvc 234 | path: ./${{ steps.get_repository_name.outputs.REPOSITORY_NAME }}-x86_64-pc-windows-msvc/ 235 | - run: ls -lah ./${{ steps.get_repository_name.outputs.REPOSITORY_NAME }}-universal-apple-darwin/ 236 | - uses: actions-rs/toolchain@v1 237 | with: 238 | profile: minimal 239 | toolchain: stable 240 | override: true 241 | - id: create_release 242 | uses: actions/create-release@latest 243 | env: 244 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 245 | with: 246 | tag_name: '${{ steps.get_version.outputs.VERSION }}' 247 | release_name: 'Release ${{ steps.get_version.outputs.VERSION }}' 248 | draft: false 249 | prerelease: false 250 | - uses: actions/upload-release-asset@latest 251 | env: 252 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 253 | with: 254 | upload_url: '${{ steps.create_release.outputs.upload_url }}' 255 | asset_path: ./${{ steps.get_repository_name.outputs.REPOSITORY_NAME }}-x86_64-unknown-linux-gnu/easyeda_to_kicad_lib_ui 256 | asset_name: ${{ steps.get_repository_name.outputs.REPOSITORY_NAME }}-x86_64-unknown-linux-gnu 257 | asset_content_type: application/octet-stream 258 | - uses: actions/upload-release-asset@latest 259 | env: 260 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 261 | with: 262 | upload_url: '${{ steps.create_release.outputs.upload_url }}' 263 | asset_path: ./${{ steps.get_repository_name.outputs.REPOSITORY_NAME }}-universal-apple-darwin/EasyEDA_to_KiCAD_Lib_UI-${{ steps.get_version.outputs.VERSION }}.dmg 264 | asset_name: ${{ steps.get_repository_name.outputs.REPOSITORY_NAME }}-universal-apple-darwin.dmg 265 | asset_content_type: application/x-diskcopy 266 | - uses: actions/upload-release-asset@latest 267 | env: 268 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 269 | with: 270 | upload_url: '${{ steps.create_release.outputs.upload_url }}' 271 | asset_path: ./${{ steps.get_repository_name.outputs.REPOSITORY_NAME }}-x86_64-pc-windows-msvc/easyeda_to_kicad_lib_ui.exe 272 | asset_name: ${{ steps.get_repository_name.outputs.REPOSITORY_NAME }}-x86_64-pc-windows-msvc.exe 273 | asset_content_type: application/octet-stream -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::VecDeque, 3 | fs::{create_dir_all, read_to_string, File}, 4 | io::Write, 5 | path::Path, 6 | }; 7 | 8 | use arboard::Clipboard; 9 | use egui::{TextEdit, Vec2, Window}; 10 | use egui_dropdown::DropDownBox; 11 | use egui_extras::{Column, TableBuilder}; 12 | use glob::glob; 13 | use indexmap::{indexmap, IndexMap}; 14 | use regex::Regex; 15 | use subprocess::Exec; 16 | 17 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 18 | 19 | /// We derive Deserialize/Serialize so we can persist app state on shutdown. 20 | #[derive(serde::Deserialize, serde::Serialize)] 21 | #[serde(default)] // if we add new fields, give them default values when deserializing old state 22 | pub struct MyApp { 23 | part: String, 24 | exe_path: String, 25 | output_path: String, 26 | symbol_lib: String, 27 | symbol_lib_dir: String, 28 | footprint_lib: String, 29 | model_dir: String, 30 | model_base_variable: String, 31 | datasheet_dir: String, 32 | download_datasheet: bool, 33 | skip_existing: bool, 34 | no_footprint: bool, 35 | no_symbol: bool, 36 | history: VecDeque, 37 | #[serde(skip)] 38 | tempdir: Option, 39 | #[serde(skip)] 40 | settings_open: bool, 41 | #[serde(skip)] 42 | is_init: bool, 43 | #[serde(skip)] 44 | search_good: bool, 45 | #[serde(skip)] 46 | current_part: IndexMap, 47 | } 48 | 49 | impl Default for MyApp { 50 | fn default() -> Self { 51 | Self { 52 | part: "C11702".to_owned(), 53 | exe_path: "JLC2KiCadLib".to_owned(), 54 | output_path: "~/kicad_libs/".to_owned(), 55 | symbol_lib: "default_lib".to_owned(), 56 | symbol_lib_dir: "symbol".to_owned(), 57 | footprint_lib: "footprint".to_owned(), 58 | model_dir: "packages3d".to_owned(), 59 | model_base_variable: "".to_owned(), 60 | datasheet_dir: "~/kicad_libs/datasheets".to_owned(), 61 | download_datasheet: true, 62 | skip_existing: false, 63 | no_footprint: false, 64 | no_symbol: false, 65 | history: VecDeque::with_capacity(11), 66 | tempdir: tempfile::Builder::new().prefix("easyedatokicadlib").tempdir().ok(), 67 | settings_open: false, 68 | is_init: false, 69 | search_good: true, 70 | current_part: indexmap! {}, 71 | } 72 | } 73 | } 74 | 75 | impl MyApp { 76 | /// Called once before the first frame. 77 | pub fn new(cc: &eframe::CreationContext<'_>) -> Self { 78 | // This is also where you can customize the look and feel of egui using 79 | // `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`. 80 | 81 | // Load previous app state (if any). 82 | // Note that you must enable the `persistence` feature for this to work. 83 | if let Some(storage) = cc.storage { 84 | return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default(); 85 | } 86 | 87 | Default::default() 88 | } 89 | 90 | fn get_imglist(lcscnumber: &str, client: &reqwest::blocking::Client) -> Option> { 91 | // this is the fallback function for when JLCPCB gives us no images, then we resort to asking LCSC 92 | let res_or_err = client 93 | .get(format!( 94 | "https://wmsc.lcsc.com/ftps/wm/product/detail?productCode={}", 95 | lcscnumber 96 | )) 97 | .header(reqwest::header::ACCEPT, "application/json") 98 | .header( 99 | reqwest::header::USER_AGENT, 100 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0", 101 | ) 102 | .send(); 103 | if let Ok(res) = res_or_err { 104 | if res.status().is_success() { 105 | let pictext = res 106 | .text() 107 | .expect("Issue decoding received response from LCSC."); 108 | let json: serde_json::Value = 109 | serde_json::from_str(&pictext).expect("Issue parsing search result JSON."); 110 | 111 | // there is a case where we get a fully valid response in an HTML 112 | // and JSON sense but it tells us via a code field in the JSON 113 | // that no part could be found, in that case we return early with nothing 114 | if let Some(code) = json.get("code") { 115 | if code != 200 { 116 | return None; 117 | } 118 | } 119 | let mut imglist = vec![]; 120 | if let Some(data) = json.get("result") { 121 | if let Some(imagelist) = data.get("productImages") { 122 | if let Some(imagevec) = imagelist.as_array() { 123 | for img in imagevec.iter() { 124 | imglist.push(img.to_string().trim_matches('"').to_owned()); 125 | } 126 | return Some(imglist); 127 | } 128 | } 129 | } 130 | } 131 | } 132 | // if we fall through to here, we failed getting data somewhere along the way 133 | None 134 | } 135 | 136 | fn get_part(search_term: &str) -> Option> { 137 | let term = search_term.trim(); 138 | let re_jlc = Regex::new(r"/(C\d+)$").unwrap(); 139 | let re_lcsc = Regex::new(r"_(C\d+)[^/]*\.html$").unwrap(); 140 | let re_lcscnumber = Regex::new(r"^C(\d+)$").unwrap(); 141 | let mut lcscnumber = ""; 142 | 143 | // case one, we got passed a URL 144 | if term.contains("http") { 145 | if term.contains("jlcpcb.com") { 146 | if let Some(captures) = re_jlc.captures(term) { 147 | lcscnumber = captures.get(1).unwrap().as_str(); 148 | } 149 | } else if term.contains("lcsc.com") { 150 | if let Some(captures) = re_lcsc.captures(term) { 151 | lcscnumber = captures.get(1).unwrap().as_str(); 152 | } 153 | } 154 | // case two, it's the number directly 155 | } else if term.starts_with("C") { 156 | lcscnumber = term; 157 | } 158 | 159 | // ensure we only make requests if what we have looks like an LCSC number and can work, 160 | // also saves us from urlencoding and such because it will only ever be "C" followed by some numbers 161 | if re_lcscnumber.is_match(lcscnumber) { 162 | let client = reqwest::blocking::Client::new(); 163 | let res_or_err = client 164 | .get(format!("https://cart.jlcpcb.com/shoppingCart/smtGood/getComponentDetail?componentCode={}", lcscnumber)) 165 | .header(reqwest::header::ACCEPT, "application/json") 166 | .send(); 167 | if let Ok(res) = res_or_err { 168 | let res_status = res.status(); 169 | if res_status.is_success() { 170 | let res_text = res 171 | .text() 172 | .expect("Issue decoding received response from JLCPCB."); 173 | let json: serde_json::Value = 174 | serde_json::from_str(&res_text).expect("Issue parsing search result JSON."); 175 | // only debug: println!("{}", json); 176 | let parameters = indexmap! { 177 | "componentCode" => "Component Code", 178 | "firstTypeNameEn" => "Primary Category", 179 | "secondTypeNameEn" => "Secondary Category", 180 | "componentBrandEn" => "Brand", 181 | "componentName" => "Full Name", 182 | "componentDesignator" => "Designator", 183 | "componentModelEn" => "Model", 184 | "componentSpecificationEn" => "Specification", 185 | "assemblyProcess" => "Assembly Process", 186 | "describe" => "Description", 187 | "matchedPartDetail" => "Details", 188 | "stockCount" => "Stock", 189 | "leastNumber" => "Minimal Quantity", 190 | "leastNumberPrice" => "Minimum Price", 191 | }; 192 | 193 | // there is a case where we get a fully valid response in an HTML 194 | // and JSON sense but it tells us via a code field in the JSON 195 | // that no part could be found, in that case we exit early 196 | if let Some(code) = json.get("code") { 197 | if code != 200 { 198 | return None; 199 | } 200 | } 201 | 202 | // if the data section is there as expected, we start taking it apart 203 | if let Some(data) = json.get("data") { 204 | let mut tabledata: IndexMap = indexmap! {}; 205 | 206 | // determine if it is a JLCPCB basic or extended assembly part 207 | if let Some(parttype) = data.get("componentLibraryType") { 208 | if parttype == "base" { 209 | tabledata.insert("Type".to_owned(), "Basic".to_owned()); 210 | } else if parttype == "expand" { 211 | tabledata.insert("Type".to_owned(), "Extended".to_owned()); 212 | } 213 | } 214 | 215 | // now pretty-format the parameters that should always be there 216 | for (key, title) in parameters { 217 | if let Some(value) = data.get(key) { 218 | tabledata.insert( 219 | title.to_owned(), 220 | value.to_string().trim_matches('"').to_owned(), 221 | ); 222 | } 223 | } 224 | 225 | // now the component specific attributes, these are in a nested array within 226 | // the JSON and vary by component 227 | if let Some(attributes) = data.get("attributes") { 228 | if let Some(array) = attributes.as_array() { 229 | for attribute in array { 230 | if let Some(name) = attribute.get("attribute_name_en") { 231 | if let Some(value) = attribute.get("attribute_value_name") { 232 | tabledata.insert( 233 | name.to_string().trim_matches('"').to_owned(), 234 | value.to_string().trim_matches('"').to_owned(), 235 | ); 236 | } 237 | } 238 | } 239 | } 240 | } 241 | 242 | // here we gather metadata for the image and datasheet URLs 243 | if let Some(imagelist) = data.get("imageList") { 244 | if let Some(imagevec) = imagelist.as_array() { 245 | for (idx, i) in imagevec.iter().enumerate() { 246 | if let Some(imageurl) = i.get("productBigImage") { 247 | // this is a f*ed up case where JLC returns API IDs instead of URLs 248 | if imageurl.is_null() { 249 | // this does not work right now because of MIME type issues, get from LCSC instead 250 | // if let Some(imageid) = i.get("productBigImageAccessId") 251 | // { 252 | // let apiurl = format!("https://jlcpcb.com/api/file/downloadByFileSystemAccessId/{}.jpg", imageid.to_string().trim_matches('"').to_owned()); 253 | // tabledata 254 | // .insert(format!("meta_image{}", idx), apiurl); 255 | // } 256 | if let Some(lcsc_imglist) = 257 | MyApp::get_imglist(lcscnumber, &client) 258 | { 259 | for (idx, i) in lcsc_imglist.iter().enumerate() { 260 | tabledata.insert( 261 | format!("meta_image{}", idx), 262 | i.to_owned(), 263 | ); 264 | } 265 | } 266 | break; 267 | } else { 268 | tabledata.insert( 269 | format!("meta_image{}", idx), 270 | imageurl.to_string().trim_matches('"').to_owned(), 271 | ); 272 | } 273 | } 274 | } 275 | } 276 | } 277 | if let Some(datasheeturl) = data.get("dataManualUrl") { 278 | tabledata.insert( 279 | "meta_datasheeturl".to_owned(), 280 | datasheeturl.to_string().trim_matches('"').to_owned(), 281 | ); 282 | } 283 | return Some(tabledata); 284 | } 285 | } 286 | } 287 | } 288 | // if we fall through to here, we failed getting data somewhere along the way 289 | None 290 | } 291 | } 292 | 293 | impl eframe::App for MyApp { 294 | /// Called by the frame work to save state before shutdown. 295 | fn save(&mut self, storage: &mut dyn eframe::Storage) { 296 | eframe::set_value(storage, eframe::APP_KEY, self); 297 | } 298 | 299 | /// Called each time the UI needs repainting, which may be many times per second. 300 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 301 | // Put your widgets into a `SidePanel`, `TopBottomPanel`, `CentralPanel`, `Window` or `Area`. 302 | // For inspiration and more examples, go to https://emilk.github.io/egui 303 | let is_web = cfg!(target_arch = "wasm32"); 304 | 305 | // on startup the current_part IndexMap is empty even if a part is set, so we populate it 306 | if !self.is_init && self.current_part.is_empty() && !self.part.is_empty() { 307 | if let Some(tabledata) = Self::get_part(self.part.as_str()) { 308 | self.current_part = tabledata; 309 | self.search_good = true; 310 | } else { 311 | self.search_good = false; 312 | } 313 | self.is_init = true 314 | } 315 | 316 | egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { 317 | // The top panel is often a good place for a menu bar: 318 | 319 | egui::menu::bar(ui, |ui| { 320 | // NOTE: no File->Quit on web pages! 321 | ui.menu_button("File", |ui| { 322 | if ui.button("Settings").clicked() { 323 | self.settings_open = true; 324 | } 325 | if !is_web && ui.button("Quit").clicked() { 326 | ctx.send_viewport_cmd(egui::ViewportCommand::Close); 327 | } 328 | }); 329 | ui.add_space(16.0); 330 | 331 | egui::widgets::global_theme_preference_buttons(ui); 332 | }); 333 | }); 334 | 335 | egui::CentralPanel::default().show(ctx, |ui| { 336 | // The central panel the region left after adding TopPanel's and SidePanel's 337 | if is_web { 338 | ui.heading("EasyEDA to KiCAD Library Converter"); 339 | } 340 | let mut imagevec = vec![]; 341 | 342 | ui.vertical(|ui| { 343 | ui.horizontal(|ui| { 344 | ui.label("LCSC number or part URL: "); 345 | // ui.add(TextEdit::singleline(&mut self.part).desired_width(800.0)); 346 | 347 | ui.add( 348 | DropDownBox::from_iter( 349 | &self.history, 350 | "searchbox", 351 | &mut self.part, 352 | |ui, text| ui.selectable_label(false, text), 353 | ) 354 | .desired_width(800.0) 355 | .select_on_focus(true) 356 | .filter_by_input(false), 357 | ); 358 | 359 | if ui.button("Search").clicked() { 360 | self.part = self.part.trim().to_owned(); 361 | if let Some(tabledata) = Self::get_part(self.part.as_str()) { 362 | self.current_part = tabledata; 363 | self.search_good = true; 364 | // handle history 365 | self.history.push_front(self.part.clone()); 366 | self.history.truncate(10); 367 | } else { 368 | self.search_good = false; 369 | } 370 | } 371 | }); 372 | ui.horizontal(|ui| { 373 | if self.search_good { 374 | ui.label(format!( 375 | "Current Part: {}", 376 | self.current_part 377 | .get("Component Code") 378 | .unwrap_or(&"".to_owned()) 379 | )); 380 | if ui.button("Add to Library").clicked() { 381 | if let Some(curr_part) = self.current_part.get("Component Code") { 382 | let mut args = vec![ 383 | curr_part, 384 | "-dir", 385 | &self.output_path, 386 | "-symbol_lib", 387 | &self.symbol_lib, 388 | "-symbol_lib_dir", 389 | &self.symbol_lib_dir, 390 | "-footprint_lib", 391 | &self.footprint_lib, 392 | "-model_dir", 393 | &self.model_dir, 394 | ]; 395 | if !self.model_base_variable.is_empty() { 396 | args.push("-model_base_variable"); 397 | args.push(&self.model_base_variable); 398 | } 399 | if self.skip_existing { 400 | args.push("--skip_existing"); 401 | } 402 | if self.no_footprint { 403 | args.push("--no_footprint"); 404 | } 405 | if self.no_symbol { 406 | args.push("--no_symbol"); 407 | } 408 | let _ = Exec::cmd(&self.exe_path).args(&args).popen(); 409 | if self.download_datasheet { 410 | let dlpath = Path::new(&self.datasheet_dir); 411 | if !dlpath.is_dir() { 412 | let _ = create_dir_all(dlpath); 413 | } 414 | if let Some(url) = self.current_part.get("meta_datasheeturl") { 415 | // the datasheet url points to an integrated parts view frame with an embedded pdf viewer 416 | // we need to modify it for the download of the actual file 417 | // https://datasheet.lcsc.com/lcsc/2206010216_UNI-ROYAL-Uniroyal-Elec-0402WGF1001TCE_C11702.pdf 418 | // https://wmsc.lcsc.com/wmsc/upload/file/pdf/v2/lcsc/2206010216_UNI-ROYAL-Uniroyal-Elec-0402WGF1001TCE_C11702.pdf 419 | let pdf_url = url.replace("https://www.lcsc.com/datasheet/lcsc_datasheet_", "https://wmsc.lcsc.com/wmsc/upload/file/pdf/v2/lcsc/"); 420 | println!("PDF-URL: {}", pdf_url); // Debug log 421 | let filename = pdf_url.rsplit('/').next().unwrap_or(&format!("{}.pdf", curr_part)).to_string(); 422 | let dest_path = dlpath.join(filename); 423 | let client = reqwest::blocking::Client::new(); 424 | if let Ok(response) = client 425 | .get(&pdf_url) 426 | .header( 427 | reqwest::header::USER_AGENT, 428 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0", 429 | ) 430 | .send() 431 | { 432 | let content_type = response 433 | .headers() 434 | .get(reqwest::header::CONTENT_TYPE) 435 | .map(|v| v.to_str().unwrap_or("unknown")) 436 | .unwrap_or("unknown"); 437 | println!("Content-Type: {}", content_type); // Debug log 438 | if response.status().is_success() && content_type.contains("application/pdf") { 439 | if let Ok(bytes) = response.bytes() { 440 | let _ = File::create(&dest_path).and_then(|mut file| file.write_all(&bytes)); 441 | } 442 | } 443 | } 444 | } 445 | } 446 | } 447 | } 448 | // in the rare case the temp dir cannot be created or isn't a UTF8 path, 449 | // we just do not render the button 450 | if let Some(tempdir) = &self.tempdir { 451 | if let Some(tempdirstr) = tempdir.path().as_os_str().to_str() { 452 | if ui.button("Copy Footprint").clicked() { 453 | if let Some(curr_part) = self.current_part.get("Component Code") 454 | { 455 | println!( 456 | "Temporary Directory for Footprint: {}", 457 | tempdirstr 458 | ); 459 | let args = vec![ 460 | curr_part, 461 | "--no_symbol", 462 | "-dir", 463 | tempdirstr, 464 | "-footprint_lib", 465 | curr_part, 466 | "-model_dir", 467 | "packages3d", 468 | ]; 469 | let _ = Exec::cmd(&self.exe_path).args(&args).popen(); 470 | 471 | // now copy the generated footprint to the clipboard 472 | let glob = glob( 473 | format!("{}/{}/*.kicad_mod", tempdirstr, curr_part) 474 | .as_str(), 475 | ) 476 | .ok(); 477 | if let Some(paths) = glob { 478 | for p in paths { 479 | match p { 480 | Ok(path) => { 481 | if let Ok(mut clipboard) = Clipboard::new() 482 | { 483 | if let Ok(contents) = read_to_string(path) 484 | { 485 | let _ = clipboard.set_text(contents); 486 | } 487 | } 488 | } 489 | Err(e) => println!("{:?}", e), 490 | } 491 | } 492 | } 493 | } 494 | } 495 | } 496 | } 497 | } else { 498 | ui.label("No such part found. Check part number or URL!"); 499 | } 500 | }); 501 | }); 502 | 503 | ui.separator(); 504 | 505 | ui.vertical(|ui| { 506 | TableBuilder::new(ui) 507 | .striped(true) 508 | .resizable(true) 509 | .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) 510 | .column(Column::initial(170.0).at_least(90.0)) 511 | .column(Column::initial(400.0).at_least(170.0)) 512 | .header(20.0, |mut header| { 513 | header.col(|ui| { 514 | ui.heading("Parameter"); 515 | }); 516 | header.col(|ui| { 517 | ui.heading("Value"); 518 | }); 519 | }) 520 | .body(|mut body| { 521 | for (key, value) in &self.current_part { 522 | if !key.starts_with("meta_") { 523 | body.row(15.0, |mut row| { 524 | row.col(|ui| { 525 | ui.label(key); 526 | }); 527 | row.col(|ui| { 528 | ui.label(value); 529 | if key == "Component Code" { 530 | ui.hyperlink_to( 531 | "LCSC", 532 | format!( 533 | "https://www.lcsc.com/product-detail/{}.html", 534 | value 535 | ), 536 | ); 537 | ui.hyperlink_to( 538 | "JLCPCB", 539 | format!("https://jlcpcb.com/partdetail/{}", value), 540 | ); 541 | } 542 | }); 543 | }); 544 | } else if key.starts_with("meta_datasheeturl") { 545 | body.row(15.0, |mut row| { 546 | row.col(|ui| { 547 | ui.label("Datasheet"); 548 | }); 549 | row.col(|ui| { 550 | ui.hyperlink(value); 551 | }); 552 | }); 553 | } else if key.starts_with("meta_image") { 554 | imagevec.push(value); 555 | } 556 | } 557 | }); 558 | 559 | ui.separator(); 560 | 561 | ui.horizontal(|ui| { 562 | for url in imagevec { 563 | let img = ui 564 | .add(egui::Image::new(url).fit_to_exact_size(Vec2::new(200.0, 200.0))); 565 | if is_hover_rect(ui, img.rect) { 566 | Window::new("") 567 | .auto_sized() 568 | .interactable(false) 569 | .order(egui::Order::Foreground) 570 | .fade_in(false) 571 | .fade_out(false) 572 | .collapsible(false) 573 | .show(ctx, |ui| { 574 | ui.add( 575 | egui::Image::new(url).fit_to_exact_size(Vec2::new(900.0, 900.0)), 576 | ); 577 | }); 578 | } 579 | } 580 | }); 581 | }); 582 | 583 | ui.separator(); 584 | 585 | ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { 586 | powered_by(ui); 587 | ui.hyperlink_to( 588 | format!("Version: v{VERSION}"), 589 | format!("https://github.com/markusdd/EasyEDA_to_KiCAD_Lib_UI/releases/tag/v{VERSION}"), 590 | ); 591 | egui::warn_if_debug_build(ui); 592 | }); 593 | 594 | //settings window 595 | if self.settings_open { 596 | Window::new("Settings") 597 | .auto_sized() 598 | .interactable(true) 599 | .show(ctx, |ui| { 600 | ui.vertical(|ui| { 601 | ui.heading("Settings"); 602 | ui.checkbox(&mut self.download_datasheet, "Download datasheet"); 603 | ui.checkbox(&mut self.skip_existing, "Skip existing components"); 604 | ui.checkbox(&mut self.no_footprint, "Skip footprint generation"); 605 | ui.checkbox(&mut self.no_symbol, "Skip symbol generation"); 606 | ui.label("Path of JLC2KiCadLib executable:"); 607 | ui.add(TextEdit::singleline(&mut self.exe_path).desired_width(800.0)); 608 | ui.label("Output directory for the generated library (absolute path):"); 609 | ui.add( 610 | TextEdit::singleline(&mut self.output_path).desired_width(800.0), 611 | ); 612 | ui.label( 613 | "Name of the symbol directory (relative to output directory):", 614 | ); 615 | ui.add( 616 | TextEdit::singleline(&mut self.symbol_lib_dir).desired_width(800.0), 617 | ); 618 | ui.label( 619 | "Name of the footprint library (relative to output directory):", 620 | ); 621 | ui.add( 622 | TextEdit::singleline(&mut self.footprint_lib).desired_width(800.0), 623 | ); 624 | ui.label("Name of the symbol library:"); 625 | ui.add(TextEdit::singleline(&mut self.symbol_lib).desired_width(800.0)); 626 | ui.label( 627 | "Name of the 3D model directory (relative to footprint directory):", 628 | ); 629 | ui.add(TextEdit::singleline(&mut self.model_dir).desired_width(800.0)); 630 | ui.label("Base path variable for 3D Models (start with $):"); 631 | ui.add( 632 | TextEdit::singleline(&mut self.model_base_variable) 633 | .desired_width(800.0), 634 | ); 635 | ui.label("Output directory for downloaded datasheets (absolute path):"); 636 | ui.add( 637 | TextEdit::singleline(&mut self.datasheet_dir).desired_width(800.0), 638 | ); 639 | if ui.button("Close").clicked() { 640 | self.settings_open = false; 641 | } 642 | }); 643 | }); 644 | } 645 | }); 646 | } 647 | } 648 | 649 | pub fn is_hover_rect(ui: &egui::Ui, rect: egui::Rect) -> bool { 650 | let pointer_pos = ui.input(|i| i.pointer.hover_pos()); 651 | let Some(pos) = pointer_pos else { 652 | return false; 653 | }; 654 | 655 | rect.contains(pos) 656 | } 657 | 658 | fn powered_by(ui: &mut egui::Ui) { 659 | ui.horizontal(|ui| { 660 | ui.spacing_mut().item_spacing.x = 0.0; 661 | ui.label("Powered by "); 662 | ui.hyperlink_to("egui", "https://github.com/emilk/egui"); 663 | ui.label(", "); 664 | ui.hyperlink_to( 665 | "eframe", 666 | "https://github.com/emilk/egui/tree/master/crates/eframe", 667 | ); 668 | ui.label(" and "); 669 | ui.hyperlink_to( 670 | "JLC2KiCad_lib", 671 | "https://github.com/TousstNicolas/JLC2KiCad_lib", 672 | ); 673 | ui.label("."); 674 | }); 675 | } 676 | --------------------------------------------------------------------------------