├── 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 |
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 | [](https://ko-fi.com/R6R8DQO8C)
6 |
7 | 
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 | 
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 |
--------------------------------------------------------------------------------