3 things to comment here. The first is the use of the svelte-spa-router library (for more details see its doc here). For the simple purpose of changing views in a desktop application, this library more than fulfills its purpose. With the 7 views or pages we create a dictionary (or JavaScript object) that associates routes with views. Then this dictionary is passed as props to the Router component of svelte-spa-router. It\\'s that simple. As we will see later, through programmatic navigation or through user action we can easily change views.
The other thing is that I added a little gadget: when the user presses the Escape key the application closes (on the Settings page a tip clarifies to the user that this key closes the application). Svelte actually makes the job a lot easier, because this simple line:
The last thing to clarify is why the HTML main tag carries Tailwind\\'s overflow-hidden utility class. Since we\\'re going to use an animation where components appear to enter from the right, which momentarily \\\"increases\\\" the width of the window, overflow-hidden prevents an ugly horizontal scrollbar from appearing.
The first view the user sees when opening the application is the Login view/page. Its logic is similar to that of a login page in any web application. Let\\'s first look at the logic used for the views animations, because it is the same as that followed on the rest of the pages:
/* Login.svelte */{#if mounted}","image":"http://www.luping.net/uploads/20241222/17348695306768021ae773f.jpg17348695306768021ae774a.gif","datePublished":"2024-12-23T01:30:13+08:00","dateModified":"2024-12-23T01:30:13+08:00","author":{"@type":"Person","name":"luping.net","url":"https://www.luping.net/articlelist/0_1.html"}}The animation requires declaring a variable (state, let mount = false;) which is initially set to false. When the component is mounted, a lifecycle hook (onMount, similar to React) sets it to true and the animation can now begin. An entrance from the right of 1200 milliseconds duration is used (in:fly={{ x: 75, duration: 1200 }}) and a fade (out:fade={{ duration: 200 }}) of 200 milliseconds duration. Simple thanks to Svelte.
When setting up the Login view we also need to know if the user is already registered in the database or if it is the first time he/she enters the application:
/* Login.svelte */...let isLogin = false;...onMount(() => { GetMasterPassword().then((result) => { isLogin = result; // console.log(\\\"Master password exists in DB:\\\", isLogin); });...};Here we make use of GetMasterPassword which is a binding generated automatically when compiling the application and which was declared as a public method of the struct App (see the first part of this series). This function queries the database and, in case there is a master password registered in it, it considers the user as already registered (it returns a promise that wraps a boolean value), asking him to enter said password to allow him access to the rest of the views. If there is no master password in the database, the user is considered as \\\"new\\\" and what is asked is that he generates his own password to enter the application for the first time.
Finally, when mounting the Login.svelte component we do something that is important for the rest of the application. Although the svelte-i18n library forces us to declare the initial language code, as we have already seen, when mounting Login.svelte we ask the database (using the GetLanguage binding) to check if there is a language code saved. In case the database returns an empty string, that is, if there is no language configured as the user\\'s preference, svelte-i18n will use the value configured as initialLocale. If instead there is a language configured, that language will be set (locale.set(result);) and the \\\"change_titles\\\" event will be emitted, to which the translated titles of the title bar and native dialogs of the app will be passed for the backend to handle:
/* Login.svelte */The following is the logic for handling the login:
/* Login.svelte */Simply put: newPassword, the state bound to the input that gets what the user types, is first checked by onLogin to see if it has at least 6 characters and that all of them are ASCII characters, i.e. they are only 1 byte long (see the reason for that in part I of this series) by this little function const isAscii = (str: string): boolean => /^[\\\\x00-\\\\x7F] $/.test(str);. If the check fails the function returns and displays a warning toast to the user. Afterwards, if there is no master password saved in the database (isLogin = false), whatever the user types is saved by the SaveMasterPassword function (a binding generated by Wails); If the promise is resolved successfully (returns a uuid string as the Id of the record stored in the database), the user is taken to the home view by the svelte-spa-router library\\'s push method. Conversely, if the password passes the check for length and absence of non-ASCII characters and there is a master password in the DB (isLogin = true) then the CheckMasterPassword function verifies its identity against the stored one and either takes the user to the home view (promise resolved with true) or a toast is shown indicating that the entered password was incorrect.
The central view of the application and at the same time the most complex is the home view. Its HTML is actually subdivided into 3 components: a top button bar with a search input (TopActions component), a bottom button bar (BottomActions component) and a central area where the total number of saved password entries or the list of these is displayed using a scrollable window (EntriesList component):
/* Home.svelte */{#if mounted}Let\\'s take a look at the TopActions and EntriesList components since they are both very closely related. And they are, especially since their props are states of the parent component. This is where that new feature of Svelte5 comes into play: runes. Both components take props declared with the $bindable rune; this means that data can also flow up from child to parent. A diagram may make it clearer:
For example, in the TopActions component if the user clicks on the \\\"Entries list\\\" button, this is executed:
/* TopActions.svelte */onclick={() => { search = \\\"\\\"; // is `searchTerms` in the parent component isEntriesList = !isEntriesList; // is `showList` in the parent component}}That is, it makes the search state (searchTerms) an empty string, so that if there are any search terms it is reset and thus the entire list is shown. And on the other hand, it toggles the showList state (props isEntriesList in TopActions) so that the parent component shows or hides the list.
As we can see in the diagram above, both child components share the same props with the parent\\'s searchTerms state. The TopActions component captures the input from the user and passes it as state to the parent component Home, which in turn passes it as props to its child component EntriesList.
The main logic of displaying the full list or a list filtered by the search terms entered by the user is carried out, as expected, by the EntriesList component:
/* EntriesList.svelte */As we said, 2 props are received (listCounter and search) and a state is maintained (let entries: models.PasswordEntry[] = $state([]);). When the component is mounted at the user\\'s request, the backend is asked for the complete list of saved password entries. If there are no search terms, they are stored in the state; if there are, a simple filtering of the obtained array is performed and it is saved in the state:
/* EntriesList.svelte */... onMount(() => { GetAllEntries().then((result) => { // console.log(\\\"SEARCH:\\\", search); if (search) { const find = search.toLowerCase(); entries = result.filter( (item) => item.Username.toLowerCase().includes(find) || item.Website.toLowerCase().includes(find), ); } else { entries = result; } }); });...In the displayed list, the user can perform 2 actions. The first is to display the details of the entry, which is carried out when he clicks on the corresponding button: onclick={() => push(`/details/${entry.Id}`)}. Basically, we call the push method of the routing library to take the user to the details view, but passing the Id parameter corresponding to the item in question.
The other action the user can perform is to delete an item from the list. If he clicks on the corresponding button, he will be shown a confirmation popup, calling the showAlert function. This function in turn calls showWarning, which is actually an abstraction layer over the sweetalert2 library (all the functions that call the sweetalert2 library are in frontend/src/lib/popups/popups.ts). If the user confirms the deletion action, the DeleteEntry binding is called (to delete it from the DB) and, in turn, if the promise it returns is resolved, deleteItem is called (to delete it from the array stored in the entries state):
/* EntriesList.svelte */...const showAlert = (website: string, id: string) => { const data: string[] = [ `${$_(\\\"alert_deleting_password\\\")} \\\"${website}.\\\"`, `${$_(\\\"alert_confirm_deleting\\\")}`, ]; showWarning(data).then((result) => { if (result.value) { DeleteEntry(id).then(() => deleteItem(id)); showSuccess($_(\\\"deletion_confirm_msg\\\")); } }); };const deleteItem = (id: string): void => { let itemIdx = entries.findIndex((x) => x.Id === id); entries.splice(itemIdx, 1); entries = entries; GetPasswordCount().then((result) => (listCounter = result));};The other component of the Home view (BottomActions) is much simpler: it does not receive props and is limited to redirecting the user to various views (Settings, About or AddPassword).
The AddPassword and EditPassword views share very similar logic and are similar to the Login view as well. Both do not allow the user to enter spaces at the beginning and end of what they typed in the text input and follow the same policy as the Login view of requiring passwords to be at least 6 ASCII characters long. Basically, what sets them apart is that they call the Wails-generated links relevant to the action they need to perform:
/* AddPassword.svelte */...AddPasswordEntry(website, username, newPassword).then((result) => { result ? push(\\\"/home\\\") : false;});.../* EditPassword.svelte */...UpdateEntry(entry).then((result) => { result ? push(\\\"/home\\\") : false;});...The other view that is somewhat complex is Settings. This has a Language component that receives as props languageName from its parent component (Settings):
/* Language.svelte */...The HTML for this component is a single select that handles the user\\'s language choice. In its onchange event it receives a function (handleChange) that does 3 things:
- sets the language on the frontend using the svelte-i18n library
- emits an event (\\\"change_titles\\\") so that the Wails runtime changes the title of the application\\'s title bar and the titles of the Select Directory and Select File dialog boxes in relation to the previous action
- saves the language selected by the user in the DB so that the next time the application is started it will open configured with that language.
Returning to the Settings view, its entire operation is governed by a series of events that are sent and received to or from the backend. The simplest of all is the Quit button: when the user clicks on it, a quit event is triggered and listened to in the backend and the application closes (onclick={() => EventsEmit(\\\"quit\\\")}). A tip informs the user that the Escape key (shortcut) performs the same action, as we already explained.
The reset button calls a function that displays a popup window:
/* Setting.svelte */... const showAlert = () => { let data: string[] = [ `${$_(\\\"alert_delete_all\\\")}`, `${$_(\\\"alert_confirm_deleting\\\")}`, ]; showWarning(data).then((result) => { if (result.value) { Drop().then(() => push(\\\"/\\\")); showSuccess($_(\\\"alert_delete_confirm_msg\\\")); } }); };...If the user accepts the action, the Drop binding is called, which cleans all the collections in the DB, and if the promise it returns is resolved, it sends the user to the Login view, showing a modal indicating the success of the action.
The other two actions that remain are similar to each other, so let\\'s look at Import Data.
If the user clicks on the corresponding button, an event is emitted (onclick={() => EventsEmit(\\\"import_data\\\")}) which is listened for in the backend. When received, the native Select File dialog box is opened to allow the user to select the backup file. If the user chooses the file, the variable containing the path (fileLocation) will not contain an empty string and this will trigger an event in the backend (\\\"enter_password\\\") which is now listened for in the frontend to, in turn, display a new popup window asking for the master password used when the export was made. Again, the frontend will emit another event (\\\"password\\\") which carries the master password entered by the user. This new event, when received in the backend, executes the ImportDump method of the Db package which performs the work of reading and restoring the data in the DB from the backup file that the user has selected. As a result, a new event (\\\"imported_data\\\") is emitted, which carries the result (successful or unsuccessful) of its execution as attached data. The frontend, when it receives the event, only has to perform 2 tasks:
- if the result was successful, set the language that was saved in the backup file and show a modal indicating the success of the action
- if for whatever reason the import could not be done, show the error and its cause.
All of this is much easier to see in the code logic than to explain with words ?:
/* Setting.svelte */...It is worth mentioning that the Wails runtime function that registers listeners on the frontend (EventsOn) returns a function, which when called cancels said listener. It is convenient to cancel said listeners when the component is destroyed. Similarly to React the onMount hook can \\\"clean up\\\" said listeners by making them return a cleanup function that, in this case, will call all the functions returned by EventsOn that we have taken the precaution of saving in separate variables:
/* Setting.svelte */... // canceling listeners return () => { cancelSavedAs(); cancelEnterPassword(); cancelImportedData(); };...To finish this review of the frontend part of our application, it only remains to say something about the About component. This has little logic since it is limited to displaying information about the application as is usual in an about. It should be said, however, that, as we can see, the view shows a link to the application repository. Obviously, in a normal web page an anchor tag () would make us navigate to the corresponding link, but in a desktop application this would not happen if Wails did not have a specific function (BrowserOpenURL) for this in its runtime:
/* About.svelte */... BrowserOpenURL(\\\"https://github.com/emarifer/Nu-i-uita\\\")} >III - A few words about building the Wails app
If you want to build the application executable by packaging everything, including the application icon and all assets (fonts, images, etc.) just run the command:
$ wails buildThis will build the binary into the build/bin folder. However, for choosing other build options or performing cross-compiling, you may want to take a look at the Wails CLI documentation.
For this application, I think I already mentioned it in the first part of this series, I have only focused on the compilation for Windows and Linux. To perform these tasks (which, due to testing, are repetitive) in a comfortable way I have created some small scripts and a Makefile that \\\"coordinates\\\" them.
The make create-bundles command creates for the Linux version a .tar.xz compressed file with the application and a Makefile that acts as an \\'installer\\' that installs the executable, a desktop entry to create an entry in the Start Menu and the corresponding application icon. For the Windows version, the binary is simply compressed as a .zip inside a folder called dist/.However, if you prefer a cross-platform automated build, Wails has a Github Actions that allows you to upload (default option) the generated artifacts to your repository.
Note that if you use the make create-bundles command when running it, it will call the Wails commands wails build -clean -upx (in the case of Linux) or wails build -skipbindings -s -platform windows/amd64 -upx (in the case of Windows). The -upx flag refers to the compression of the binary using the UPX utility that you should have installed on your computer. Part of the secret of the small size of the executable is due to the magnificent compression job that this utility does.
Finally, note that the build scripts automatically add the current repository tag to the About view and after the build restore its value to the default (DEV_VERSION).
Phew! These 2 posts ended up being longer than I thought! But I hope you liked them and, above all, that they help you think about new projects. Learning something in programming works like that…
Remember that you can find all the application code in this GitHub repository.
I\\'m sure I\\'ll see you in other posts. Happy coding ?!!
极简密码管理器桌面应用程序:进军 Golang 的 Wails 框架(第 2 部分)
发布于2024-12-23浏览:409
Hi again, coders! In the first part of this short series we saw the creation and operation of a desktop application to store and encrypt our passwords made with the Wails framework. We also made a description of the Go backend and how we bind it to the frontend side.
In this part, we are going to deal with the user interface. As we stated in that post, Wails allows us to use any web framework we like, even Vanilla JS, to build our GUI. As I said, it seems that the creators of Wails have a preference for Svelte, because they always mention it as their first choice. The Wails CLI (in its current version) when we ask to create a project with Svelte Typescript (wails init -n myproject -t svelte-ts) generates the scaffolding with Svelte3. As I already told you, if you prefer to use Svelte5 (and its new features) I have a bash script that automates its creation (in any case, you have to have the Wails CLI installed). In addition, it adds Taildwindcss Daisyui which seems to me a perfect combination for the interface design.
The truth is that I had worked first with Vanilla Js and Vue, then with React, and even with that strange library that for many is HTMX (which I have to say that I love ❤️). But Svelte makes you fall in love from the beginning, and I have to say that it was while experimenting with Wails that I used it for the first time (and I promise to continue using it…). But as comfortable as a web framework is, we must remind backend developers that the frontend is not that easy ?!!
But let's get to the point.
I - A look at the frontend structure
If you have used any web framework, you will quickly recognize that the Wails CLI uses ViteJs under the hood:
... . ├── index.html ├── package.json ├── package.json.md5 ├── package-lock.json ├── postcss.config.js ├── README.md ├── src │ ├── App.svelte │ ├── assets │ │ ├── fonts │ │ │ ├── nunito-v16-latin-regular.woff2 │ │ │ └── OFL.txt │ │ └── images │ │ └── logo-universal.png │ ├── lib │ │ ├── BackBtn.svelte │ │ ├── BottomActions.svelte │ │ ├── EditActions.svelte │ │ ├── EntriesList.svelte │ │ ├── Language.svelte │ │ ├── popups │ │ │ ├── alert-icons.ts │ │ │ └── popups.ts │ │ ├── ShowPasswordBtn.svelte │ │ └── TopActions.svelte │ ├── locales │ │ ├── en.json │ │ └── es.json │ ├── main.ts │ ├── pages │ │ ├── About.svelte │ │ ├── AddPassword.svelte │ │ ├── Details.svelte │ │ ├── EditPassword.svelte │ │ ├── Home.svelte │ │ ├── Login.svelte │ │ └── Settings.svelte │ ├── style.css │ └── vite-env.d.ts ├── svelte.config.js ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── wailsjs ├── go │ ├── main │ │ ├── App.d.ts │ │ └── App.js │ └── models.ts └── runtime ├── package.json ├── runtime.d.ts └── runtime.js ...If you have used any web framework generated by Vite you will not be surprised by its configuration files. Here I use Svelte5 (plus the configuration of Taildwindcss Daisyui) which is what generates my own bash script, as I have already told you. We also use TypeScript, which will facilitate the development of the frontend, so you can also see its configurations.
But the important thing in this explanation is the content of the wailsjs folder. This is where the compilation done by Wails has done its magic. The go subfolder is where the methods "translated" to Js/Ts of the backend part that has to interact with the frontend are stored. For example, in main/App.js (or its TypeScript version, main/App.d.ts) there are all the exported (public) methods of the App structure:
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT import {models} from '../models'; export function AddPasswordEntry(arg1:string,arg2:string,arg3:string):Promise; export function CheckMasterPassword(arg1:string):Promise ; export function DeleteEntry(arg1:string):Promise ; export function Drop():Promise ; export function GetAllEntries():Promise >; export function GetEntryById(arg1:string):Promise ; export function GetLanguage():Promise ; export function GetMasterPassword():Promise ; export function GetPasswordCount():Promise ; export function SaveLanguage(arg1:string):Promise ; export function SaveMasterPassword(arg1:string):Promise ; export function UpdateEntry(arg1:models.PasswordEntry):Promise ; All of them return a promise. If the promise "wraps" some Go structure used as return type or the respective function takes an argument type, there will be a module (models.ts, typed in this case, because we use TypeScript) that contains the class corresponding to the Go structure and its constructor in a namespace.
Additionally, the runtime subfolder contains all the methods from Go's runtime package that allow us to manipulate the window and events listened to or emitted from or to the backend, respectively.
The src folder contains the files that will be compiled by Vite to save them in "frontend/dist" (and embedded in the final executable), as in any web application. Note that, since we use Tailwindcss, style.css contains the basic Tailwind configuration plus any CSS classes we need to use. Also, as an advantage of using web technology for the interface, we can easily use one or more fonts (folder assets/fonts) or exchange them.
To finish with this overview, note that when we compile in development mode (wails dev), in addition to allowing us to hot reloading, we can not only observe the changes made (both in the backend and in the frontend) in the application window itself, but also in a web browser through the address http://localhost:34115, since a webserver is started. This allows you to use your favorite browser development extensions. Although it must be said that Wails himself provides us with some very useful dev tools, when we right-click on the application window (only in development mode) and choose "Inspect Element":
II - And now… a dive into HTML, CSS and JavaScript ?
/* package.json */ ... }, "dependencies": { "svelte-copy": "^2.0.0", "svelte-i18n": "^4.0.1", "svelte-spa-router": "^4.0.1", "sweetalert2": "^11.14.5" } ...As you can see, there are 4 JavaScript packages I've added to Svelte (apart from the already mentioned Tailwindcss Daisyui):
- svelte-copy, to make it easier to copy username and password to clipboard.
- svelte-i18n, for i18n handling, i.e. allowing the user to change the application's language.
- svelte-spa-router, a small routing library for Svelte, which makes it easier to change views in the application window, since it's not worth it, in this case, to use the "official" routing provided by SvelteKit.
- sweetalert2, basically use it to create modals/dialog boxes easily and quickly.
The entry point of every SPA is the main.js (or main.ts) file, so let's start with that:
/* main.ts */ import { mount } from 'svelte' import './style.css' import App from './App.svelte' import { addMessages, init } from "svelte-i18n"; // ⇐ ⇐ import en from './locales/en.json'; // ⇐ ⇐ import es from './locales/es.json'; // ⇐ ⇐ addMessages('en', en); // ⇐ ⇐ addMessages('es', es); // ⇐ ⇐ init({ fallbackLocale: 'en', // ⇐ ⇐ initialLocale: 'en', // ⇐ ⇐ }); const app = mount(App, { target: document.getElementById('app')!, }) export default appI've highlighted the things I've added to the skeleton generated by the Wails CLI. The svelte-i18n library requires that JSON files containing translations be registered in the main.js/ts file, at the same time as setting the fallback/initial language (although as we'll see, that will be manipulated later based on what the user has selected as their preferences). The JSON files containing the translations are in the format:
/* frontend/src/locales/en.json */ { "language": "Language", "app_title": "Nu-i uita • minimalist password store", "select_directory": "Select the directory where to save the data export", "select_file": "Select the backup file to import", "master_password": "Master Password ?", "generate": "Generate", "insert": "Insert", "login": "Login", ... } /* frontend/src/locales/es.json */ { "language": "Idioma", "app_title": "Nu-i uita • almacén de contraseñas minimalista", "select_directory": "Selecciona el directorio donde guardar los datos exportados", "select_file": "Selecciona el archivo de respaldo que deseas importar", "master_password": "Contraseña Maestra ?", "generate": "Generar", "insert": "Insertar", "login": "Inciar sesión", ... }I find this library's system to be easy and convenient for facilitating translations of Svelte applications (you can consult its documentation for more details):
{$_("app_title")} You can also use sites like this one, which will help you translate JSON files into different languages. However, the problem is that when you fill your .svelte files with $format you have to manually keep track of them, which is tedious and error-prone. I don't know of any way to automate this task, if anyone knows, I'd be interested if you'd let me know ?… Otherwise, I'd have to think of some kind of script to do that job.
The next step in building the interface, as in any Svelte application, is the App.svelte file:
/* App.svelte */3 things to comment here. The first is the use of the svelte-spa-router library (for more details see its doc here). For the simple purpose of changing views in a desktop application, this library more than fulfills its purpose. With the 7 views or pages we create a dictionary (or JavaScript object) that associates routes with views. Then this dictionary is passed as props to the Router component of svelte-spa-router. It's that simple. As we will see later, through programmatic navigation or through user action we can easily change views.
The other thing is that I added a little gadget: when the user presses the Escape key the application closes (on the Settings page a tip clarifies to the user that this key closes the application). Svelte actually makes the job a lot easier, because this simple line:
catches the Keyboard event from the DOM triggering the execution of the onKeyDown function which in turn emits a Wails event (which we call "quit") which is listened to in the backend and when received there, closes the application. Since App.svelte is the component that encompasses the entire application, this is the right place to put the code for this action. The last thing to clarify is why the HTML main tag carries Tailwind's overflow-hidden utility class. Since we're going to use an animation where components appear to enter from the right, which momentarily "increases" the width of the window, overflow-hidden prevents an ugly horizontal scrollbar from appearing.
The first view the user sees when opening the application is the Login view/page. Its logic is similar to that of a login page in any web application. Let's first look at the logic used for the views animations, because it is the same as that followed on the rest of the pages:
/* Login.svelte */ {#if mounted}The animation requires declaring a variable (state, let mount = false;) which is initially set to false. When the component is mounted, a lifecycle hook (onMount, similar to React) sets it to true and the animation can now begin. An entrance from the right of 1200 milliseconds duration is used (in:fly={{ x: 75, duration: 1200 }}) and a fade (out:fade={{ duration: 200 }}) of 200 milliseconds duration. Simple thanks to Svelte.
When setting up the Login view we also need to know if the user is already registered in the database or if it is the first time he/she enters the application:
/* Login.svelte */ ... let isLogin = false; ... onMount(() => { GetMasterPassword().then((result) => { isLogin = result; // console.log("Master password exists in DB:", isLogin); }); ... };Here we make use of GetMasterPassword which is a binding generated automatically when compiling the application and which was declared as a public method of the struct App (see the first part of this series). This function queries the database and, in case there is a master password registered in it, it considers the user as already registered (it returns a promise that wraps a boolean value), asking him to enter said password to allow him access to the rest of the views. If there is no master password in the database, the user is considered as "new" and what is asked is that he generates his own password to enter the application for the first time.
Finally, when mounting the Login.svelte component we do something that is important for the rest of the application. Although the svelte-i18n library forces us to declare the initial language code, as we have already seen, when mounting Login.svelte we ask the database (using the GetLanguage binding) to check if there is a language code saved. In case the database returns an empty string, that is, if there is no language configured as the user's preference, svelte-i18n will use the value configured as initialLocale. If instead there is a language configured, that language will be set (locale.set(result);) and the "change_titles" event will be emitted, to which the translated titles of the title bar and native dialogs of the app will be passed for the backend to handle:
/* Login.svelte */The following is the logic for handling the login:
/* Login.svelte */Simply put: newPassword, the state bound to the input that gets what the user types, is first checked by onLogin to see if it has at least 6 characters and that all of them are ASCII characters, i.e. they are only 1 byte long (see the reason for that in part I of this series) by this little function const isAscii = (str: string): boolean => /^[\x00-\x7F] $/.test(str);. If the check fails the function returns and displays a warning toast to the user. Afterwards, if there is no master password saved in the database (isLogin = false), whatever the user types is saved by the SaveMasterPassword function (a binding generated by Wails); If the promise is resolved successfully (returns a uuid string as the Id of the record stored in the database), the user is taken to the home view by the svelte-spa-router library's push method. Conversely, if the password passes the check for length and absence of non-ASCII characters and there is a master password in the DB (isLogin = true) then the CheckMasterPassword function verifies its identity against the stored one and either takes the user to the home view (promise resolved with true) or a toast is shown indicating that the entered password was incorrect.
The central view of the application and at the same time the most complex is the home view. Its HTML is actually subdivided into 3 components: a top button bar with a search input (TopActions component), a bottom button bar (BottomActions component) and a central area where the total number of saved password entries or the list of these is displayed using a scrollable window (EntriesList component):
/* Home.svelte */ {#if mounted}Let's take a look at the TopActions and EntriesList components since they are both very closely related. And they are, especially since their props are states of the parent component. This is where that new feature of Svelte5 comes into play: runes. Both components take props declared with the $bindable rune; this means that data can also flow up from child to parent. A diagram may make it clearer:
For example, in the TopActions component if the user clicks on the "Entries list" button, this is executed:
/* TopActions.svelte */ onclick={() => { search = ""; // is `searchTerms` in the parent component isEntriesList = !isEntriesList; // is `showList` in the parent component }}That is, it makes the search state (searchTerms) an empty string, so that if there are any search terms it is reset and thus the entire list is shown. And on the other hand, it toggles the showList state (props isEntriesList in TopActions) so that the parent component shows or hides the list.
As we can see in the diagram above, both child components share the same props with the parent's searchTerms state. The TopActions component captures the input from the user and passes it as state to the parent component Home, which in turn passes it as props to its child component EntriesList.
The main logic of displaying the full list or a list filtered by the search terms entered by the user is carried out, as expected, by the EntriesList component:
/* EntriesList.svelte */As we said, 2 props are received (listCounter and search) and a state is maintained (let entries: models.PasswordEntry[] = $state([]);). When the component is mounted at the user's request, the backend is asked for the complete list of saved password entries. If there are no search terms, they are stored in the state; if there are, a simple filtering of the obtained array is performed and it is saved in the state:
/* EntriesList.svelte */ ... onMount(() => { GetAllEntries().then((result) => { // console.log("SEARCH:", search); if (search) { const find = search.toLowerCase(); entries = result.filter( (item) => item.Username.toLowerCase().includes(find) || item.Website.toLowerCase().includes(find), ); } else { entries = result; } }); }); ...In the displayed list, the user can perform 2 actions. The first is to display the details of the entry, which is carried out when he clicks on the corresponding button: onclick={() => push(`/details/${entry.Id}`)}. Basically, we call the push method of the routing library to take the user to the details view, but passing the Id parameter corresponding to the item in question.
The other action the user can perform is to delete an item from the list. If he clicks on the corresponding button, he will be shown a confirmation popup, calling the showAlert function. This function in turn calls showWarning, which is actually an abstraction layer over the sweetalert2 library (all the functions that call the sweetalert2 library are in frontend/src/lib/popups/popups.ts). If the user confirms the deletion action, the DeleteEntry binding is called (to delete it from the DB) and, in turn, if the promise it returns is resolved, deleteItem is called (to delete it from the array stored in the entries state):
/* EntriesList.svelte */ ... const showAlert = (website: string, id: string) => { const data: string[] = [ `${$_("alert_deleting_password")} "${website}."`, `${$_("alert_confirm_deleting")}`, ]; showWarning(data).then((result) => { if (result.value) { DeleteEntry(id).then(() => deleteItem(id)); showSuccess($_("deletion_confirm_msg")); } }); }; const deleteItem = (id: string): void => { let itemIdx = entries.findIndex((x) => x.Id === id); entries.splice(itemIdx, 1); entries = entries; GetPasswordCount().then((result) => (listCounter = result)); };The other component of the Home view (BottomActions) is much simpler: it does not receive props and is limited to redirecting the user to various views (Settings, About or AddPassword).
The AddPassword and EditPassword views share very similar logic and are similar to the Login view as well. Both do not allow the user to enter spaces at the beginning and end of what they typed in the text input and follow the same policy as the Login view of requiring passwords to be at least 6 ASCII characters long. Basically, what sets them apart is that they call the Wails-generated links relevant to the action they need to perform:
/* AddPassword.svelte */ ... AddPasswordEntry(website, username, newPassword).then((result) => { result ? push("/home") : false; }); ... /* EditPassword.svelte */ ... UpdateEntry(entry).then((result) => { result ? push("/home") : false; }); ...The other view that is somewhat complex is Settings. This has a Language component that receives as props languageName from its parent component (Settings):
/* Language.svelte */ ...The HTML for this component is a single select that handles the user's language choice. In its onchange event it receives a function (handleChange) that does 3 things:
- sets the language on the frontend using the svelte-i18n library
- emits an event ("change_titles") so that the Wails runtime changes the title of the application's title bar and the titles of the Select Directory and Select File dialog boxes in relation to the previous action
- saves the language selected by the user in the DB so that the next time the application is started it will open configured with that language.
Returning to the Settings view, its entire operation is governed by a series of events that are sent and received to or from the backend. The simplest of all is the Quit button: when the user clicks on it, a quit event is triggered and listened to in the backend and the application closes (onclick={() => EventsEmit("quit")}). A tip informs the user that the Escape key (shortcut) performs the same action, as we already explained.
The reset button calls a function that displays a popup window:
/* Setting.svelte */ ... const showAlert = () => { let data: string[] = [ `${$_("alert_delete_all")}`, `${$_("alert_confirm_deleting")}`, ]; showWarning(data).then((result) => { if (result.value) { Drop().then(() => push("/")); showSuccess($_("alert_delete_confirm_msg")); } }); }; ...If the user accepts the action, the Drop binding is called, which cleans all the collections in the DB, and if the promise it returns is resolved, it sends the user to the Login view, showing a modal indicating the success of the action.
The other two actions that remain are similar to each other, so let's look at Import Data.
If the user clicks on the corresponding button, an event is emitted (onclick={() => EventsEmit("import_data")}) which is listened for in the backend. When received, the native Select File dialog box is opened to allow the user to select the backup file. If the user chooses the file, the variable containing the path (fileLocation) will not contain an empty string and this will trigger an event in the backend ("enter_password") which is now listened for in the frontend to, in turn, display a new popup window asking for the master password used when the export was made. Again, the frontend will emit another event ("password") which carries the master password entered by the user. This new event, when received in the backend, executes the ImportDump method of the Db package which performs the work of reading and restoring the data in the DB from the backup file that the user has selected. As a result, a new event ("imported_data") is emitted, which carries the result (successful or unsuccessful) of its execution as attached data. The frontend, when it receives the event, only has to perform 2 tasks:
- if the result was successful, set the language that was saved in the backup file and show a modal indicating the success of the action
- if for whatever reason the import could not be done, show the error and its cause.
All of this is much easier to see in the code logic than to explain with words ?:
/* Setting.svelte */ ...It is worth mentioning that the Wails runtime function that registers listeners on the frontend (EventsOn) returns a function, which when called cancels said listener. It is convenient to cancel said listeners when the component is destroyed. Similarly to React the onMount hook can "clean up" said listeners by making them return a cleanup function that, in this case, will call all the functions returned by EventsOn that we have taken the precaution of saving in separate variables:
/* Setting.svelte */ ... // canceling listeners return () => { cancelSavedAs(); cancelEnterPassword(); cancelImportedData(); }; ...To finish this review of the frontend part of our application, it only remains to say something about the About component. This has little logic since it is limited to displaying information about the application as is usual in an about. It should be said, however, that, as we can see, the view shows a link to the application repository. Obviously, in a normal web page an anchor tag () would make us navigate to the corresponding link, but in a desktop application this would not happen if Wails did not have a specific function (BrowserOpenURL) for this in its runtime:
/* About.svelte */ ... BrowserOpenURL("https://github.com/emarifer/Nu-i-uita")} >III - A few words about building the Wails app
If you want to build the application executable by packaging everything, including the application icon and all assets (fonts, images, etc.) just run the command:
$ wails buildThis will build the binary into the build/bin folder. However, for choosing other build options or performing cross-compiling, you may want to take a look at the Wails CLI documentation.
For this application, I think I already mentioned it in the first part of this series, I have only focused on the compilation for Windows and Linux. To perform these tasks (which, due to testing, are repetitive) in a comfortable way I have created some small scripts and a Makefile that "coordinates" them.
The make create-bundles command creates for the Linux version a .tar.xz compressed file with the application and a Makefile that acts as an 'installer' that installs the executable, a desktop entry to create an entry in the Start Menu and the corresponding application icon. For the Windows version, the binary is simply compressed as a .zip inside a folder called dist/.However, if you prefer a cross-platform automated build, Wails has a Github Actions that allows you to upload (default option) the generated artifacts to your repository.
Note that if you use the make create-bundles command when running it, it will call the Wails commands wails build -clean -upx (in the case of Linux) or wails build -skipbindings -s -platform windows/amd64 -upx (in the case of Windows). The -upx flag refers to the compression of the binary using the UPX utility that you should have installed on your computer. Part of the secret of the small size of the executable is due to the magnificent compression job that this utility does.
Finally, note that the build scripts automatically add the current repository tag to the About view and after the build restore its value to the default (DEV_VERSION).
Phew! These 2 posts ended up being longer than I thought! But I hope you liked them and, above all, that they help you think about new projects. Learning something in programming works like that…
Remember that you can find all the application code in this GitHub repository.
I'm sure I'll see you in other posts. Happy coding ?!!
版本声明 本文转载于:https://dev.to/emarifer/a-minimalist-password-manager-desktop-app-a-foray-into-golangs-wails-framework-part-2-2inn?1如有侵犯,请联系[email protected]删除最新教程 更多>
左连接为何在右表WHERE子句过滤时像内连接?左JOIN CONUNDRUM:WITCHING小时在数据库Wizard的领域中变成内在的加入很有趣,当将c.foobar条件放置在上面的Where子句中时,据说左联接似乎会转换为内部连接。仅当满足A.Foo和C.Foobar标准时,才会返回结果。为什么要变形?关键在于其中的子句。当左联接的右侧值...编程 发布于2025-05-10
为什么我在Silverlight Linq查询中获得“无法找到查询模式的实现”错误?查询模式实现缺失:解决“无法找到”错误在Silverlight应用程序中,尝试使用LINQ建立LINQ连接以错误而实现的数据库”,无法找到查询模式的实现。”当省略LINQ名称空间或查询类型缺少IEnumerable 实现时,通常会发生此错误。 解决问题来验证该类型的质量是至关重要的。在此特定实例中...编程 发布于2025-05-10
如何解决由于Android的内容安全策略而拒绝加载脚本... \”错误?Unveiling the Mystery: Content Security Policy Directive ErrorsEncountering the enigmatic error "Refused to load the script..." when deployi...编程 发布于2025-05-10
如何简化PHP中的JSON解析以获取多维阵列?php 试图在PHP中解析JSON数据的JSON可能具有挑战性,尤其是在处理多维数组时。 To simplify the process, it's recommended to parse the JSON as an array rather than an object.To do...编程 发布于2025-05-10
在JavaScript中如何并发运行异步操作并正确处理错误?同意操作execution 在执行asynchronous操作时,相关的代码段落会遇到一个问题,当执行asynchronous操作:此实现在启动下一个操作之前依次等待每个操作的完成。要启用并发执行,需要进行修改的方法。 第一个解决方案试图通过获得每个操作的承诺来解决此问题,然后单独等待它们: co...编程 发布于2025-05-10
如何避免Go语言切片时的内存泄漏?,a [j:] ...虽然通常有效,但如果使用指针,可能会导致内存泄漏。这是因为原始的备份阵列保持完整,这意味着新切片外部指针引用的任何对象仍然可能占据内存。 copy(a [i:] 对于k,n:= len(a)-j i,len(a); k编程 发布于2025-05-10
如何有效地选择熊猫数据框中的列?在处理数据操作任务时,在Pandas DataFrames 中选择列时,选择特定列的必要条件是必要的。在Pandas中,选择列的各种选项。选项1:使用列名 如果已知列索引,请使用ILOC函数选择它们。请注意,python索引基于零。 df1 = df.iloc [:,0:2]#使用索引0和1 c...编程 发布于2025-05-10
为什么PYTZ最初显示出意外的时区偏移?与pytz 最初从pytz获得特定的偏移。例如,亚洲/hong_kong最初显示一个七个小时37分钟的偏移: 差异源利用本地化将时区分配给日期,使用了适当的时区名称和偏移量。但是,直接使用DateTime构造器分配时区不允许进行正确的调整。 example pytz.timezone(...编程 发布于2025-05-10
反射动态实现Go接口用于RPC方法探索在GO 使用反射来实现定义RPC式方法的界面。例如,考虑一个接口,例如:键入myService接口{ 登录(用户名,密码字符串)(sessionId int,错误错误) helloworld(sessionid int)(hi String,错误错误) } 替代方案而不是依靠反射...编程 发布于2025-05-10
为什么在我的Linux服务器上安装Archive_Zip后,我找不到“ class \” class \'ziparchive \'错误?Class 'ZipArchive' Not Found Error While Installing Archive_Zip on Linux ServerSymptom:When attempting to run a script that utilizes the ZipAr...编程 发布于2025-05-10
C++中如何将独占指针作为函数或构造函数参数传递?在构造函数和函数中将唯一的指数管理为参数 unique pointers( unique_ptr [2启示。通过值: base(std :: simelor_ptr n) :next(std :: move(n)){} 此方法将唯一指针的所有权转移到函数/对象。指针的内容被移至功能中,在操作...编程 发布于2025-05-10
CSS强类型语言解析您可以通过其强度或弱输入的方式对编程语言进行分类的方式之一。在这里,“键入”意味着是否在编译时已知变量。一个例子是一个场景,将整数(1)添加到包含整数(“ 1”)的字符串: result = 1 "1";包含整数的字符串可能是由带有许多运动部件的复杂逻辑套件无意间生成的。它也可以是故意从单个真理...编程 发布于2025-05-10
Async Void vs. Async Task在ASP.NET中:为什么Async Void方法有时会抛出异常?在ASP.NET async void void async void void void void void的设计无需返回asynchroncon而无需返回任务对象。他们在执行过程中增加未偿还操作的计数,并在完成后减少。在某些情况下,这种行为可能是有益的,例如未期望或明确预期操作结果的火灾和...编程 发布于2025-05-10
用户本地时间格式及时区偏移显示指南在用户的语言环境格式中显示日期/时间,并使用时间偏移在向最终用户展示日期和时间时,以其localzone and格式显示它们至关重要。这确保了不同地理位置的清晰度和无缝用户体验。以下是使用JavaScript实现此目的的方法。方法:推荐方法是处理客户端的Javascript中的日期/时间格式化和时...编程 发布于2025-05-10
如何将PANDAS DataFrame列转换为DateTime格式并按日期过滤?Transform Pandas DataFrame Column to DateTime FormatScenario:Data within a Pandas DataFrame often exists in various formats, including strings.使用时间数据时...编程 发布于2025-05-10学习中文
- 1 走路用中文怎么说?走路中文发音,走路中文学习
- 2 坐飞机用中文怎么说?坐飞机中文发音,坐飞机中文学习
- 3 坐火车用中文怎么说?坐火车中文发音,坐火车中文学习
- 4 坐车用中文怎么说?坐车中文发音,坐车中文学习
- 5 开车用中文怎么说?开车中文发音,开车中文学习
- 6 游泳用中文怎么说?游泳中文发音,游泳中文学习
- 7 骑自行车用中文怎么说?骑自行车中文发音,骑自行车中文学习
- 8 你好用中文怎么说?你好中文发音,你好中文学习
- 9 谢谢用中文怎么说?谢谢中文发音,谢谢中文学习
- 10 How to say goodbye in Chinese? 再见Chinese pronunciation, 再见Chinese learning
免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。
Copyright© 2022 湘ICP备2022001581号-3