diff --git a/application/package.json b/application/package.json index 2923d66..a678469 100644 --- a/application/package.json +++ b/application/package.json @@ -1,7 +1,7 @@ { "name": "kbs-electron", "productName": "Keyboard Sounds", - "version": "1.1.0", + "version": "1.1.1", "description": "https://keyboardsounds.net/", "main": ".webpack/main", "repository": { diff --git a/application/src/api/core.js b/application/src/api/core.js index 521d2d0..b243ece 100644 --- a/application/src/api/core.js +++ b/application/src/api/core.js @@ -170,6 +170,28 @@ const kbs = { return ""; }, + selectExportPath: async function(profileToExport) { + if (this.openFileDialogIsOpen) { + return; + } + + this.openFileDialogIsOpen = true; + const res = await dialog.showSaveDialog(this.mainWindow, { + title: `Export Profile '${profileToExport}'`, + defaultPath: `${profileToExport}.zip`, + filters: [ + { name: 'Zip Archive', extensions: ['zip'] } + ] + }); + this.openFileDialogIsOpen = false; + this.mainWindow.show(); + this.mainWindow.focus(); + if (!res.canceled) { + return res.filePath + } + return ""; + }, + executeDaemonCommand: async function(command) { const status = await this.status(); if (status.status !== 'running') { diff --git a/application/src/ui/pages/profiles.jsx b/application/src/ui/pages/profiles.jsx index 61814e9..a602d3e 100644 --- a/application/src/ui/pages/profiles.jsx +++ b/application/src/ui/pages/profiles.jsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import IconButton from "@mui/material/IconButton"; import Tooltip from "@mui/material/Tooltip"; @@ -10,16 +10,19 @@ import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; import ListItemText from '@mui/material/ListItemText'; import InputAdornment from '@mui/material/InputAdornment'; +import Dialog from '@mui/material/Dialog'; import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined'; import AddIcon from '@mui/icons-material/Add'; import SearchIcon from '@mui/icons-material/Search'; import IosShareIcon from '@mui/icons-material/IosShare'; import FileOpenIcon from '@mui/icons-material/FileOpenOutlined'; +import CloseIcon from '@mui/icons-material/Close'; +import SaveIcon from '@mui/icons-material/Save'; import { Chip, CircularProgress } from "@mui/material"; import { execute } from "../execute"; -function ProfileListItem({ statusLoaded, status, profile: { name, author, description } }) { +function ProfileListItem({ statusLoaded, status, profile: { name, author, description }, onExport }) { const [isDeleting, setIsDeleting] = useState(false); return ( @@ -31,7 +34,7 @@ function ProfileListItem({ statusLoaded, status, profile: { name, author, descri )} - + @@ -102,12 +105,123 @@ function ProfileListItem({ statusLoaded, status, profile: { name, author, descri const Profiles = ({statusLoaded, status, profilesLoaded, profiles}) => { const [profileSearchValue, setProfileSearchValue] = useState(''); + const [exportProfileDialogOpen, setExportProfileDialogOpen] = useState(false); + const [exportPath, setExportPath] = useState(""); + const [exportingProfile, setExportingProfile] = useState(false); + const [profileToExport, setProfileToExport] = useState(""); + + useEffect(() => { + if (!exportProfileDialogOpen) { + setExportPath(""); + } + }, [exportProfileDialogOpen]); + + const selectExportPath = () => { + execute(`selectExportPath ${profileToExport}`).then((path) => { + if (path) { + setExportPath(path); + } + }); + }; + + const exportProfile = () => { + if (exportPath === "") { + return; + } + + setExportingProfile(true); + execute(`export-profile --name "${profileToExport}" --output "${exportPath}"`) + .then(() => { + setExportingProfile(false); + setExportProfileDialogOpen(false); + setProfileToExport(""); + }); + }; return ( + + + + Export Profile + setExportProfileDialogOpen(false)}> + + + + + Export To + + + {exportPath !== "" && ( + + + {exportPath} + + + )} + {exportPath === "" && ( + + Specify an export path... + + )} + + + + + + + + { }, }}> {profilesLoaded && profiles.filter(p => profileSearchValue === "" || p.name.toLowerCase().includes(profileSearchValue.toLowerCase())).map((profile) => ( - + { + setProfileToExport(profile.name); + setExportProfileDialogOpen(true); + }} /> ))} diff --git a/docs/backend.md b/docs/backend.md index f8eb112..932943b 100644 --- a/docs/backend.md +++ b/docs/backend.md @@ -38,6 +38,12 @@ $ kbs stop ## Manage Profiles ```bash +# Create a new profile +$ kbs new -n "My Profile" + +# Export an existing profile +$ kbs export-profile -n "My Profile" -o "My Profile.zip" + # List downloadable profiles $ kbs list-profiles --remote diff --git a/docs/custom-profiles.md b/docs/custom-profiles.md index fca2b95..f33c7f8 100644 --- a/docs/custom-profiles.md +++ b/docs/custom-profiles.md @@ -5,6 +5,7 @@ This application supports custom profiles in which you can provide your own WAV ## Index - [Importing a profile](#importing-a-profile) +- [Exporting an existing profile](#exporting-an-existing-profile) - [Creating a new Profile](#creating-a-new-profile) - [Editing a Profile](#editing-a-profile) - [Compiling a Profile](#compiling-a-profile) @@ -18,6 +19,14 @@ Profiles can be imported from a ZIP file using the [`add-profile`](./backend.md# $ kbs add-profile -z "./my-profile.zip" ``` +## Exporting an existing profile + +Profiles can be exported from the command line using the [`export-profile`](./backend.md#manage-profiles) action. + +```bash +$ kbs export-profile -n my-profile -o "./my-profile.zip" +``` + ## Creating a new Profile Create a new profile using the following command: diff --git a/keyboardsounds/main.py b/keyboardsounds/main.py index 878b9f8..d0b56b2 100644 --- a/keyboardsounds/main.py +++ b/keyboardsounds/main.py @@ -68,6 +68,7 @@ def main(): f" %(prog)s -n {os.linesep}" f" %(prog)s [-s] [--remote]{os.linesep}" f" %(prog)s -n {os.linesep}" + f" %(prog)s -n -o {os.linesep * 2}" f" %(prog)s -d -o {os.linesep * 2}" ) + win_messages @@ -291,11 +292,12 @@ def main(): Profile.download_profile(args.name) except Exception as e: print(e) - elif args.action == "create-profile" or args.action == "new": Profile.create_profile(args.directory, args.name) return - + elif args.action == "export-profile" or args.action == "ex": + Profile.export_profile(args.name, args.output) + return # Rules are only available on windows elif WIN32 and (args.action == "list-rules" or args.action == "lr"): rules = get_rules() diff --git a/keyboardsounds/profile.py b/keyboardsounds/profile.py index 242a848..a123b66 100644 --- a/keyboardsounds/profile.py +++ b/keyboardsounds/profile.py @@ -8,6 +8,7 @@ from keyboardsounds.root import ROOT from keyboardsounds.path_resolver import PathResolver from keyboardsounds.profile_validation import validate_profile +from keyboardsounds.profile_builder import CliProfileBuilder PROFILES_REMOTE_URL = "https://api.github.com/repos/nathan-fiscaletti/keyboardsounds/contents/keyboardsounds/profiles?ref=master" PROFILE_REMOTE_URL = "https://api.github.com/repos/nathan-fiscaletti/keyboardsounds/contents/keyboardsounds/profiles/{name}?ref=master" @@ -60,6 +61,10 @@ def metadata(self): ) return {"name": name, "author": author, "description": description} + def export(self, output: str): + # export profile to zip file + CliProfileBuilder(self.root, output).save() + @classmethod def list(cls): names = [ @@ -220,3 +225,17 @@ def create_profile(cls, path: str, name: str): print("") print(f" kbs build-profile -path {path} -o {name}.zip") print("") + + @classmethod + def export_profile(cls, name: str, output: str): + if name is None: + print("Please specify a name for the profile to export.") + return + + try: + profile = Profile(name) + except Exception as e: + print(f"Failed to find profile '{name}'. Error: {e}") + return + + profile.export(output) diff --git a/keyboardsounds/profile_builder.py b/keyboardsounds/profile_builder.py index 62bd4c4..9b83868 100644 --- a/keyboardsounds/profile_builder.py +++ b/keyboardsounds/profile_builder.py @@ -185,8 +185,8 @@ def build(self, output: str): print(f"Writing '{file}'...") source = self.get_file_path(file) destination = intermediate.get_file_path(file) - if not os.path.isfile(destination): - raise ValueError(f"Missing audio file '{file}'.") + if not os.path.isfile(source): + raise ValueError(f"Missing intermediate audio file '{file}'.") shutil.copy(source, destination) print(f"Writing archive to '{output}'...") @@ -276,7 +276,7 @@ def start(self): self.__collect_metadata() else: if self.__output is not None: - self.__save() + self.save() else: self.__open_command_interface() @@ -313,7 +313,7 @@ def __process_command(self): elif self.__cmd == "remove": return self.__remove() elif self.__cmd == "save": - return self.__save() + return self.save() elif self.__cmd == "cancel": return False @@ -546,7 +546,7 @@ def __preview(self) -> bool: print(self.__builder.preview()) return True - def __save(self) -> bool: + def save(self) -> bool: if len(self.__args) < 1: output = self.__output else: diff --git a/pyproject.toml b/pyproject.toml index 64c7736..2f973bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "keyboardsounds" -version = "5.8.4" +version = "5.8.5" authors = [ { name="Nathan Fiscaletti", email="nate.fiscaletti@gmail.com" }, ] diff --git a/setup.py b/setup.py index 2a26528..ae4c3b5 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="keyboardsounds", - version="5.8.4", + version="5.8.5", description="Adds the ability to play sounds while typing on any system.", author="Nathan Fiscaletti", author_email="nate.fiscaletti@gmail.com",