commit 4f5420b65894f993bd9f64068f04fea320f6071a Author: Vixalie Date: Wed Feb 26 05:39:36 2025 +0800 project initiate. diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..641881e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,74 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 +max_line_length = 100 + +[*.java] +indent_size = 4 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 + +[*.{yml, yaml}] +indent_style = space +indent_size = 2 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 + +[*.json] +indent_style = space +indent_size = 2 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 + +[*.sql] +indent_style = space +indent_size = 2 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 + +[*.go] +indent_style = space +indent_size = 4 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 + +[*.py] +indent_style = space +indent_size = 4 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 + +[*.sh] +indent_style = space +indent_size = 2 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 + +[*.rs] +indent_size = 4 +indent_style = space +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6842b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,1033 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Log/OS Files +*.log + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.apk +output.json + +# IntelliJ +*.iml +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Android Profiling +*.hprof + +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps +CMakeUserPresets.json + +# See https://www.dartlang.org/guides/libraries/private-files + +# Files and directories created by pub +.dart_tool/ +.packages +build/ +# If you're building an application, you may want to check-in your pubspec.lock +pubspec.lock + +# Directory created by dartdoc +# If you don't generate documentation locally you can remove this line. +doc/api/ + +# dotenv environment variables file +.env* + +# Avoid committing generated Javascript files: +*.dart.js +*.info.json # Produced by the --dump-info flag. +*.js # When generated by dart2js. Don't specify *.js if your + # project includes source files written in JavaScript. +*.js_ +*.js.deps +*.js.map + +.flutter-plugins +.flutter-plugins-dependencies + +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar + +# Eclipse m2e generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +# This .gitignore is appropriate for repositories deployed to GitHub Pages and using +# a Gemfile as specified at https://github.com/github/pages-gem#conventional + +# Basic Jekyll gitignores (synchronize to Jekyll.gitignore) +_site/ +.sass-cache/ +.jekyll-cache/ +.jekyll-metadata + +# Additional Ruby/bundler ignore for when you run: bundle install +/vendor + +# Specific ignore for GitHub Pages +# GitHub Pages will always use its own deployed version of pages-gem +# This means GitHub Pages will NOT use your Gemfile.lock and therefore it is +# counterproductive to check this file into the repository. +# Details at https://github.com/github/pages-gem/issues/768 +Gemfile.lock + +!**/*.module.css diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..d592b12 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +**/*.md +.env +.env.* diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ab14e5f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,12 @@ +{ + "bracketSpacing": true, + "bracketSameLine": true, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "jsxSingleQuote": false, + "trailingComma": "all", + "arrowParens": "always", + "printWidth": 100 +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..24d7cc6 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..102e366 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Tauri + React + Typescript + +This template should help get you started developing with Tauri, React and Typescript in Vite. + +## Recommended IDE Setup + +- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..d1af3c3 --- /dev/null +++ b/deno.lock @@ -0,0 +1,1416 @@ +{ + "version": "4", + "specifiers": { + "npm:@eslint/js@^9.19.0": "9.21.0", + "npm:@iconify/react@^5.2.0": "5.2.0_react@19.0.0", + "npm:@tauri-apps/api@2": "2.2.0", + "npm:@tauri-apps/cli@2": "2.2.7", + "npm:@tauri-apps/plugin-dialog@2.2": "2.2.0", + "npm:@tauri-apps/plugin-notification@~2.2.1": "2.2.1", + "npm:@tauri-apps/plugin-opener@2": "2.2.5", + "npm:@tauri-apps/plugin-os@2.2": "2.2.0", + "npm:@types/lodash-es@^4.17.12": "4.17.12", + "npm:@types/react-dom@19.0.4": "19.0.4_@types+react@19.0.10", + "npm:@types/react@19.0.10": "19.0.10", + "npm:@types/uuid@10": "10.0.0", + "npm:@vitejs/plugin-react@^4.3.4": "4.3.4_vite@6.2.0__lightningcss@1.29.1_@babel+core@7.26.9_lightningcss@1.29.1", + "npm:clsx@^2.1.1": "2.1.1", + "npm:dayjs@^1.11.13": "1.11.13", + "npm:eslint@^9.19.0": "9.21.0", + "npm:jotai@^2.12.1": "2.12.1_@types+react@19.0.10_react@19.0.0", + "npm:lightningcss@^1.29.1": "1.29.1", + "npm:lodash-es@^4.17.21": "4.17.21", + "npm:react-dom@19.0.0": "19.0.0_react@19.0.0", + "npm:react-router-dom@^7.2.0": "7.2.0_react@19.0.0_react-dom@19.0.0__react@19.0.0", + "npm:react@19.0.0": "19.0.0", + "npm:sanitize.css@13": "13.0.0", + "npm:typescript-eslint@^8.22.0": "8.25.0_eslint@9.21.0_typescript@5.7.3_@typescript-eslint+parser@8.25.0__eslint@9.21.0__typescript@5.7.3", + "npm:typescript@~5.7.3": "5.7.3", + "npm:uuid@^11.0.5": "11.1.0", + "npm:vite@^6.0.3": "6.2.0_lightningcss@1.29.1" + }, + "npm": { + "@ampproject/remapping@2.3.0": { + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dependencies": [ + "@jridgewell/gen-mapping", + "@jridgewell/trace-mapping" + ] + }, + "@babel/code-frame@7.26.2": { + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dependencies": [ + "@babel/helper-validator-identifier", + "js-tokens", + "picocolors" + ] + }, + "@babel/compat-data@7.26.8": { + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==" + }, + "@babel/core@7.26.9": { + "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", + "dependencies": [ + "@ampproject/remapping", + "@babel/code-frame", + "@babel/generator", + "@babel/helper-compilation-targets", + "@babel/helper-module-transforms", + "@babel/helpers", + "@babel/parser", + "@babel/template", + "@babel/traverse", + "@babel/types", + "convert-source-map", + "debug", + "gensync", + "json5", + "semver@6.3.1" + ] + }, + "@babel/generator@7.26.9": { + "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==", + "dependencies": [ + "@babel/parser", + "@babel/types", + "@jridgewell/gen-mapping", + "@jridgewell/trace-mapping", + "jsesc" + ] + }, + "@babel/helper-compilation-targets@7.26.5": { + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "dependencies": [ + "@babel/compat-data", + "@babel/helper-validator-option", + "browserslist", + "lru-cache", + "semver@6.3.1" + ] + }, + "@babel/helper-module-imports@7.25.9": { + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dependencies": [ + "@babel/traverse", + "@babel/types" + ] + }, + "@babel/helper-module-transforms@7.26.0_@babel+core@7.26.9": { + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dependencies": [ + "@babel/core", + "@babel/helper-module-imports", + "@babel/helper-validator-identifier", + "@babel/traverse" + ] + }, + "@babel/helper-plugin-utils@7.26.5": { + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==" + }, + "@babel/helper-string-parser@7.25.9": { + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==" + }, + "@babel/helper-validator-identifier@7.25.9": { + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==" + }, + "@babel/helper-validator-option@7.25.9": { + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==" + }, + "@babel/helpers@7.26.9": { + "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==", + "dependencies": [ + "@babel/template", + "@babel/types" + ] + }, + "@babel/parser@7.26.9": { + "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "dependencies": [ + "@babel/types" + ] + }, + "@babel/plugin-transform-react-jsx-self@7.25.9_@babel+core@7.26.9": { + "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==", + "dependencies": [ + "@babel/core", + "@babel/helper-plugin-utils" + ] + }, + "@babel/plugin-transform-react-jsx-source@7.25.9_@babel+core@7.26.9": { + "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==", + "dependencies": [ + "@babel/core", + "@babel/helper-plugin-utils" + ] + }, + "@babel/template@7.26.9": { + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "dependencies": [ + "@babel/code-frame", + "@babel/parser", + "@babel/types" + ] + }, + "@babel/traverse@7.26.9": { + "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==", + "dependencies": [ + "@babel/code-frame", + "@babel/generator", + "@babel/parser", + "@babel/template", + "@babel/types", + "debug", + "globals@11.12.0" + ] + }, + "@babel/types@7.26.9": { + "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "dependencies": [ + "@babel/helper-string-parser", + "@babel/helper-validator-identifier" + ] + }, + "@esbuild/aix-ppc64@0.25.0": { + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==" + }, + "@esbuild/android-arm64@0.25.0": { + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==" + }, + "@esbuild/android-arm@0.25.0": { + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==" + }, + "@esbuild/android-x64@0.25.0": { + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==" + }, + "@esbuild/darwin-arm64@0.25.0": { + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==" + }, + "@esbuild/darwin-x64@0.25.0": { + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==" + }, + "@esbuild/freebsd-arm64@0.25.0": { + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==" + }, + "@esbuild/freebsd-x64@0.25.0": { + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==" + }, + "@esbuild/linux-arm64@0.25.0": { + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==" + }, + "@esbuild/linux-arm@0.25.0": { + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==" + }, + "@esbuild/linux-ia32@0.25.0": { + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==" + }, + "@esbuild/linux-loong64@0.25.0": { + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==" + }, + "@esbuild/linux-mips64el@0.25.0": { + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==" + }, + "@esbuild/linux-ppc64@0.25.0": { + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==" + }, + "@esbuild/linux-riscv64@0.25.0": { + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==" + }, + "@esbuild/linux-s390x@0.25.0": { + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==" + }, + "@esbuild/linux-x64@0.25.0": { + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==" + }, + "@esbuild/netbsd-arm64@0.25.0": { + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==" + }, + "@esbuild/netbsd-x64@0.25.0": { + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==" + }, + "@esbuild/openbsd-arm64@0.25.0": { + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==" + }, + "@esbuild/openbsd-x64@0.25.0": { + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==" + }, + "@esbuild/sunos-x64@0.25.0": { + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==" + }, + "@esbuild/win32-arm64@0.25.0": { + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==" + }, + "@esbuild/win32-ia32@0.25.0": { + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==" + }, + "@esbuild/win32-x64@0.25.0": { + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==" + }, + "@eslint-community/eslint-utils@4.4.1_eslint@9.21.0": { + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dependencies": [ + "eslint", + "eslint-visitor-keys@3.4.3" + ] + }, + "@eslint-community/regexpp@4.12.1": { + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==" + }, + "@eslint/config-array@0.19.2": { + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "dependencies": [ + "@eslint/object-schema", + "debug", + "minimatch@3.1.2" + ] + }, + "@eslint/core@0.12.0": { + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "dependencies": [ + "@types/json-schema" + ] + }, + "@eslint/eslintrc@3.3.0": { + "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", + "dependencies": [ + "ajv", + "debug", + "espree", + "globals@14.0.0", + "ignore", + "import-fresh", + "js-yaml", + "minimatch@3.1.2", + "strip-json-comments" + ] + }, + "@eslint/js@9.21.0": { + "integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==" + }, + "@eslint/object-schema@2.1.6": { + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==" + }, + "@eslint/plugin-kit@0.2.7": { + "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "dependencies": [ + "@eslint/core", + "levn" + ] + }, + "@humanfs/core@0.19.1": { + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==" + }, + "@humanfs/node@0.16.6": { + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dependencies": [ + "@humanfs/core", + "@humanwhocodes/retry@0.3.1" + ] + }, + "@humanwhocodes/module-importer@1.0.1": { + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==" + }, + "@humanwhocodes/retry@0.3.1": { + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==" + }, + "@humanwhocodes/retry@0.4.2": { + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==" + }, + "@iconify/react@5.2.0_react@19.0.0": { + "integrity": "sha512-7Sdjrqq3fkkQNks9SY3adGC37NQTHsBJL2PRKlQd455PoDi9s+Es9AUTY+vGLFOYs5yO9w9yCE42pmxCwG26WA==", + "dependencies": [ + "@iconify/types", + "react" + ] + }, + "@iconify/types@2.0.0": { + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==" + }, + "@jridgewell/gen-mapping@0.3.8": { + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dependencies": [ + "@jridgewell/set-array", + "@jridgewell/sourcemap-codec", + "@jridgewell/trace-mapping" + ] + }, + "@jridgewell/resolve-uri@3.1.2": { + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" + }, + "@jridgewell/set-array@1.2.1": { + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==" + }, + "@jridgewell/sourcemap-codec@1.5.0": { + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "@jridgewell/trace-mapping@0.3.25": { + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": [ + "@jridgewell/resolve-uri", + "@jridgewell/sourcemap-codec" + ] + }, + "@nodelib/fs.scandir@2.1.5": { + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": [ + "@nodelib/fs.stat", + "run-parallel" + ] + }, + "@nodelib/fs.stat@2.0.5": { + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" + }, + "@nodelib/fs.walk@1.2.8": { + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": [ + "@nodelib/fs.scandir", + "fastq" + ] + }, + "@rollup/rollup-android-arm-eabi@4.34.8": { + "integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==" + }, + "@rollup/rollup-android-arm64@4.34.8": { + "integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==" + }, + "@rollup/rollup-darwin-arm64@4.34.8": { + "integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==" + }, + "@rollup/rollup-darwin-x64@4.34.8": { + "integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==" + }, + "@rollup/rollup-freebsd-arm64@4.34.8": { + "integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==" + }, + "@rollup/rollup-freebsd-x64@4.34.8": { + "integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==" + }, + "@rollup/rollup-linux-arm-gnueabihf@4.34.8": { + "integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==" + }, + "@rollup/rollup-linux-arm-musleabihf@4.34.8": { + "integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==" + }, + "@rollup/rollup-linux-arm64-gnu@4.34.8": { + "integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==" + }, + "@rollup/rollup-linux-arm64-musl@4.34.8": { + "integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==" + }, + "@rollup/rollup-linux-loongarch64-gnu@4.34.8": { + "integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==" + }, + "@rollup/rollup-linux-powerpc64le-gnu@4.34.8": { + "integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==" + }, + "@rollup/rollup-linux-riscv64-gnu@4.34.8": { + "integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==" + }, + "@rollup/rollup-linux-s390x-gnu@4.34.8": { + "integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==" + }, + "@rollup/rollup-linux-x64-gnu@4.34.8": { + "integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==" + }, + "@rollup/rollup-linux-x64-musl@4.34.8": { + "integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==" + }, + "@rollup/rollup-win32-arm64-msvc@4.34.8": { + "integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==" + }, + "@rollup/rollup-win32-ia32-msvc@4.34.8": { + "integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==" + }, + "@rollup/rollup-win32-x64-msvc@4.34.8": { + "integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==" + }, + "@tauri-apps/api@2.2.0": { + "integrity": "sha512-R8epOeZl1eJEl603aUMIGb4RXlhPjpgxbGVEaqY+0G5JG9vzV/clNlzTeqc+NLYXVqXcn8mb4c5b9pJIUDEyAg==" + }, + "@tauri-apps/cli-darwin-arm64@2.2.7": { + "integrity": "sha512-54kcpxZ3X1Rq+pPTzk3iIcjEVY4yv493uRx/80rLoAA95vAC0c//31Whz75UVddDjJfZvXlXZ3uSZ+bnCOnt0A==" + }, + "@tauri-apps/cli-darwin-x64@2.2.7": { + "integrity": "sha512-Vgu2XtBWemLnarB+6LqQeLanDlRj7CeFN//H8bVVdjbNzxcSxsvbLYMBP8+3boa7eBnjDrqMImRySSgL6IrwTw==" + }, + "@tauri-apps/cli-linux-arm-gnueabihf@2.2.7": { + "integrity": "sha512-+Clha2iQAiK9zoY/KKW0KLHkR0k36O78YLx5Sl98tWkwI3OBZFg5H5WT1plH/4sbZIS2aLFN6dw58/JlY9Bu/g==" + }, + "@tauri-apps/cli-linux-arm64-gnu@2.2.7": { + "integrity": "sha512-Z/Lp4SQe6BUEOays9BQAEum2pvZF4w9igyXijP+WbkOejZx4cDvarFJ5qXrqSLmBh7vxrdZcLwoLk9U//+yQrg==" + }, + "@tauri-apps/cli-linux-arm64-musl@2.2.7": { + "integrity": "sha512-+8HZ+txff/Y3YjAh80XcLXcX8kpGXVdr1P8AfjLHxHdS6QD4Md+acSxGTTNbplmHuBaSHJvuTvZf9tU1eDCTDg==" + }, + "@tauri-apps/cli-linux-x64-gnu@2.2.7": { + "integrity": "sha512-ahlSnuCnUntblp9dG7/w5ZWZOdzRFi3zl0oScgt7GF4KNAOEa7duADsxPA4/FT2hLRa0SvpqtD4IYFvCxoVv3Q==" + }, + "@tauri-apps/cli-linux-x64-musl@2.2.7": { + "integrity": "sha512-+qKAWnJRSX+pjjRbKAQgTdFY8ecdcu8UdJ69i7wn3ZcRn2nMMzOO2LOMOTQV42B7/Q64D1pIpmZj9yblTMvadA==" + }, + "@tauri-apps/cli-win32-arm64-msvc@2.2.7": { + "integrity": "sha512-aa86nRnrwT04u9D9fhf5JVssuAZlUCCc8AjqQjqODQjMd4BMA2+d4K9qBMpEG/1kVh95vZaNsLogjEaqSTTw4A==" + }, + "@tauri-apps/cli-win32-ia32-msvc@2.2.7": { + "integrity": "sha512-EiJ5/25tLSQOSGvv+t6o3ZBfOTKB5S3vb+hHQuKbfmKdRF0XQu2YPdIi1CQw1DU97ZAE0Dq4frvnyYEKWgMzVQ==" + }, + "@tauri-apps/cli-win32-x64-msvc@2.2.7": { + "integrity": "sha512-ZB8Kw90j8Ld+9tCWyD2fWCYfIrzbQohJ4DJSidNwbnehlZzP7wAz6Z3xjsvUdKtQ3ibtfoeTqVInzCCEpI+pWg==" + }, + "@tauri-apps/cli@2.2.7": { + "integrity": "sha512-ZnsS2B4BplwXP37celanNANiIy8TCYhvg5RT09n72uR/o+navFZtGpFSqljV8fy1Y4ixIPds8FrGSXJCN2BerA==", + "dependencies": [ + "@tauri-apps/cli-darwin-arm64", + "@tauri-apps/cli-darwin-x64", + "@tauri-apps/cli-linux-arm-gnueabihf", + "@tauri-apps/cli-linux-arm64-gnu", + "@tauri-apps/cli-linux-arm64-musl", + "@tauri-apps/cli-linux-x64-gnu", + "@tauri-apps/cli-linux-x64-musl", + "@tauri-apps/cli-win32-arm64-msvc", + "@tauri-apps/cli-win32-ia32-msvc", + "@tauri-apps/cli-win32-x64-msvc" + ] + }, + "@tauri-apps/plugin-dialog@2.2.0": { + "integrity": "sha512-6bLkYK68zyK31418AK5fNccCdVuRnNpbxquCl8IqgFByOgWFivbiIlvb79wpSXi0O+8k8RCSsIpOquebusRVSg==", + "dependencies": [ + "@tauri-apps/api" + ] + }, + "@tauri-apps/plugin-notification@2.2.1": { + "integrity": "sha512-QF8Zod6XDhxD6xkD5nU/BjbOpJ6+3gxGCrVULOdLpvMuMSN2Z2IdObV/qgnrEJk1UamUCF1ClQUqNCbk4zTJNQ==", + "dependencies": [ + "@tauri-apps/api" + ] + }, + "@tauri-apps/plugin-opener@2.2.5": { + "integrity": "sha512-hHsJ9RPWpZvZEPVFaL+d25gABMUMOf/A6ESXnvf/ii9guTukj58WXsAE/SOysXRIhej7kseRCxnOnIMpSCdUsQ==", + "dependencies": [ + "@tauri-apps/api" + ] + }, + "@tauri-apps/plugin-os@2.2.0": { + "integrity": "sha512-HszbCdbisMlu5QhCNAN8YIWyz2v33abAWha6+uvV2CKX8P5VSct/y+kEe22JeyqrxCnWlQ3DRx7s49Byg7/0EA==", + "dependencies": [ + "@tauri-apps/api" + ] + }, + "@types/babel__core@7.20.5": { + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dependencies": [ + "@babel/parser", + "@babel/types", + "@types/babel__generator", + "@types/babel__template", + "@types/babel__traverse" + ] + }, + "@types/babel__generator@7.6.8": { + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dependencies": [ + "@babel/types" + ] + }, + "@types/babel__template@7.4.4": { + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dependencies": [ + "@babel/parser", + "@babel/types" + ] + }, + "@types/babel__traverse@7.20.6": { + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dependencies": [ + "@babel/types" + ] + }, + "@types/cookie@0.6.0": { + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, + "@types/estree@1.0.6": { + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + }, + "@types/json-schema@7.0.15": { + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "@types/lodash-es@4.17.12": { + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "dependencies": [ + "@types/lodash" + ] + }, + "@types/lodash@4.17.15": { + "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==" + }, + "@types/react-dom@19.0.4_@types+react@19.0.10": { + "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==", + "dependencies": [ + "@types/react" + ] + }, + "@types/react@19.0.10": { + "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", + "dependencies": [ + "csstype" + ] + }, + "@types/uuid@10.0.0": { + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==" + }, + "@typescript-eslint/eslint-plugin@8.25.0_@typescript-eslint+parser@8.25.0__eslint@9.21.0__typescript@5.7.3_eslint@9.21.0_typescript@5.7.3": { + "integrity": "sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA==", + "dependencies": [ + "@eslint-community/regexpp", + "@typescript-eslint/parser", + "@typescript-eslint/scope-manager", + "@typescript-eslint/type-utils", + "@typescript-eslint/utils", + "@typescript-eslint/visitor-keys", + "eslint", + "graphemer", + "ignore", + "natural-compare", + "ts-api-utils", + "typescript" + ] + }, + "@typescript-eslint/parser@8.25.0_eslint@9.21.0_typescript@5.7.3": { + "integrity": "sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg==", + "dependencies": [ + "@typescript-eslint/scope-manager", + "@typescript-eslint/types", + "@typescript-eslint/typescript-estree", + "@typescript-eslint/visitor-keys", + "debug", + "eslint", + "typescript" + ] + }, + "@typescript-eslint/scope-manager@8.25.0": { + "integrity": "sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg==", + "dependencies": [ + "@typescript-eslint/types", + "@typescript-eslint/visitor-keys" + ] + }, + "@typescript-eslint/type-utils@8.25.0_eslint@9.21.0_typescript@5.7.3": { + "integrity": "sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g==", + "dependencies": [ + "@typescript-eslint/typescript-estree", + "@typescript-eslint/utils", + "debug", + "eslint", + "ts-api-utils", + "typescript" + ] + }, + "@typescript-eslint/types@8.25.0": { + "integrity": "sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw==" + }, + "@typescript-eslint/typescript-estree@8.25.0_typescript@5.7.3": { + "integrity": "sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q==", + "dependencies": [ + "@typescript-eslint/types", + "@typescript-eslint/visitor-keys", + "debug", + "fast-glob", + "is-glob", + "minimatch@9.0.5", + "semver@7.7.1", + "ts-api-utils", + "typescript" + ] + }, + "@typescript-eslint/utils@8.25.0_eslint@9.21.0_typescript@5.7.3": { + "integrity": "sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA==", + "dependencies": [ + "@eslint-community/eslint-utils", + "@typescript-eslint/scope-manager", + "@typescript-eslint/types", + "@typescript-eslint/typescript-estree", + "eslint", + "typescript" + ] + }, + "@typescript-eslint/visitor-keys@8.25.0": { + "integrity": "sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ==", + "dependencies": [ + "@typescript-eslint/types", + "eslint-visitor-keys@4.2.0" + ] + }, + "@vitejs/plugin-react@4.3.4_vite@6.2.0__lightningcss@1.29.1_@babel+core@7.26.9_lightningcss@1.29.1": { + "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", + "dependencies": [ + "@babel/core", + "@babel/plugin-transform-react-jsx-self", + "@babel/plugin-transform-react-jsx-source", + "@types/babel__core", + "react-refresh", + "vite" + ] + }, + "acorn-jsx@5.3.2_acorn@8.14.0": { + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dependencies": [ + "acorn" + ] + }, + "acorn@8.14.0": { + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==" + }, + "ajv@6.12.6": { + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": [ + "fast-deep-equal", + "fast-json-stable-stringify", + "json-schema-traverse", + "uri-js" + ] + }, + "ansi-styles@4.3.0": { + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": [ + "color-convert" + ] + }, + "argparse@2.0.1": { + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "balanced-match@1.0.2": { + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "brace-expansion@1.1.11": { + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": [ + "balanced-match", + "concat-map" + ] + }, + "brace-expansion@2.0.1": { + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": [ + "balanced-match" + ] + }, + "braces@3.0.3": { + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": [ + "fill-range" + ] + }, + "browserslist@4.24.4": { + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dependencies": [ + "caniuse-lite", + "electron-to-chromium", + "node-releases", + "update-browserslist-db" + ] + }, + "callsites@3.1.0": { + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, + "caniuse-lite@1.0.30001700": { + "integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==" + }, + "chalk@4.1.2": { + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": [ + "ansi-styles", + "supports-color" + ] + }, + "clsx@2.1.1": { + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" + }, + "color-convert@2.0.1": { + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": [ + "color-name" + ] + }, + "color-name@1.1.4": { + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "concat-map@0.0.1": { + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "convert-source-map@2.0.0": { + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "cookie@1.0.2": { + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==" + }, + "cross-spawn@7.0.6": { + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": [ + "path-key", + "shebang-command", + "which" + ] + }, + "csstype@3.1.3": { + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "dayjs@1.11.13": { + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, + "debug@4.4.0": { + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": [ + "ms" + ] + }, + "deep-is@0.1.4": { + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, + "detect-libc@1.0.3": { + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==" + }, + "electron-to-chromium@1.5.104": { + "integrity": "sha512-Us9M2L4cO/zMBqVkJtnj353nQhMju9slHm62NprKTmdF3HH8wYOtNvDFq/JB2+ZRoGLzdvYDiATlMHs98XBM1g==" + }, + "esbuild@0.25.0": { + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "dependencies": [ + "@esbuild/aix-ppc64", + "@esbuild/android-arm", + "@esbuild/android-arm64", + "@esbuild/android-x64", + "@esbuild/darwin-arm64", + "@esbuild/darwin-x64", + "@esbuild/freebsd-arm64", + "@esbuild/freebsd-x64", + "@esbuild/linux-arm", + "@esbuild/linux-arm64", + "@esbuild/linux-ia32", + "@esbuild/linux-loong64", + "@esbuild/linux-mips64el", + "@esbuild/linux-ppc64", + "@esbuild/linux-riscv64", + "@esbuild/linux-s390x", + "@esbuild/linux-x64", + "@esbuild/netbsd-arm64", + "@esbuild/netbsd-x64", + "@esbuild/openbsd-arm64", + "@esbuild/openbsd-x64", + "@esbuild/sunos-x64", + "@esbuild/win32-arm64", + "@esbuild/win32-ia32", + "@esbuild/win32-x64" + ] + }, + "escalade@3.2.0": { + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" + }, + "escape-string-regexp@4.0.0": { + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "eslint-scope@8.2.0": { + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "dependencies": [ + "esrecurse", + "estraverse" + ] + }, + "eslint-visitor-keys@3.4.3": { + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==" + }, + "eslint-visitor-keys@4.2.0": { + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==" + }, + "eslint@9.21.0": { + "integrity": "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==", + "dependencies": [ + "@eslint-community/eslint-utils", + "@eslint-community/regexpp", + "@eslint/config-array", + "@eslint/core", + "@eslint/eslintrc", + "@eslint/js", + "@eslint/plugin-kit", + "@humanfs/node", + "@humanwhocodes/module-importer", + "@humanwhocodes/retry@0.4.2", + "@types/estree", + "@types/json-schema", + "ajv", + "chalk", + "cross-spawn", + "debug", + "escape-string-regexp", + "eslint-scope", + "eslint-visitor-keys@4.2.0", + "espree", + "esquery", + "esutils", + "fast-deep-equal", + "file-entry-cache", + "find-up", + "glob-parent@6.0.2", + "ignore", + "imurmurhash", + "is-glob", + "json-stable-stringify-without-jsonify", + "lodash.merge", + "minimatch@3.1.2", + "natural-compare", + "optionator" + ] + }, + "espree@10.3.0_acorn@8.14.0": { + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dependencies": [ + "acorn", + "acorn-jsx", + "eslint-visitor-keys@4.2.0" + ] + }, + "esquery@1.6.0": { + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dependencies": [ + "estraverse" + ] + }, + "esrecurse@4.3.0": { + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": [ + "estraverse" + ] + }, + "estraverse@5.3.0": { + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + }, + "esutils@2.0.3": { + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "fast-deep-equal@3.1.3": { + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-glob@3.3.3": { + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dependencies": [ + "@nodelib/fs.stat", + "@nodelib/fs.walk", + "glob-parent@5.1.2", + "merge2", + "micromatch" + ] + }, + "fast-json-stable-stringify@2.1.0": { + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fast-levenshtein@2.0.6": { + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "fastq@1.19.0": { + "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", + "dependencies": [ + "reusify" + ] + }, + "file-entry-cache@8.0.0": { + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dependencies": [ + "flat-cache" + ] + }, + "fill-range@7.1.1": { + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": [ + "to-regex-range" + ] + }, + "find-up@5.0.0": { + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dependencies": [ + "locate-path", + "path-exists" + ] + }, + "flat-cache@4.0.1": { + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dependencies": [ + "flatted", + "keyv" + ] + }, + "flatted@3.3.3": { + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" + }, + "fsevents@2.3.3": { + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==" + }, + "gensync@1.0.0-beta.2": { + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" + }, + "glob-parent@5.1.2": { + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": [ + "is-glob" + ] + }, + "glob-parent@6.0.2": { + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": [ + "is-glob" + ] + }, + "globals@11.12.0": { + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, + "globals@14.0.0": { + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==" + }, + "graphemer@1.4.0": { + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" + }, + "has-flag@4.0.0": { + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "ignore@5.3.2": { + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==" + }, + "import-fresh@3.3.1": { + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dependencies": [ + "parent-module", + "resolve-from" + ] + }, + "imurmurhash@0.1.4": { + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" + }, + "is-extglob@2.1.1": { + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + }, + "is-glob@4.0.3": { + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": [ + "is-extglob" + ] + }, + "is-number@7.0.0": { + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "isexe@2.0.0": { + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "jotai@2.12.1_@types+react@19.0.10_react@19.0.0": { + "integrity": "sha512-VUW0nMPYIru5g89tdxwr9ftiVdc/nGV9jvHISN8Ucx+m1vI9dBeHemfqYzEuw5XSkmYjD/MEyApN9k6yrATsZQ==", + "dependencies": [ + "@types/react", + "react" + ] + }, + "js-tokens@4.0.0": { + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml@4.1.0": { + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": [ + "argparse" + ] + }, + "jsesc@3.1.0": { + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==" + }, + "json-buffer@3.0.1": { + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "json-schema-traverse@0.4.1": { + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stable-stringify-without-jsonify@1.0.1": { + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + }, + "json5@2.2.3": { + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + }, + "keyv@4.5.4": { + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dependencies": [ + "json-buffer" + ] + }, + "levn@0.4.1": { + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dependencies": [ + "prelude-ls", + "type-check" + ] + }, + "lightningcss-darwin-arm64@1.29.1": { + "integrity": "sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw==" + }, + "lightningcss-darwin-x64@1.29.1": { + "integrity": "sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA==" + }, + "lightningcss-freebsd-x64@1.29.1": { + "integrity": "sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ==" + }, + "lightningcss-linux-arm-gnueabihf@1.29.1": { + "integrity": "sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg==" + }, + "lightningcss-linux-arm64-gnu@1.29.1": { + "integrity": "sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ==" + }, + "lightningcss-linux-arm64-musl@1.29.1": { + "integrity": "sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw==" + }, + "lightningcss-linux-x64-gnu@1.29.1": { + "integrity": "sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw==" + }, + "lightningcss-linux-x64-musl@1.29.1": { + "integrity": "sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw==" + }, + "lightningcss-win32-arm64-msvc@1.29.1": { + "integrity": "sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog==" + }, + "lightningcss-win32-x64-msvc@1.29.1": { + "integrity": "sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q==" + }, + "lightningcss@1.29.1": { + "integrity": "sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q==", + "dependencies": [ + "detect-libc", + "lightningcss-darwin-arm64", + "lightningcss-darwin-x64", + "lightningcss-freebsd-x64", + "lightningcss-linux-arm-gnueabihf", + "lightningcss-linux-arm64-gnu", + "lightningcss-linux-arm64-musl", + "lightningcss-linux-x64-gnu", + "lightningcss-linux-x64-musl", + "lightningcss-win32-arm64-msvc", + "lightningcss-win32-x64-msvc" + ] + }, + "locate-path@6.0.0": { + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": [ + "p-locate" + ] + }, + "lodash-es@4.17.21": { + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, + "lodash.merge@4.6.2": { + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "lru-cache@5.1.1": { + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": [ + "yallist" + ] + }, + "merge2@1.4.1": { + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" + }, + "micromatch@4.0.8": { + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dependencies": [ + "braces", + "picomatch" + ] + }, + "minimatch@3.1.2": { + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": [ + "brace-expansion@1.1.11" + ] + }, + "minimatch@9.0.5": { + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": [ + "brace-expansion@2.0.1" + ] + }, + "ms@2.1.3": { + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "nanoid@3.3.8": { + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==" + }, + "natural-compare@1.4.0": { + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, + "node-releases@2.0.19": { + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" + }, + "optionator@0.9.4": { + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dependencies": [ + "deep-is", + "fast-levenshtein", + "levn", + "prelude-ls", + "type-check", + "word-wrap" + ] + }, + "p-limit@3.1.0": { + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": [ + "yocto-queue" + ] + }, + "p-locate@5.0.0": { + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dependencies": [ + "p-limit" + ] + }, + "parent-module@1.0.1": { + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": [ + "callsites" + ] + }, + "path-exists@4.0.0": { + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-key@3.1.1": { + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "picocolors@1.1.1": { + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "picomatch@2.3.1": { + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, + "postcss@8.5.3": { + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dependencies": [ + "nanoid", + "picocolors", + "source-map-js" + ] + }, + "prelude-ls@1.2.1": { + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" + }, + "punycode@2.3.1": { + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + }, + "queue-microtask@1.2.3": { + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" + }, + "react-dom@19.0.0_react@19.0.0": { + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "dependencies": [ + "react", + "scheduler" + ] + }, + "react-refresh@0.14.2": { + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==" + }, + "react-router-dom@7.2.0_react@19.0.0_react-dom@19.0.0__react@19.0.0": { + "integrity": "sha512-cU7lTxETGtQRQbafJubvZKHEn5izNABxZhBY0Jlzdv0gqQhCPQt2J8aN5ZPjS6mQOXn5NnirWNh+FpE8TTYN0Q==", + "dependencies": [ + "react", + "react-dom", + "react-router" + ] + }, + "react-router@7.2.0_react@19.0.0_react-dom@19.0.0__react@19.0.0": { + "integrity": "sha512-fXyqzPgCPZbqhrk7k3hPcCpYIlQ2ugIXDboHUzhJISFVy2DEPsmHgN588MyGmkIOv3jDgNfUE3kJi83L28s/LQ==", + "dependencies": [ + "@types/cookie", + "cookie", + "react", + "react-dom", + "set-cookie-parser", + "turbo-stream" + ] + }, + "react@19.0.0": { + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==" + }, + "resolve-from@4.0.0": { + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + }, + "reusify@1.0.4": { + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" + }, + "rollup@4.34.8": { + "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==", + "dependencies": [ + "@rollup/rollup-android-arm-eabi", + "@rollup/rollup-android-arm64", + "@rollup/rollup-darwin-arm64", + "@rollup/rollup-darwin-x64", + "@rollup/rollup-freebsd-arm64", + "@rollup/rollup-freebsd-x64", + "@rollup/rollup-linux-arm-gnueabihf", + "@rollup/rollup-linux-arm-musleabihf", + "@rollup/rollup-linux-arm64-gnu", + "@rollup/rollup-linux-arm64-musl", + "@rollup/rollup-linux-loongarch64-gnu", + "@rollup/rollup-linux-powerpc64le-gnu", + "@rollup/rollup-linux-riscv64-gnu", + "@rollup/rollup-linux-s390x-gnu", + "@rollup/rollup-linux-x64-gnu", + "@rollup/rollup-linux-x64-musl", + "@rollup/rollup-win32-arm64-msvc", + "@rollup/rollup-win32-ia32-msvc", + "@rollup/rollup-win32-x64-msvc", + "@types/estree", + "fsevents" + ] + }, + "run-parallel@1.2.0": { + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dependencies": [ + "queue-microtask" + ] + }, + "sanitize.css@13.0.0": { + "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==" + }, + "scheduler@0.25.0": { + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==" + }, + "semver@6.3.1": { + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + }, + "semver@7.7.1": { + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==" + }, + "set-cookie-parser@2.7.1": { + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + }, + "shebang-command@2.0.0": { + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": [ + "shebang-regex" + ] + }, + "shebang-regex@3.0.0": { + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "source-map-js@1.2.1": { + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + }, + "strip-json-comments@3.1.1": { + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + }, + "supports-color@7.2.0": { + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": [ + "has-flag" + ] + }, + "to-regex-range@5.0.1": { + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": [ + "is-number" + ] + }, + "ts-api-utils@2.0.1_typescript@5.7.3": { + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "dependencies": [ + "typescript" + ] + }, + "turbo-stream@2.4.0": { + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==" + }, + "type-check@0.4.0": { + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dependencies": [ + "prelude-ls" + ] + }, + "typescript-eslint@8.25.0_eslint@9.21.0_typescript@5.7.3_@typescript-eslint+parser@8.25.0__eslint@9.21.0__typescript@5.7.3": { + "integrity": "sha512-TxRdQQLH4g7JkoFlYG3caW5v1S6kEkz8rqt80iQJZUYPq1zD1Ra7HfQBJJ88ABRaMvHAXnwRvRB4V+6sQ9xN5Q==", + "dependencies": [ + "@typescript-eslint/eslint-plugin", + "@typescript-eslint/parser", + "@typescript-eslint/utils", + "eslint", + "typescript" + ] + }, + "typescript@5.7.3": { + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==" + }, + "update-browserslist-db@1.1.2_browserslist@4.24.4": { + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "dependencies": [ + "browserslist", + "escalade", + "picocolors" + ] + }, + "uri-js@4.4.1": { + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": [ + "punycode" + ] + }, + "uuid@11.1.0": { + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==" + }, + "vite@6.2.0_lightningcss@1.29.1": { + "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", + "dependencies": [ + "esbuild", + "fsevents", + "lightningcss", + "postcss", + "rollup" + ] + }, + "which@2.0.2": { + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": [ + "isexe" + ] + }, + "word-wrap@1.2.5": { + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==" + }, + "yallist@3.1.1": { + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "yocto-queue@0.1.0": { + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + } + }, + "workspace": { + "packageJson": { + "dependencies": [ + "npm:@eslint/js@^9.19.0", + "npm:@iconify/react@^5.2.0", + "npm:@tauri-apps/api@2", + "npm:@tauri-apps/cli@2", + "npm:@tauri-apps/plugin-dialog@2.2", + "npm:@tauri-apps/plugin-notification@~2.2.1", + "npm:@tauri-apps/plugin-opener@2", + "npm:@tauri-apps/plugin-os@2.2", + "npm:@types/lodash-es@^4.17.12", + "npm:@types/react-dom@19.0.4", + "npm:@types/react@19.0.10", + "npm:@types/uuid@10", + "npm:@vitejs/plugin-react@^4.3.4", + "npm:clsx@^2.1.1", + "npm:dayjs@^1.11.13", + "npm:eslint@^9.19.0", + "npm:jotai@^2.12.1", + "npm:lightningcss@^1.29.1", + "npm:lodash-es@^4.17.21", + "npm:react-dom@19.0.0", + "npm:react-router-dom@^7.2.0", + "npm:react@19.0.0", + "npm:sanitize.css@13", + "npm:typescript-eslint@^8.22.0", + "npm:typescript@~5.7.3", + "npm:uuid@^11.0.5", + "npm:vite@^6.0.3" + ] + } + } +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..67b0f50 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,25 @@ +import js from '@eslint/js'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': 'off', + }, + }, +); diff --git a/index.html b/index.html new file mode 100644 index 0000000..ff93803 --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + + Tauri + React + Typescript + + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..6ebaea0 --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "estim", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "tauri": "tauri", + "tauri:dev": "tauri dev" + }, + "dependencies": { + "@iconify/react": "^5.2.0", + "@tauri-apps/plugin-dialog": "~2.2.0", + "@tauri-apps/plugin-notification": "~2.2.1", + "@tauri-apps/plugin-os": "~2.2.0", + "clsx": "^2.1.1", + "dayjs": "^1.11.13", + "jotai": "^2.12.1", + "lodash-es": "^4.17.21", + "react": "19.0.0", + "react-dom": "19.0.0", + "react-router-dom": "^7.2.0", + "sanitize.css": "^13.0.0", + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-opener": "^2", + "uuid": "^11.0.5" + }, + "devDependencies": { + "@eslint/js": "^9.19.0", + "@types/lodash-es": "^4.17.12", + "@types/uuid": "^10.0.0", + "eslint": "^9.19.0", + "lightningcss": "^1.29.1", + "@types/react": "19.0.10", + "@types/react-dom": "19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "~5.7.3", + "typescript-eslint": "^8.22.0", + "vite": "^6.0.3", + "@tauri-apps/cli": "^2" + } +} diff --git a/public/tauri.svg b/public/tauri.svg new file mode 100644 index 0000000..31b62c9 --- /dev/null +++ b/public/tauri.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore new file mode 100644 index 0000000..b21bd68 --- /dev/null +++ b/src-tauri/.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Generated by Tauri +# will have schema files for capabilities auto-completion +/gen/schemas diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..8b55fec --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "estim" +version = "0.1.0" +description = "A Tauri App" +authors = ["you"] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +# The `_lib` suffix may seem redundant but it is necessary +# to make the lib name unique and wouldn't conflict with the bin name. +# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 +name = "estim_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +tauri-plugin-opener = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +btleplug = { version = "0.11.6", features = ["serde"] } +tokio = { version = "1.40.0", features = ["full"] } +sled = "0.34.7" +anyhow = "1.0.89" +thiserror = "2.0.11" +serde_repr = "0.1.19" +bincode = "1.3.3" +uuid = { version = "1.10.0", features = ["serde"] } +tauri-plugin-dialog = "2" +futures = "0.3.31" +tauri-plugin-devtools = "2.0.0" +tauri-plugin-os = "2" +tauri-plugin-notification = "2" + +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] +tauri-plugin-single-instance = "2" diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..0de21f6 --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,16 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capability for the main window", + "windows": [ + "main" + ], + "permissions": [ + "core:default", + "opener:default", + "core:window:allow-start-dragging", + "os:default", + "notification:default", + "dialog:default" + ] +} \ No newline at end of file diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000..6be5e50 Binary files /dev/null and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..e81bece Binary files /dev/null and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 0000000..a437dd5 Binary files /dev/null and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000..0ca4f27 Binary files /dev/null and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000..b81f820 Binary files /dev/null and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000..624c7bf Binary files /dev/null and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000..c021d2b Binary files /dev/null and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000..6219700 Binary files /dev/null and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000..f9bc048 Binary files /dev/null and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000..d5fbfb2 Binary files /dev/null and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000..63440d7 Binary files /dev/null and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000..f3f705a Binary files /dev/null and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000..4556388 Binary files /dev/null and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns new file mode 100644 index 0000000..12a5bce Binary files /dev/null and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 0000000..b3636e4 Binary files /dev/null and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 0000000..e1cd261 Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/src/bluetooth.rs b/src-tauri/src/bluetooth.rs new file mode 100644 index 0000000..3ed95b9 --- /dev/null +++ b/src-tauri/src/bluetooth.rs @@ -0,0 +1,134 @@ +use std::sync::Arc; + +use btleplug::{ + api::{Central, CentralState, Manager as _, Peripheral as _, ScanFilter}, + platform::{Adapter, Manager, Peripheral}, +}; +use futures::StreamExt; +use tauri::{ + async_runtime::{self, JoinHandle, RwLock}, + AppHandle, Emitter, State, +}; + +use crate::{errors, state::AppState}; + +pub async fn handle_bluetooth_events( + app_handle: Arc, + app_state: Arc>, +) -> Result, errors::AppError> { + let state = app_state.read().await; + let adapter = state.central_adapter.read().await; + let app = Arc::clone(&app_handle); + if let Some(adapter) = &*adapter { + let adapter = adapter.clone(); + let app_state = Arc::clone(&app_state); + Ok(async_runtime::spawn(async move { + let mut event_stream = adapter.events().await.unwrap(); + while let Some(event) = event_stream.next().await { + match event { + btleplug::api::CentralEvent::DeviceDiscovered(_id) => { + app.emit("app_state_updated", ()).unwrap(); + } + btleplug::api::CentralEvent::DeviceConnected(_id) => { + let state = app_state.write().await; + let peripherals = adapter.peripherals().await; + if let Ok(peripherals) = peripherals { + for peripheral in peripherals { + if peripheral.id() == _id { + state.set_connected_peripheral(peripheral).await; + app.emit("app_state_updated", ()).unwrap(); + break; + } + } + } + } + btleplug::api::CentralEvent::DeviceDisconnected(_id) => { + let state = app_state.write().await; + state.clear_connected_peripheral().await; + app.emit("app_state_updated", ()).unwrap(); + } + _ => {} + } + } + })) + } else { + app.emit("app_state_updated", ()).unwrap(); + Err(errors::AppError::BluetoothNotReady) + } +} + +pub async fn get_central_adapter() -> Result { + let manager = Manager::new() + .await + .map_err(|_| errors::AppError::BluetoothNotReady)?; + let adapters = manager + .adapters() + .await + .map_err(|_| errors::AppError::BluetoothAdapterNotFound)?; + + let mut found_adapter = None; + for adpater in adapters { + let state = adpater.adapter_state().await; + if let Ok(state) = state { + if state == CentralState::PoweredOn { + found_adapter = Some(adpater); + break; + } + } + } + + found_adapter.ok_or(errors::AppError::NoAvailableBluetoothAdapter) +} + +pub async fn start_scan( + app_handle: Arc, + app_state: State<'_, Arc>>, +) -> Result<(), errors::AppError> { + let state = app_state.read().await; + let adapter = state.get_central_adapter().await; + if let Some(adapter) = adapter { + adapter + .start_scan(ScanFilter::default()) + .await + .map_err(|_| errors::AppError::UnableToStartScan)?; + app_handle.emit("app_state_updated", ()).unwrap(); + state.set_scanning(true).await; + Ok(()) + } else { + Err(errors::AppError::NoAvailableBluetoothAdapter) + } +} + +pub async fn stop_scan( + app_handle: Arc, + app_state: State<'_, Arc>>, +) -> Result<(), errors::AppError> { + let state = app_state.read().await; + let adapter = state.get_central_adapter().await; + if let Some(adapter) = adapter { + adapter + .stop_scan() + .await + .map_err(|_| errors::AppError::UnableToStopScan)?; + app_handle.emit("app_state_updated", ()).unwrap(); + state.set_scanning(false).await; + Ok(()) + } else { + Err(errors::AppError::NoAvailableBluetoothAdapter) + } +} + +pub async fn get_peripherals( + app_state: State<'_, Arc>>, +) -> Result, errors::AppError> { + let state = app_state.read().await; // Change from lock() to read() + let adapter = state.get_central_adapter().await; + if let Some(adapter) = adapter { + Ok(adapter + .peripherals() + .await + .map_err(|_| errors::AppError::UnableToRetrievePeripherals)?) + } else { + Err(errors::AppError::NoAvailableBluetoothAdapter) + } +} diff --git a/src-tauri/src/cmd/mod.rs b/src-tauri/src/cmd/mod.rs new file mode 100644 index 0000000..b94341e --- /dev/null +++ b/src-tauri/src/cmd/mod.rs @@ -0,0 +1,122 @@ +use std::sync::Arc; + +use btleplug::api::{Central as _, Peripheral as _}; +use state::{ApplicationState, CentralState, ChannelState, PeripheralItem}; +use tauri::{async_runtime::RwLock, AppHandle, Emitter, State}; + +use crate::{bluetooth, errors, state::AppState}; + +mod state; + +#[tauri::command] +pub async fn activate_central_adapter( + app_handle: AppHandle, + app_state: State<'_, Arc>>, +) -> Result<(), errors::AppError> { + let app = Arc::new(app_handle); + let adapter = bluetooth::get_central_adapter().await?; + { + let state = app_state.read().await; + state.set_central_adapter(adapter).await; + state.clear_central_event_handler().await; + } + let handle = + bluetooth::handle_bluetooth_events(Arc::clone(&app), Arc::clone(&app_state)).await?; + { + let state = app_state.write().await; // Changed from lock() to write() + state.set_central_event_handler(handle).await; + } + app.emit("app_state_updated", ()).unwrap(); + Ok(()) +} + +#[tauri::command] +pub async fn refresh_application_state( + app_state: State<'_, Arc>>, +) -> Result { + let state = app_state.read().await; + let mut peripherals: Vec = vec![]; + let mut connected_peripheral = None; + + let central = state.get_central_adapter().await; + let central_state = if let Some(central) = central { + let central_device_state = central + .adapter_state() + .await + .map_err(|_| errors::AppError::BluetoothNotReady)?; + + let found_peripherals = central + .peripherals() + .await + .map_err(|_| errors::AppError::UnableToRetrievePeripherals)?; + for peripheral in found_peripherals { + let properties = peripheral + .properties() + .await + .map_err(|_| errors::AppError::UnableToRetrievePeripheralProperties)?; + if let Some(properties) = properties { + let represent = properties + .local_name + .unwrap_or_else(|| properties.address.to_string()); + let item = PeripheralItem { + id: peripheral.id(), + address: properties.address.to_string(), + represent, + is_connected: peripheral + .is_connected() + .await + .map_err(|_| errors::AppError::UnableToRetrievePeripheralProperties)?, + rssi: properties.rssi, + battery: properties.tx_power_level, + }; + if peripheral + .is_connected() + .await + .map_err(|_| errors::AppError::UnableToRetrievePeripheralState)? + { + connected_peripheral = Some(item.clone()); + } + peripherals.push(item); + } + } + + CentralState { + is_ready: central_device_state == btleplug::api::CentralState::PoweredOn, + is_scanning: *state.scanning.lock().await, + connected: state + .connected_peripheral + .lock() + .await + .clone() + .map(|p| p.id()), + } + } else { + CentralState::default() + }; + + Ok(ApplicationState { + central: central_state, + peripherals, + connected_peripheral, + channel_a: ChannelState::default(), + channel_b: ChannelState::default(), + }) +} + +#[tauri::command] +pub async fn start_scan_devices( + app_handle: AppHandle, + app_state: State<'_, Arc>>, +) -> Result<(), errors::AppError> { + bluetooth::start_scan(Arc::new(app_handle), app_state).await?; + Ok(()) +} + +#[tauri::command] +pub async fn stop_scan_devices( + app_handle: AppHandle, + app_state: State<'_, Arc>>, +) -> Result<(), errors::AppError> { + bluetooth::stop_scan(Arc::new(app_handle), app_state).await?; + Ok(()) +} diff --git a/src-tauri/src/cmd/state.rs b/src-tauri/src/cmd/state.rs new file mode 100644 index 0000000..1dde792 --- /dev/null +++ b/src-tauri/src/cmd/state.rs @@ -0,0 +1,41 @@ +use btleplug::platform::PeripheralId; +use serde::Serialize; + +use crate::playlist::PlayMode; + +#[derive(Debug, Clone, Default, Serialize)] +pub struct CentralState { + pub is_ready: bool, + pub is_scanning: bool, + pub connected: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct PeripheralItem { + pub id: PeripheralId, + pub address: String, + pub represent: String, + pub is_connected: bool, + pub rssi: Option, + pub battery: Option, +} + +#[derive(Debug, Clone, Default, Serialize)] +pub struct ChannelState { + pub is_playing: bool, + pub strength: u32, + pub strength_limit: u32, + pub is_boosting: bool, + pub boost_level: u32, + pub boost_limit: u32, + pub play_mode: PlayMode, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ApplicationState { + pub central: CentralState, + pub peripherals: Vec, + pub connected_peripheral: Option, + pub channel_a: ChannelState, + pub channel_b: ChannelState, +} diff --git a/src-tauri/src/config_db.rs b/src-tauri/src/config_db.rs new file mode 100644 index 0000000..aed7af6 --- /dev/null +++ b/src-tauri/src/config_db.rs @@ -0,0 +1,23 @@ +use std::{fs::DirBuilder, path::PathBuf}; + +use tauri::{path::PathResolver, AppHandle, Manager, Runtime}; + +fn ensure_dir(resolver: &PathResolver) -> anyhow::Result { + let config_dir = resolver + .local_data_dir() + .map_err(|e| anyhow::anyhow!("Unable to get local data directory: {}", e))?; + if !config_dir.exists() { + let mut dir_creator = DirBuilder::new(); + dir_creator + .recursive(true) + .create(&config_dir) + .map_err(|e| anyhow::anyhow!("Unable to create config directory: {}", e))?; + } + Ok(config_dir) +} + +pub fn open_config_db(app_handle: &AppHandle) -> anyhow::Result { + let config_dir = ensure_dir(app_handle.path())?; + let db_path = config_dir.join("conf.db"); + sled::open(&db_path).map_err(|e| anyhow::anyhow!("Unable to open config database: {}", e)) +} diff --git a/src-tauri/src/errors.rs b/src-tauri/src/errors.rs new file mode 100644 index 0000000..859d41b --- /dev/null +++ b/src-tauri/src/errors.rs @@ -0,0 +1,22 @@ +use serde::Serialize; +use thiserror::Error; + +#[derive(Debug, Error, Serialize)] +pub enum AppError { + #[error("Bluetooth not ready")] + BluetoothNotReady, + #[error("No Bluetooth adapter found")] + BluetoothAdapterNotFound, + #[error("No available Bluetooth adapter, maybe not powered on")] + NoAvailableBluetoothAdapter, + #[error("Unable to start scan devices")] + UnableToStartScan, + #[error("Unable to stop scan devices")] + UnableToStopScan, + #[error("Unable to retrieve peripherals")] + UnableToRetrievePeripherals, + #[error("Unable to retrieve peripheral properties")] + UnableToRetrievePeripheralProperties, + #[error("Unable to retrieve peripheral state")] + UnableToRetrievePeripheralState, +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..0229bed --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,63 @@ +#![allow(dead_code)] +#![feature(stmt_expr_attributes)] + +use std::sync::Arc; + +use tauri::{ + async_runtime::{self, RwLock}, + generate_handler, Manager, +}; +use tauri_plugin_dialog::DialogExt; + +mod bluetooth; +mod cmd; +mod config_db; +mod errors; +mod playlist; +mod state; + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + #[cfg(debug_assertions)] + let devtools = tauri_plugin_devtools::init(); + + let mut builder = tauri::Builder::default(); + + #[cfg(debug_assertions)] + builder = builder.plugin(devtools); + + builder + .plugin(tauri_plugin_os::init()) + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_single_instance::init(|_app, _args, _cwd| {})) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_notification::init()) + .setup(|app| { + if let Err(e) = async_runtime::block_on(async { + let state = state::AppState::new(app.handle()).await?; + app.manage(Arc::new(RwLock::new(state))); + Ok::<(), anyhow::Error>(()) + }) { + app.dialog() + .message(e.to_string()) + .kind(tauri_plugin_dialog::MessageDialogKind::Error) + .title("Initialization error") + .blocking_show(); + return Err(e.into()); + } + #[cfg(debug_assertions)] + { + let window = app.get_webview_window("main").unwrap(); + window.open_devtools(); + } + Ok(()) + }) + .invoke_handler(generate_handler![ + cmd::refresh_application_state, + cmd::activate_central_adapter, + cmd::start_scan_devices, + cmd::stop_scan_devices + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..1aa4c16 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + estim_lib::run() +} diff --git a/src-tauri/src/playlist.rs b/src-tauri/src/playlist.rs new file mode 100644 index 0000000..9f44f08 --- /dev/null +++ b/src-tauri/src/playlist.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum PlayMode { + #[serde(rename = "repeat")] + Repeat, + #[serde(rename = "repeat-one")] + RepeatOne, + #[serde(rename = "shuffle")] + Shuffle, +} + +impl Default for PlayMode { + fn default() -> Self { + PlayMode::Repeat + } +} diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs new file mode 100644 index 0000000..5208d82 --- /dev/null +++ b/src-tauri/src/state.rs @@ -0,0 +1,86 @@ +use std::sync::Arc; + +use btleplug::platform::{Adapter, Peripheral}; +use tauri::{ + async_runtime::{JoinHandle, Mutex, RwLock}, + AppHandle, Runtime, +}; + +use crate::config_db; + +pub struct AppState { + pub db: Arc>, + pub central_adapter: Arc>>, + pub central_event_handler: Arc>>>, + pub scanning: Arc>, + pub connected_peripheral: Arc>>>, +} + +unsafe impl Send for AppState {} +unsafe impl Sync for AppState {} + +impl AppState { + pub async fn new(app: &AppHandle) -> anyhow::Result { + let db = config_db::open_config_db(app)?; + + Ok(Self { + db: Arc::new(Mutex::new(db)), + central_adapter: Arc::new(RwLock::new(None)), + central_event_handler: Arc::new(Mutex::new(None)), + scanning: Arc::new(Mutex::new(false)), + connected_peripheral: Arc::new(Mutex::new(None)), + }) + } + + pub async fn set_central_adapter(&self, adapter: Adapter) { + let mut central_adapter = self.central_adapter.write().await; + *central_adapter = Some(adapter); + } + + pub async fn clear_central_adapter(&self) { + let mut central_adapter = self.central_adapter.write().await; + *central_adapter = None; + } + + pub async fn get_central_adapter(&self) -> Option { + let central_adapter = self.central_adapter.read().await; + central_adapter.clone() + } + + pub async fn set_central_event_handler(&self, handler: JoinHandle<()>) { + let mut central_event_handler = self.central_event_handler.lock().await; + *central_event_handler = Some(handler); + } + + pub async fn clear_central_event_handler(&self) { + let mut central_event_handler = self.central_event_handler.lock().await; + if let Some(handler) = central_event_handler.take() { + handler.abort(); + } + } + + pub async fn set_scanning(&self, scanning_state: bool) { + let mut scanning = self.scanning.lock().await; + *scanning = scanning_state; + } + + pub async fn set_connected_peripheral(&self, peripheral: Peripheral) { + let mut connected_peripheral = self.connected_peripheral.lock().await; + *connected_peripheral = Some(Arc::new(peripheral)); + } + + pub async fn clear_connected_peripheral(&self) { + let mut connected_peripheral = self.connected_peripheral.lock().await; + *connected_peripheral = None; + } + + pub async fn get_connected_peripheral(&self) -> Option> { + let connected_peripheral = self.connected_peripheral.lock().await; + + if let Some(peripheral) = connected_peripheral.clone() { + Some(Arc::clone(&peripheral)) + } else { + None + } + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..07c3f10 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "estim", + "version": "0.1.0", + "identifier": "net.archgrid.app.estim", + "build": { + "beforeDevCommand": "deno task dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "deno task build", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "title": "estim", + "width": 1200, + "height": 800, + "resizable": false, + "hiddenTitle": true, + "titleBarStyle": "Overlay", + "theme": "Dark" + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} \ No newline at end of file diff --git a/src/Layout.module.css b/src/Layout.module.css new file mode 100644 index 0000000..18c1ef1 --- /dev/null +++ b/src/Layout.module.css @@ -0,0 +1,81 @@ +@layer pages { + .layout { + width: 100%; + height: 100%; + display: flex; + padding-block-start: calc(var(--spacing)); + padding-block-end: calc(var(--spacing) * 4); + padding-inline: calc(var(--spacing) * 4); + margin: 0; + flex-direction: column; + align-items: stretch; + gap: calc(var(--spacing) * 3); + header { + display: flex; + flex-direction: row; + align-items: flex-start; + height: calc(var(--spacing) * 6); + z-index: 10; + &.mac_titlebar { + padding-left: calc(var(--spacing) * 14); + } + h1 { + line-height: 1em; + font-style: italic; + } + } + section { + flex: 1; + display: flex; + flex-direction: row; + align-items: stretch; + gap: calc(var(--spacing) * 3); + menu { + flex: 1; + padding: calc(var(--spacing) * 4) 0; + max-width: calc(var(--spacing) * 12); + display: flex; + flex-direction: column; + align-items: stretch; + gap: calc(var(--spacing) * 4); + } + .main_content { + flex: 1; + border-radius: calc(var(--border-radius) * 2); + } + } + } + .window_move_handler { + position: absolute; + top: 0; + left: 0; + right: 0; + height: calc(var(--spacing) * 8); + z-index: 300; + } + .route_link { + display: flex; + flex-direction: column; + align-items: stretch; + gap: calc(var(--spacing)); + div { + text-align: center; + &.filled { + color: var(--color-dark-on-surface); + background-color: transparent; + border-radius: calc(var(--border-radius) * 2); + padding-block: calc(var(--spacing)); + } + } + &.inactive { + color: var(--color-dark-on-surface-variant); + } + &.active { + color: var(--color-dark-on-surface); + div.filled { + color: var(--color-dark-on-secondary-container); + background-color: var(--color-dark-secondary-container); + } + } + } +} diff --git a/src/Layout.tsx b/src/Layout.tsx new file mode 100644 index 0000000..88d8980 --- /dev/null +++ b/src/Layout.tsx @@ -0,0 +1,70 @@ +import { Icon } from '@iconify/react/dist/iconify.js'; +import { platform } from '@tauri-apps/plugin-os'; +import cx from 'clsx'; +import { FC, ReactNode } from 'react'; +import { createPortal } from 'react-dom'; +import { NavLink, Outlet } from 'react-router-dom'; +import styles from './Layout.module.css'; +import { defaultIconProps } from './icons/shared-props'; +import StateBar from './page-components/state-bar/StateBar'; + +type FunctionLinkProps = { + name: string; + url: string; + end?: boolean; + children?: ReactNode; +}; + +const FunctionLink: FC = ({ name, url, end, children }) => { + return ( + + cx(styles.route_link, isActive ? styles.active : styles.inactive) + } + end={end}> +
{children}
+
{name}
+
+ ); +}; +const Layout: FC = () => { + const os = platform(); + + return ( +
+
+

ESTIM Remote

+ +
+
+ + + + + + + + + + + + + + + + + +
+ +
+
+ {createPortal( +
, + document.body, + )} +
+ ); +}; + +export default Layout; diff --git a/src/components.css b/src/components.css new file mode 100644 index 0000000..2d25227 --- /dev/null +++ b/src/components.css @@ -0,0 +1,203 @@ +@layer base { + :root { + --button-text: var(--color-dark-on-primary); + --button-surface: var(--color-dark-primary); + --button-outline: var(--color-dark-outline); + } + + :where(ul, menu) { + margin: 0; + padding: 0; + } + + :where(a) { + text-decoration: none; + color: var(--color-dark-on-surface); + + &:hover { + color: var(--color-dark-primary); + } + + &:active { + color: var(--color-dark-tertiary); + } + } + + :is(h1, h2, h3, h4, h5, h6) { + font-weight: bold; + line-height: 1.2em; + } + + h1 { + font-size: 2.6em; + } + + h2 { + font-size: 2em; + } + + h3 { + font-size: 1.8em; + } + + h4 { + font-size: 1.5em; + } + + h5 { + font-size: 1.2em; + } + + h6 { + font-size: 1em; + } + + .center { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + } + + .spacer { + flex: 1 1; + } + + .workspace { + height: 100%; + width: 100%; + overflow: hidden; + display: flex; + align-items: stretch; + gap: calc(var(--spacing) * 6); + padding: calc(var(--spacing) * 2) calc(var(--spacing) * 2); + &.veritcal { + flex-direction: column; + } + &.horizontal { + flex-direction: row; + } + } + + :where(button, .button) { + border: none; + border-radius: calc(var(--border-radius) * 2); + padding: calc(var(--spacing) * 1) calc(var(--spacing) * 3); + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: calc(var(--spacing) * 2); + font-size: calc(var(--font-size) * 1.2); + line-height: 1.3em; + color: var(--button-text); + background-color: var(--button-surface); + box-shadow: var(--elevation-dark-0); + &.smaller { + font-size: calc(var(--font-size) * 0.8); + } + &.small { + font-size: calc(var(--font-size) * 1); + } + &.large { + font-size: calc(var(--font-size) * 1.4); + } + &.larger { + font-size: calc(var(--font-size) * 1.6); + } + &:hover:not(:disabled) { + --button-text: var(--color-dark-on-primary); + box-shadow: var(--elevation-dark-1-ambient), var(--elevation-dark-1-umbra); + } + &:active:not(:disabled) { + color: color-mix(in oklch, var(--button-text) 12%, transparent); + } + &.tonal:not(:disabled) { + --button-text: var(--color-dark-on-primary-container); + --button-surface: var(--color-dark-primary-container); + } + &.danger:not(:disabled) { + --button-text: var(--color-dark-on-error); + --button-surface: var(--color-dark-error); + } + &.warn:not(:disabled) { + --button-text: var(--color-dark-on-warning); + --button-surface: var(--color-dark-warning); + } + &.success:not(:disabled) { + --button-text: var(--color-dark-on-success); + --button-surface: var(--color-dark-success); + } + &.info:not(:disabled) { + --button-text: var(--color-dark-on-info); + --button-surface: var(--color-dark-info); + } + &:disabled { + --button-text: color-mix(in oklch, var(--color-dark-on-surface) 38%, transparent); + --button-surface: color-mix(in oklch, var(--color-dark-on-surface) 12%, transparent); + --button-outline: color-mix(in oklch, var(--color-dark-on-surface) 12%, transparent); + cursor: not-allowed; + } + &.outline { + --button-text: var(--color-dark-primary); + --button-surface: transparent; + border: 1px solid var(--button-outline); + &:hover:not(:disabled) { + --button-text: var(--color-dark-primary); + --button-surface: color-mix(in oklch, var(--button-text) 8%, transparent); + box-shadow: var(--elevation-dark-0); + } + &:active:not(:disabled) { + --button-text: var(--color-dark-primary); + --button-surface: color-mix(in oklch, var(--button-text) 10%, transparent); + box-shadow: var(--elevation-dark-0); + } + &.danger:not(:disabled) { + --button-text: var(--color-dark-error); + } + &.warn:not(:disabled) { + --button-text: var(--color-dark-warning); + } + &.success:not(:disabled) { + --button-text: var(--color-dark-success); + } + &.info:not(:disabled) { + --button-text: var(--color-dark-info); + } + &:disabled { + --button-text: color-mix(in oklch, var(--color-dark-on-surface) 38%, transparent); + } + } + &.text { + --button-text: --color-dark-primary; + --button-surface: transparent; + border: none; + &:hover:not(:disabled) { + --button-surface: color-mix(in oklch, var(--color-dark-primary) 8%, transparent); + } + &:active:not(:disabled) { + --button-surface: color-mix(in oklch, var(--color-dark-primary) 10%, transparent); + } + &.danger:not(:disabled) { + --button-text: var(--color-dark-error); + --button-surface: transparent; + } + &.warn:not(:disabled) { + --button-text: var(--color-dark-warning); + --button-surface: transparent; + } + &.success:not(:disabled) { + --button-text: var(--color-dark-success); + --button-surface: transparent; + } + &.info:not(:disabled) { + --button-text: var(--color-dark-info); + --button-surface: transparent; + } + &:disabled { + --button-text: color-mix(in oklch, var(--color-dark-on-surface) 38%, transparent); + } + } + } +} diff --git a/src/components/ScrollArea.module.css b/src/components/ScrollArea.module.css new file mode 100644 index 0000000..700b93f --- /dev/null +++ b/src/components/ScrollArea.module.css @@ -0,0 +1,43 @@ +@layer components { + .scroll_area { + display: grid; + width: 100%; + height: 100%; + overflow: hidden; + grid-template-columns: auto calc(var(--spacing) * 3); + grid-template-rows: auto calc(var(--spacing) * 3); + } + .content { + grid-column: 1; + grid-row: 1; + overflow: hidden; + } + .v_scrollbar { + grid-column: 2; + grid-row: 1; + overflow: hidden; + position: relative; + .v_thumb { + width: 100%; + aspect-ratio: 1 / 3; + position: absolute; + border-radius: calc(var(--border-radius) * 2); + background-color: oklch(from var(--color-dark-primary) l c h / 70%); + cursor: pointer; + } + } + .h_scrollbar { + grid-column: 1; + grid-row: 2; + overflow: hidden; + position: relative; + .h_thumb { + height: 100%; + aspect-ratio: 3 / 1; + position: absolute; + border-radius: calc(var(--border-radius) * 2); + background-color: oklch(from var(--color-dark-primary) l c h / 70%); + cursor: pointer; + } + } +} diff --git a/src/components/ScrollArea.tsx b/src/components/ScrollArea.tsx new file mode 100644 index 0000000..4357298 --- /dev/null +++ b/src/components/ScrollArea.tsx @@ -0,0 +1,195 @@ +import { clamp } from 'lodash-es'; +import { MouseEvent, RefObject, useEffect, useRef, useState, WheelEvent } from 'react'; +import styles from './ScrollArea.module.css'; + +type ScrollBarProps = { + containerRef: RefObject | null; +}; + +function VerticalScrollBar({ containerRef }: ScrollBarProps) { + const [thumbPos, setThumbPos] = useState(0); + const trackRef = useRef(null); + const thumbRef = useRef(null); + const handleMouseDown = (evt: MouseEvent) => { + evt.preventDefault(); + //@ts-expect-error TS2769 + document.addEventListener('mousemove', handleMouseMove); + //@ts-expect-error TS2769 + document.addEventListener('mouseup', handleMouseUp); + }; + const handleMouseMove = (evt: MouseEvent) => { + evt.preventDefault(); + const container = containerRef?.current; + const scrollbar = trackRef.current; + const thumb = thumbRef.current; + + if (container && scrollbar && thumb) { + const trackRect = scrollbar.getBoundingClientRect(); + const thumbRect = thumb.getBoundingClientRect(); + const offsetY = evt.clientY - trackRect.top - thumbRect.height / 2; + const thumbPosition = clamp(offsetY, 0, trackRect.height - thumbRect.height); + setThumbPos(thumbPosition); + + const scrollPercentage = thumbPosition / (trackRect.height - thumbRect.height); + container.scrollTop = scrollPercentage * (container.scrollHeight - container.clientHeight); + } + }; + const handleMouseUp = (evt: MouseEvent) => { + evt.preventDefault(); + //@ts-expect-error TS2769 + document.removeEventListener('mousemove', handleMouseMove); + //@ts-expect-error TS2769 + document.removeEventListener('mouseup', handleMouseUp); + }; + + const updateThumbPosition = () => { + const container = containerRef?.current; + const scrollbar = trackRef.current; + const scrollPercentage = + container.scrollTop / (container.scrollHeight - container.clientHeight); + const thumbPosition = + scrollPercentage * (scrollbar.clientHeight - thumbRef.current!.clientHeight); + setThumbPos(thumbPosition); + }; + + useEffect(() => { + const container = containerRef?.current; + const scrollbar = trackRef.current; + if (container && scrollbar) { + container.addEventListener('scroll', updateThumbPosition); + return () => { + container.removeEventListener('scroll', updateThumbPosition); + }; + } + }, []); + + return ( +
+
+
+ ); +} + +function HorizontalScrollBar({ containerRef }: ScrollBarProps) { + const [thumbPos, setThumbPos] = useState(0); + const trackRef = useRef(null); + const thumbRef = useRef(null); + const handleMouseDown = (evt: MouseEvent) => { + evt.preventDefault(); + //@ts-expect-error TS2769 + document.addEventListener('mousemove', handleMouseMove); + //@ts-expect-error TS2769 + document.addEventListener('mouseup', handleMouseUp); + }; + const handleMouseMove = (evt: MouseEvent) => { + evt.preventDefault(); + const container = containerRef?.current; + const scrollbar = trackRef.current; + const thumb = thumbRef.current; + + if (container && scrollbar && thumb) { + const trackRect = scrollbar.getBoundingClientRect(); + const thumbRect = thumb.getBoundingClientRect(); + const offsetX = evt.clientX - trackRect.left - thumbRect.width / 2; + const thumbPosition = clamp(offsetX, 0, trackRect.width - thumbRect.width); + setThumbPos(thumbPosition); + + const scrollPercentage = thumbPosition / (trackRect.width - thumbRect.width); + container.scrollLeft = scrollPercentage * (container.scrollWidth - container.clientWidth); + } + }; + const handleMouseUp = (evt: MouseEvent) => { + evt.preventDefault(); + //@ts-expect-error TS2769 + document.removeEventListener('mousemove', handleMouseMove); + //@ts-expect-error TS2769 + document.removeEventListener('mouseup', handleMouseUp); + }; + + const updateThumbPosition = () => { + const container = containerRef?.current; + const scrollbar = trackRef.current; + const scrollPercentage = container.scrollLeft / (container.scrollWidth - container.clientWidth); + const thumbPosition = + scrollPercentage * (scrollbar.clientWidth - thumbRef.current!.clientWidth); + setThumbPos(thumbPosition); + }; + + useEffect(() => { + const container = containerRef?.current; + const scrollbar = trackRef.current; + if (container && scrollbar) { + container.addEventListener('scroll', updateThumbPosition); + return () => { + container.removeEventListener('scroll', updateThumbPosition); + }; + } + }, []); + + return ( +
+
handleMouseDown(e)} + /> +
+ ); +} + +type ScrollAreaProps = { + children?: React.ReactNode; + enableX?: boolean; + enableY?: boolean; + normalizedScroll?: boolean; +}; + +export function ScrollArea({ + children, + enableX = false, + enableY = false, + normalizedScroll = false, +}: ScrollAreaProps) { + const scrollContainerRef = useRef(null); + const [xScrollNeeded, setXScrollNeeded] = useState(false); + const [yScrollNeeded, setYScrollNeeded] = useState(false); + const handleWheel = (evt: WheelEvent) => { + const container = scrollContainerRef?.current; + if (enableY && container) { + const delta = evt.deltaY; + const normalizedDelta = normalizedScroll ? clamp(delta, -1, 1) * 30 : delta; + const newScrollTop = container.scrollTop + normalizedDelta; + container.scrollTop = clamp(newScrollTop, 0, container.scrollHeight - container.clientHeight); + } + if (enableX && container) { + const delta = evt.deltaX; + const normalizedDelta = normalizedScroll ? clamp(delta, -1, 1) * 30 : delta; + const newScrollLeft = container.scrollLeft + normalizedDelta; + container.scrollLeft = clamp(newScrollLeft, 0, container.scrollWidth - container.clientWidth); + } + }; + + useEffect(() => { + const container = scrollContainerRef.current; + if (container) { + setXScrollNeeded(container.scrollWidth > container.clientWidth); + setYScrollNeeded(container.scrollHeight > container.clientHeight); + } + }, [children]); + + return ( +
+
handleWheel(e)}> + {children} +
+ {enableY && yScrollNeeded && } + {enableX && xScrollNeeded && } +
+ ); +} diff --git a/src/context/EstimContext.tsx b/src/context/EstimContext.tsx new file mode 100644 index 0000000..ef84a4d --- /dev/null +++ b/src/context/EstimContext.tsx @@ -0,0 +1,96 @@ +import { invoke } from '@tauri-apps/api/core'; +import { Event, listen, UnlistenFn } from '@tauri-apps/api/event'; +import { message } from '@tauri-apps/plugin-dialog'; +import { atom, PrimitiveAtom, useSetAtom } from 'jotai'; +import { atomFamily } from 'jotai/utils'; +import { FC, ReactNode, useCallback, useEffect, useRef } from 'react'; + +export type Channels = 'a' | 'b'; +type ChannelState = { + playing: boolean; + playMode: 'shuffle' | 'repeat' | 'repeat-one'; + strength: number; + maxStrength: number; + boosting: boolean; + boostLevel: number; + maxBoostLevel: number; +}; +type DeviceState = { + rssi: number | null; + battery: number | null; +}; +type BluetoothState = { + ready: boolean | null; + searching: boolean | null; + connected: string | null; +}; + +export const BleState = atom({ + ready: null, + searching: null, + connected: null, +}); +export const DeviceState = atom({ + rssi: null, + battery: null, +}); +const Channels: Record> = { + a: atom({ + playing: false, + playMode: 'repeat-one', + strength: 0, + maxStrength: 100, + boosting: false, + boostLevel: 0, + maxBoostLevel: 100, + }), + b: atom({ + playing: false, + playMode: 'repeat-one', + strength: 0, + maxStrength: 100, + boosting: false, + boostLevel: 0, + maxBoostLevel: 100, + }), +}; +export const ChannelState = atomFamily((channel: Channels) => Channels[channel]); + +const EstimWatchProvider: FC<{ children?: ReactNode }> = ({ children }) => { + const unlisten = useRef(null); + const setBleState = useSetAtom(BleState); + const handleAppStateRefresh = useCallback(async (event: Event) => { + try { + const newState = await invoke('refresh_application_state'); + setBleState({ + ready: newState.central.is_ready, + searching: newState.central.is_scanning, + connected: newState.central.connected, + }); + } catch (e) { + console.error('[Answer refresh state]', e); + } + }, []); + + useEffect(() => { + (async function () { + try { + unlisten.current = await listen('app_state_updated', handleAppStateRefresh); + await invoke('activate_central_adapter'); + } catch (e) { + console.error('[Activate Adapter]', e); + await message('Fail to activate Bluetooth adapter.', { + title: 'Bluetooth Error', + kind: 'error', + }); + } + })(); + + return () => { + unlisten.current?.(); + }; + }, []); + return <>{children}; +}; + +export default EstimWatchProvider; diff --git a/src/icons/IconBattery.tsx b/src/icons/IconBattery.tsx new file mode 100644 index 0000000..3b9e5ec --- /dev/null +++ b/src/icons/IconBattery.tsx @@ -0,0 +1,42 @@ +import { Icon } from '@iconify/react/dist/iconify.js'; +import { FC, useMemo } from 'react'; +import { SharedIconProps } from './shared-props'; + +type IconBatteryProps = SharedIconProps & { + level?: number | null; +}; + +const IconBattery: FC = ({ + height = 24, + aspect = 1, + stroke = 0.5, + level = null, +}) => { + const width = useMemo(() => height * aspect, [height, aspect]); + const batteryIcon = useMemo(() => { + if (level !== null && level >= 90) { + return 'material-symbols-light:battery-full'; + } else if (level !== null && level >= 80) { + return 'material-symbols-light:battery-6-bar'; + } else if (level !== null && level >= 70) { + return 'material-symbols-light:battery-5-bar'; + } else if (level !== null && level >= 50) { + return 'material-symbols-light:battery-4-bar'; + } else if (level !== null && level >= 30) { + return 'material-symbols-light:battery-3-bar'; + } else if (level !== null && level >= 20) { + return 'material-symbols-light:battery-2-bar'; + } else if (level !== null && level >= 10) { + return 'material-symbols-light:battery-1-bar'; + } else if (level !== null && level >= 0) { + return 'material-symbols-light:battery-0-bar'; + } else { + return 'material-symbols-light:battery-error'; + } + }, [level]); + console.debug('[icon battery]', level, batteryIcon); + + return ; +}; + +export default IconBattery; diff --git a/src/icons/IconBluetooth.tsx b/src/icons/IconBluetooth.tsx new file mode 100644 index 0000000..013a57e --- /dev/null +++ b/src/icons/IconBluetooth.tsx @@ -0,0 +1,35 @@ +import { Icon } from '@iconify/react/dist/iconify.js'; +import { FC, useMemo } from 'react'; +import { SharedIconProps } from './shared-props'; + +type IconBluetoothProps = SharedIconProps & { + ready?: boolean | null; + searching?: boolean | null; + connected?: boolean | null; +}; + +const IconBluetooth: FC = ({ + height = 24, + aspect = 1, + stroke = 0.5, + ready = false, + searching = false, + connected = false, +}) => { + const width = useMemo(() => height * aspect, [height, aspect]); + const bleIcon = useMemo(() => { + if (ready && !searching && !connected) { + return 'material-symbols-light:bluetooth'; + } else if (ready && searching) { + return 'material-symbols-light:bluetooth-searching'; + } else if (ready && !searching && connected) { + return 'material-symbols-light:bluetooth-connected'; + } else { + return 'material-symbols-light:bluetooth-disabled'; + } + }, [ready, searching, connected]); + + return ; +}; + +export default IconBluetooth; diff --git a/src/icons/IconRssi.tsx b/src/icons/IconRssi.tsx new file mode 100644 index 0000000..a6a70ee --- /dev/null +++ b/src/icons/IconRssi.tsx @@ -0,0 +1,30 @@ +import { Icon } from '@iconify/react/dist/iconify.js'; +import { FC, useMemo } from 'react'; +import { SharedIconProps } from './shared-props'; + +type IconRssiProps = SharedIconProps & { + level?: number | null; +}; + +const IconRssi: FC = ({ height = 24, aspect = 1, stroke = 0.5, level = null }) => { + const width = useMemo(() => height * aspect, [height, aspect]); + const rssiIcon = useMemo(() => { + if (level === null) { + return 'material-symbols-light:signal-cellular-nodata'; + } else if (level <= -80) { + return 'material-symbols-light:signal-cellular-4-bar'; + } else if (level <= -70) { + return 'material-symbols-light:signal-cellular-3-bar'; + } else if (level <= -60) { + return 'material-symbols-light:signal-cellular-2-bar'; + } else if (level <= -50) { + return 'material-symbols-light:signal-cellular-1-bar'; + } else { + return 'material-symbols-light:signal-cellular-null'; + } + }, [level]); + + return ; +}; + +export default IconRssi; diff --git a/src/icons/shared-props.ts b/src/icons/shared-props.ts new file mode 100644 index 0000000..ef266fc --- /dev/null +++ b/src/icons/shared-props.ts @@ -0,0 +1,17 @@ +import { IconProps } from '@iconify/react/dist/iconify.js'; + +export type SharedIconProps = { + height?: number | null; + aspect?: number | null; + stroke?: number | null; +}; + +export const defaultIconProps: Partial = { + height: 24, + stroke: 0.5, +}; + +export const smallIconProps: Partial = { + height: 16, + stroke: 0.5, +}; diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..e0e1b0b --- /dev/null +++ b/src/index.css @@ -0,0 +1,5 @@ +@layer theme, base, components, utilities, pages; +@import 'sanitize.css' layer(base); +@import 'sanitize.css/forms.css' layer(base); +@import './theme.css'; +@import './components.css'; diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..c002ffd --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,31 @@ +// Load global styles +import './index.css'; +// Load foundations +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import Layout from './Layout'; +import EstimWatchProvider from './context/EstimContext'; +import Device from './pages/Device'; +import PatternEditor from './pages/PatternEditor'; +import PatternLibrary from './pages/PatternLibrary'; +import PlayControl from './pages/Play'; +import Settings from './pages/Settings'; + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + + + }> + } /> + } /> + } /> + } /> + } /> + + + + + , +); diff --git a/src/page-components/device/BleControl.module.css b/src/page-components/device/BleControl.module.css new file mode 100644 index 0000000..26291a5 --- /dev/null +++ b/src/page-components/device/BleControl.module.css @@ -0,0 +1,11 @@ +@layer pages { + .ble_control { + height: calc(var(--spacing) * 12); + padding-inline: calc(var(--spacing) * 2); + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: center; + gap: calc(var(--spacing) * 2); + } +} diff --git a/src/page-components/device/BleControl.tsx b/src/page-components/device/BleControl.tsx new file mode 100644 index 0000000..a068e75 --- /dev/null +++ b/src/page-components/device/BleControl.tsx @@ -0,0 +1,17 @@ +import { useAtomValue } from 'jotai'; +import { FC } from 'react'; +import { BleState } from '../../context/EstimContext'; +import styles from './BleControl.module.css'; + +const BleControl: FC = () => { + const bleState = useAtomValue(BleState); + + return ( +
+ + +
+ ); +}; + +export default BleControl; diff --git a/src/page-components/device/DeviceDetail.module.css b/src/page-components/device/DeviceDetail.module.css new file mode 100644 index 0000000..8597fbb --- /dev/null +++ b/src/page-components/device/DeviceDetail.module.css @@ -0,0 +1,8 @@ +@layer pages { + .device_detail { + flex: 2; + border-radius: calc(var(--border-radius) * 2); + padding: calc(var(--spacing) * 2); + background-color: var(--color-dark-surface-container); + } +} diff --git a/src/page-components/device/DeviceDetail.tsx b/src/page-components/device/DeviceDetail.tsx new file mode 100644 index 0000000..0b7fba0 --- /dev/null +++ b/src/page-components/device/DeviceDetail.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import styles from './DeviceDetail.module.css'; + +const DeviceDetail: FC = () => { + return
; +}; + +export default DeviceDetail; diff --git a/src/page-components/device/DeviceList.module.css b/src/page-components/device/DeviceList.module.css new file mode 100644 index 0000000..ce41bb4 --- /dev/null +++ b/src/page-components/device/DeviceList.module.css @@ -0,0 +1,8 @@ +@layer pages { + .devices { + flex: 1; + display: flex; + flex-direction: column; + align-items: stretch; + } +} diff --git a/src/page-components/device/DeviceList.tsx b/src/page-components/device/DeviceList.tsx new file mode 100644 index 0000000..a21b9ed --- /dev/null +++ b/src/page-components/device/DeviceList.tsx @@ -0,0 +1,13 @@ +import { FC } from 'react'; +import { ScrollArea } from '../../components/ScrollArea'; +import styles from './DeviceList.module.css'; + +const DeviceList: FC = () => { + return ( +
+ +
+ ); +}; + +export default DeviceList; diff --git a/src/page-components/play-control/ChannelHost.module.css b/src/page-components/play-control/ChannelHost.module.css new file mode 100644 index 0000000..85257e6 --- /dev/null +++ b/src/page-components/play-control/ChannelHost.module.css @@ -0,0 +1,11 @@ +@layer pages { + .channel_host { + flex: 1; + padding: calc(var(--spacing) * 2) calc(var(--spacing) * 3); + border-radius: calc(var(--border-radius) * 2); + background-color: var(--color-dark-surface-container); + display: flex; + flex-direction: column; + gap: calc(var(--spacing)); + } +} diff --git a/src/page-components/play-control/ChannelHost.tsx b/src/page-components/play-control/ChannelHost.tsx new file mode 100644 index 0000000..22fad0f --- /dev/null +++ b/src/page-components/play-control/ChannelHost.tsx @@ -0,0 +1,17 @@ +import { FC } from 'react'; +import { Channels } from '../../context/EstimContext'; +import styles from './ChannelHost.module.css'; + +type ChannelHostProps = { + channel: Channels; +}; + +const ChannelHost: FC = ({ channel }) => { + return ( +
+

Channel {channel.toUpperCase()}

+
+ ); +}; + +export default ChannelHost; diff --git a/src/page-components/state-bar/BleState.tsx b/src/page-components/state-bar/BleState.tsx new file mode 100644 index 0000000..47a8b6b --- /dev/null +++ b/src/page-components/state-bar/BleState.tsx @@ -0,0 +1,30 @@ +import { useAtomValue } from 'jotai'; +import { FC, useMemo } from 'react'; +import { BleState } from '../../context/EstimContext'; +import IconBluetooth from '../../icons/IconBluetooth'; + +const BleStates: FC = () => { + const ble = useAtomValue(BleState); + const bleIcon = useMemo(() => { + if (ble.ready && !ble.searching && !ble.connected) { + return 'material-symbols-light:bluetooth'; + } else if (ble.ready && ble.searching) { + return 'material-symbols-light:bluetooth-searching'; + } else if (ble.ready && !ble.searching && ble.connected) { + return 'material-symbols-light:bluetooth-connected'; + } else { + return 'material-symbols-light:bluetooth-disabled'; + } + }, [ble]); + + return ( + 0} + /> + ); +}; + +export default BleStates; diff --git a/src/page-components/state-bar/ChannelStates.module.css b/src/page-components/state-bar/ChannelStates.module.css new file mode 100644 index 0000000..3e7d7d0 --- /dev/null +++ b/src/page-components/state-bar/ChannelStates.module.css @@ -0,0 +1,10 @@ +@layer pages { + .channel_state { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: calc(var(--spacing)); + font-size: calc(var(--font-size) * 1.4); + } +} diff --git a/src/page-components/state-bar/ChannelStates.tsx b/src/page-components/state-bar/ChannelStates.tsx new file mode 100644 index 0000000..4a51123 --- /dev/null +++ b/src/page-components/state-bar/ChannelStates.tsx @@ -0,0 +1,35 @@ +import { Icon } from '@iconify/react/dist/iconify.js'; +import { useAtomValue } from 'jotai'; +import { FC } from 'react'; +import { Channels, ChannelState } from '../../context/EstimContext'; +import { smallIconProps } from '../../icons/shared-props'; +import styles from './ChannelStates.module.css'; + +const ChannelStates: FC<{ channel: Channels }> = ({ channel }) => { + const chState = useAtomValue(ChannelState(channel)); + + return ( +
+ Ch {channel.toUpperCase()} + {chState.playing ? ( + + ) : ( + + )} + {chState.strength} + + {chState.boostLevel} + {chState.playMode === 'shuffle' && ( + + )} + {chState.playMode === 'repeat' && ( + + )} + {chState.playMode === 'repeat-one' && ( + + )} +
+ ); +}; + +export default ChannelStates; diff --git a/src/page-components/state-bar/DeviceStates.tsx b/src/page-components/state-bar/DeviceStates.tsx new file mode 100644 index 0000000..741827a --- /dev/null +++ b/src/page-components/state-bar/DeviceStates.tsx @@ -0,0 +1,18 @@ +import { useAtomValue } from 'jotai'; +import { FC } from 'react'; +import { DeviceState } from '../../context/EstimContext'; +import IconBattery from '../../icons/IconBattery'; +import IconRssi from '../../icons/IconRssi'; + +const DeviceStates: FC = () => { + const deviceState = useAtomValue(DeviceState); + + return ( + <> + + + + ); +}; + +export default DeviceStates; diff --git a/src/page-components/state-bar/StateBar.module.css b/src/page-components/state-bar/StateBar.module.css new file mode 100644 index 0000000..a61769a --- /dev/null +++ b/src/page-components/state-bar/StateBar.module.css @@ -0,0 +1,13 @@ +@layer pages { + .state_bar { + flex: 1; + padding-block-start: calc(var(--spacing)); + padding-inline: calc(var(--spacing) * 2); + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: center; + gap: calc(var(--spacing) * 4); + font-size: calc(var(--font-size) * 1.6); + } +} diff --git a/src/page-components/state-bar/StateBar.tsx b/src/page-components/state-bar/StateBar.tsx new file mode 100644 index 0000000..4f4f4ca --- /dev/null +++ b/src/page-components/state-bar/StateBar.tsx @@ -0,0 +1,18 @@ +import { FC } from 'react'; +import BleStates from './BleState'; +import ChannelStates from './ChannelStates'; +import DeviceStates from './DeviceStates'; +import styles from './StateBar.module.css'; + +const StateBar: FC = () => { + return ( +
+ + + + +
+ ); +}; + +export default StateBar; diff --git a/src/pages/Device.module.css b/src/pages/Device.module.css new file mode 100644 index 0000000..ba81ed8 --- /dev/null +++ b/src/pages/Device.module.css @@ -0,0 +1,11 @@ +@layer pages { + .device_list { + flex: 1; + display: flex; + flex-direction: column; + align-items: stretch; + gap: calc(var(--spacing) * 2); + border-radius: calc(var(--border-radius) * 2); + background-color: var(--color-dark-surface-container); + } +} diff --git a/src/pages/Device.tsx b/src/pages/Device.tsx new file mode 100644 index 0000000..dc052d2 --- /dev/null +++ b/src/pages/Device.tsx @@ -0,0 +1,19 @@ +import { FC } from 'react'; +import BleControl from '../page-components/device/BleControl'; +import DeviceDetail from '../page-components/device/DeviceDetail'; +import DeviceList from '../page-components/device/DeviceList'; +import styles from './Device.module.css'; + +const Device: FC = () => { + return ( +
+
+ + +
+ +
+ ); +}; + +export default Device; diff --git a/src/pages/PatternEditor.tsx b/src/pages/PatternEditor.tsx new file mode 100644 index 0000000..3a953aa --- /dev/null +++ b/src/pages/PatternEditor.tsx @@ -0,0 +1,7 @@ +import { FC } from 'react'; + +const PatternEditor: FC = () => { + return
; +}; + +export default PatternEditor; diff --git a/src/pages/PatternLibrary.tsx b/src/pages/PatternLibrary.tsx new file mode 100644 index 0000000..a7fdb34 --- /dev/null +++ b/src/pages/PatternLibrary.tsx @@ -0,0 +1,7 @@ +import { FC } from 'react'; + +const PatternLibrary: FC = () => { + return
; +}; + +export default PatternLibrary; diff --git a/src/pages/Play.module.css b/src/pages/Play.module.css new file mode 100644 index 0000000..fb67200 --- /dev/null +++ b/src/pages/Play.module.css @@ -0,0 +1,10 @@ +@layer pages { + .play_control { + width: 100%; + overflow: hidden; + display: flex; + flex-direction: row; + align-items: stretch; + gap: calc(var(--spacing) * 2); + } +} diff --git a/src/pages/Play.tsx b/src/pages/Play.tsx new file mode 100644 index 0000000..4e2509e --- /dev/null +++ b/src/pages/Play.tsx @@ -0,0 +1,13 @@ +import { FC } from 'react'; +import ChannelHost from '../page-components/play-control/ChannelHost'; + +const PlayControl: FC = () => { + return ( +
+ + +
+ ); +}; + +export default PlayControl; diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx new file mode 100644 index 0000000..a9f5506 --- /dev/null +++ b/src/pages/Settings.tsx @@ -0,0 +1,7 @@ +import { FC } from 'react'; + +const Settings: FC = () => { + return
; +}; + +export default Settings; diff --git a/src/theme.css b/src/theme.css new file mode 100644 index 0000000..dfe1e99 --- /dev/null +++ b/src/theme.css @@ -0,0 +1,75 @@ +@import './variables.css' layer(theme); + +@layer base { + :root { + font-family: var(--font-family); + font-size: var(--font-size); + line-height: var(--line-height); + font-weight: 400; + + width: 100vw; + height: 100vh; + overflow: hidden; + user-select: none; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; + } + + :where(html, body) { + color-scheme: dark; + width: 100vw; + height: 100vh; + overflow: hidden; + z-index: 10; + } + + body { + position: relative; + color: var(--color-dark-on-surface); + background-color: var(--color-dark-surface); + } + + :where(h1, h2, h3, h4, h5, h6, p, div, span) { + margin: 0; + padding: 0; + } + + :where(menu) { + margin: 0; + } + + #root { + width: 100%; + height: 100%; + overflow: hidden; + z-index: 15; + } + + .evelation-0 { + box-shadow: var(--elevation-dark-0); + } + + .evelation-1 { + box-shadow: var(--elevation-dark-1-ambient), --var(--elevation-dark-1-umbra); + } + + .evelation-2 { + box-shadow: var(--elevation-dark-2-ambient), var(--elevation-dark-2-umbra); + } + + .evelation-3 { + box-shadow: var(--elevation-dark-3-ambient), var(--elevation-dark-3-umbra); + } + + .evelation-4 { + box-shadow: var(--elevation-dark-4-ambient), var(--elevation-dark-4-umbra); + } + + .evelation-5 { + box-shadow: var(--elevation-dark-5-ambient), var(--elevation-dark-5-umbra); + } +} diff --git a/src/variables.css b/src/variables.css new file mode 100644 index 0000000..e8e7bb4 --- /dev/null +++ b/src/variables.css @@ -0,0 +1,271 @@ +@layer theme { + :root { + --font-family: 'Inter', system-ui, Avenir, Helvetica, Arial, sans-serif; + + --color-white: #ffffff; + --color-black: #000000; + --color-light-primary: #744c1f; + --color-light-on-primary: #fff5c0; + --color-light-primary-container: #95693b; + --color-light-on-primary-container: #fff5c0; + --color-light-primary-fixed: #95693b; + --color-light-primary-fixed-dim: #7a5124; + --color-light-on-primary-fixed: #fff5c0; + --color-light-on-primary-fixed-variant: #fff5c0; + --color-light-inverse-primary: #6f481b; + --color-light-secondary: #67513b; + --color-light-on-secondary: #fffae0; + --color-light-secondary-container: #866e57; + --color-light-on-secondary-container: #fffae0; + --color-light-secondary-fixed: #866e57; + --color-light-secondary-fixed-dim: #6c5540; + --color-light-on-secondary-fixed: #fffae0; + --color-light-on-secondary-fixed-variant: #fffae0; + --color-light-inverse-secondary: #dec1a8; + --color-light-tertiary: #5c561a; + --color-light-on-tertiary: #ffffbb; + --color-light-tertiary-container: #7b7436; + --color-light-on-tertiary-container: #ffffbb; + --color-light-tertiary-fixed: #7b7436; + --color-light-tertiary-fixed-dim: #615b1f; + --color-light-on-tertiary-fixed: #ffffbb; + --color-light-on-tertiary-fixed-variant: #ffffbb; + --color-light-inverse-tertiary: #d3c886; + --color-light-error: #b20000; + --color-light-on-error: #ffc89b; + --color-light-error-container: #d92f18; + --color-light-on-error-container: #ffc89b; + --color-light-error-fixed: #ffab80; + --color-light-error-fixed-dim: #ff8e66; + --color-light-on-error-fixed: #640000; + --color-light-on-error-fixed-variant: #9f0000; + --color-light-inverse-error: #ff8e66; + --color-light-surface: #fff8f1; + --color-light-surface-dim: #bfb7b1; + --color-light-surface-bright: #fff8f1; + --color-light-surface-variant: #efe0d4; + --color-light-surface-container: #e9e1db; + --color-light-surface-container-lowest: #fffef7; + --color-light-surface-container-low: #f7efe9; + --color-light-surface-container-high: #dbd3cd; + --color-light-surface-container-highest: #cdc5bf; + --color-light-on-surface: #090000; + --color-light-on-surface-variant: #41362d; + --color-light-inverse-surface: #f7efe9; + --color-light-inverse-on-surface: #090000; + --color-light-outline: #5f5349; + --color-light-outline-variant: #7d7065; + --color-light-scrim: #090000; + --color-light-shadow: #090000; + --color-light-success: #1c6300; + --color-light-on-success: #d6ff6a; + --color-light-success-container: #3f8200; + --color-light-on-success-container: #d6ff6a; + --color-light-success-fixed: #3f8200; + --color-light-success-fixed-dim: #226800; + --color-light-on-success-fixed: #d6ff6a; + --color-light-on-success-fixed-variant: #d6ff6a; + --color-light-inverse-success: #053f00; + --color-light-defensive: #7131b4; + --color-light-on-defensive: #ffdeff; + --color-light-defensive-container: #9350d6; + --color-light-on-defensive-container: #ffdeff; + --color-light-defensive-fixed: #9350d6; + --color-light-defensive-fixed-dim: #7737ba; + --color-light-on-defensive-fixed: #ffdeff; + --color-light-on-defensive-fixed-variant: #ffdeff; + --color-light-inverse-defensive: #47038c; + --color-light-gentle: #7c4b00; + --color-light-on-gentle: #fff25f; + --color-light-gentle-container: #9e6700; + --color-light-on-gentle-container: #fff25f; + --color-light-gentle-fixed: #9e6700; + --color-light-gentle-fixed-dim: #814f00; + --color-light-on-gentle-fixed: #fff25f; + --color-light-on-gentle-fixed-variant: #fff25f; + --color-light-inverse-gentle: #562900; + --color-light-aggressive: #b6002d; + --color-light-on-aggressive: #ffc3cc; + --color-light-aggressive-container: #dc2247; + --color-light-on-aggressive-container: #ffc3cc; + --color-light-aggressive-fixed: #dc2247; + --color-light-aggressive-fixed-dim: #bd0031; + --color-light-on-aggressive-fixed: #ffc3cc; + --color-light-on-aggressive-fixed-variant: #ffc3cc; + --color-light-inverse-aggressive: #88000e; + --color-light-info: #225694; + --color-light-on-info: #ddffff; + --color-light-info-container: #4973b4; + --color-light-on-info-container: #ddffff; + --color-light-info-fixed: #4973b4; + --color-light-info-fixed-dim: #2a5a99; + --color-light-on-info-fixed: #ddffff; + --color-light-on-info-fixed-variant: #ddffff; + --color-light-inverse-info: #00346e; + --color-light-warning: #834600; + --color-light-on-warning: #ffed93; + --color-light-warning-container: #a66204; + --color-light-on-warning-container: #ffed93; + --color-light-warning-fixed: #a66204; + --color-light-warning-fixed-dim: #884a00; + --color-light-on-warning-fixed: #ffed93; + --color-light-on-warning-fixed-variant: #ffed93; + --color-light-inverse-warning: #5b2300; + --color-dark-primary: #ffe6b1; + --color-dark-on-primary: #280000; + --color-dark-primary-container: #ecb986; + --color-dark-on-primary-container: #2b0000; + --color-dark-primary-fixed: #ffd8a5; + --color-dark-primary-fixed-dim: #f1bd8a; + --color-dark-on-primary-fixed: #280000; + --color-dark-on-primary-fixed-variant: #2d0600; + --color-dark-inverse-primary: #e8b482; + --color-dark-secondary: #ffebd1; + --color-dark-on-secondary: #1a0000; + --color-dark-secondary-container: #dabea5; + --color-dark-on-secondary-container: #1f0500; + --color-dark-secondary-fixed: #fbddc4; + --color-dark-secondary-fixed-dim: #dec1a8; + --color-dark-on-secondary-fixed: #1a0000; + --color-dark-on-secondary-fixed-variant: #220c00; + --color-dark-inverse-secondary: #715a44; + --color-dark-tertiary: #fef1ad; + --color-dark-on-tertiary: #150100; + --color-dark-tertiary-container: #cfc482; + --color-dark-on-tertiary-container: #1b0c00; + --color-dark-tertiary-fixed: #f0e4a0; + --color-dark-tertiary-fixed-dim: #d3c886; + --color-dark-on-tertiary-fixed: #150100; + --color-dark-on-tertiary-fixed-variant: #1e1200; + --color-dark-inverse-tertiary: #666023; + --color-dark-error: #ffb88d; + --color-dark-on-error: #4e0000; + --color-dark-error-container: #ff8a63; + --color-dark-on-error-container: #540000; + --color-dark-error-fixed: #ffab80; + --color-dark-error-fixed-dim: #ff8e66; + --color-dark-on-error-fixed: #640000; + --color-dark-on-error-fixed-variant: #9f0000; + --color-dark-inverse-error: #bf0902; + --color-dark-surface: #18120c; + --color-dark-surface-dim: #18120c; + --color-dark-surface-bright: #554f4a; + --color-dark-surface-variant: #50453b; + --color-dark-surface-container: #352f2a; + --color-dark-surface-container-lowest: #090000; + --color-dark-surface-container-low: #241f1a; + --color-dark-surface-container-high: #403a35; + --color-dark-surface-container-highest: #4c4640; + --color-dark-on-surface: #fffef7; + --color-dark-on-surface-variant: #fffdf0; + --color-dark-inverse-surface: #352f2a; + --color-dark-inverse-on-surface: #fffef7; + --color-dark-outline: #fcede1; + --color-dark-outline-variant: #cec0b4; + --color-dark-scrim: #090000; + --color-dark-shadow: #090000; + --color-dark-defensive: #ffcfff; + --color-dark-on-defensive: #050055; + --color-dark-defensive-container: #eca2ff; + --color-dark-on-defensive-container: #0d005d; + --color-dark-defensive-fixed: #ffc1ff; + --color-dark-defensive-fixed-dim: #f0a5ff; + --color-dark-on-defensive-fixed: #050055; + --color-dark-on-defensive-fixed-variant: #140062; + --color-dark-inverse-defensive: #ffcbff; + --color-dark-info: #cef0ff; + --color-dark-on-info: #00013a; + --color-dark-info-container: #9fc3ff; + --color-dark-on-info-container: #000d41; + --color-dark-info-fixed: #c0e2ff; + --color-dark-info-fixed-dim: #a3c6ff; + --color-dark-on-info-fixed: #00013a; + --color-dark-on-info-fixed-variant: #001346; + --color-dark-inverse-info: #caecff; + --color-dark-gentle: #ffe350; + --color-dark-on-gentle: #370000; + --color-dark-gentle-container: #fcb61a; + --color-dark-on-gentle-container: #390000; + --color-dark-gentle-fixed: #ffd642; + --color-dark-gentle-fixed-dim: #ffba20; + --color-dark-on-gentle-fixed: #370000; + --color-dark-on-gentle-fixed-variant: #3b0000; + --color-dark-inverse-gentle: #ffdf4c; + --color-dark-success: #c7ff5b; + --color-dark-on-success: #001600; + --color-dark-success-container: #97d529; + --color-dark-on-success-container: #001c00; + --color-dark-success-fixed: #b9f54d; + --color-dark-success-fixed-dim: #9cd92e; + --color-dark-on-success-fixed: #001600; + --color-dark-on-success-fixed-variant: #001f00; + --color-dark-inverse-success: #c2ff57; + --color-dark-warning: #ffde85; + --color-dark-on-warning: #350000; + --color-dark-warning-container: #ffb15b; + --color-dark-on-warning-container: #380000; + --color-dark-warning-fixed: #ffd179; + --color-dark-warning-fixed-dim: #ffb55e; + --color-dark-on-warning-fixed: #350000; + --color-dark-on-warning-fixed-variant: #3a0000; + --color-dark-inverse-warning: #ffda82; + --color-dark-aggressive: #ffb3bd; + --color-dark-on-aggressive: #4d0000; + --color-dark-aggressive-container: #ff8492; + --color-dark-on-aggressive-container: #540000; + --color-dark-aggressive-fixed: #ffa6b0; + --color-dark-aggressive-fixed-dim: #ff8896; + --color-dark-on-aggressive-fixed: #4d0000; + --color-dark-on-aggressive-fixed-variant: #590000; + --color-dark-inverse-aggressive: #ffafba; + + --spacing: 4px; + --border-radius: 2px; + --font-size: 10px; + --line-height: 1.2em; + + --elevation-light-0: none; + --elevation-light-1-ambient: 0 1px 3px 1px + color-mix(in oklch, var(--color-light-shadow) 15%, transparent); + --elevation-light-1-umbra: 0 1px 2px 0px + color-mix(in oklch, var(--color-light-shadow) 30%, transparent); + --elevation-light-2-ambient: 0 2px 6px 2px + color-mix(in oklch, var(--color-light-shadow) 15%, transparent); + --elevation-light-2-umbra: 0 1px 2px 0px + color-mix(in oklch, var(--color-light-shadow) 30%, transparent); + --elevation-light-3-ambient: 0 4px 8px 3px + color-mix(in oklch, var(--color-light-shadow) 15%, transparent); + --elevation-light-3-umbra: 0 1px 3px 0px + color-mix(in oklch, var(--color-light-shadow) 30%, transparent); + --elevation-light-4-ambient: 0 6px 10px 4px + color-mix(in oklch, var(--color-light-shadow) 15%, transparent); + --elevation-light-4-umbra: 0 2px 3px 0px + color-mix(in oklch, var(--color-light-shadow) 30%, transparent); + --elevation-light-5-ambient: 0 8px 12px 6px + color-mix(in oklch, var(--color-light-shadow) 15%, transparent); + --elevation-light-5-umbra: 0 4px 4px 0px + color-mix(in oklch, var(--color-light-shadow) 30%, transparent); + + --elevation-dark-0: none; + --elevation-dark-1-ambient: 0 1px 3px 1px + color-mix(in oklch, var(--color-dark-shadow) 15%, transparent); + --elevation-dark-1-umbra: 0 1px 2px 0px + color-mix(in oklch, var(--color-dark-shadow) 30%, transparent); + --elevation-dark-2-ambient: 0 2px 6px 2px + color-mix(in oklch, var(--color-dark-shadow) 15%, transparent); + --elevation-dark-2-umbra: 0 1px 2px 0px + color-mix(in oklch, var(--color-dark-shadow) 30%, transparent); + --elevation-dark-3-ambient: 0 4px 8px 3px + color-mix(in oklch, var(--color-dark-shadow) 15%, transparent); + --elevation-dark-3-umbra: 0 1px 3px 0px + color-mix(in oklch, var(--color-dark-shadow) 30%, transparent); + --elevation-dark-4-ambient: 0 6px 10px 4px + color-mix(in oklch, var(--color-dark-shadow) 15%, transparent); + --elevation-dark-4-umbra: 0 2px 3px 0px + color-mix(in oklch, var(--color-dark-shadow) 30%, transparent); + --elevation-dark-5-ambient: 0 8px 12px 6px + color-mix(in oklch, var(--color-dark-shadow) 15%, transparent); + --elevation-dark-5-umbra: 0 4px 4px 0px + color-mix(in oklch, var(--color-dark-shadow) 30%, transparent); + } +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a7fc6fb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..42872c5 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..4571eb3 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,38 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +// @ts-expect-error process is a nodejs global +const host = process.env.TAURI_DEV_HOST; + +// https://vitejs.dev/config/ +export default defineConfig(async () => ({ + plugins: [react()], + css: { + transformer: 'lightningcss', + lightningcss: { + cssModules: true, + }, + }, + + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // + // 1. prevent vite from obscuring rust errors + clearScreen: false, + // 2. tauri expects a fixed port, fail if that port is not available + server: { + port: 1420, + strictPort: true, + host: host || false, + hmr: host + ? { + protocol: 'ws', + host, + port: 1421, + } + : undefined, + watch: { + // 3. tell vite to ignore watching `src-tauri` + ignored: ['**/src-tauri/**'], + }, + }, +}));