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 日に公開ブラウズ:340Hi 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] までご連絡ください。最新のチュートリアル もっと>
macOS 上の Django で「ImproperlyConfigured: MySQLdb モジュールのロード中にエラーが発生しました」を修正する方法?MySQL の不適切な構成: 相対パスの問題Django で python manage.py runserver を実行すると、次のエラーが発生する場合があります:ImproperlyConfigured: Error loading MySQLdb module: dlopen(/Library...プログラミング 2024 年 12 月 23 日に公開 Go で WebSocket を使用してリアルタイム通信を行うチャット アプリケーション、ライブ通知、共同作業ツールなど、リアルタイムの更新が必要なアプリを構築するには、従来の HTTP よりも高速でインタラクティブな通信方法が必要です。そこで WebSocket が登場します。今日は、アプリケーションにリアルタイム機能を追加できるように、Go で WebSo...プログラミング 2024 年 12 月 23 日に公開 データ挿入時の「一般エラー: 2006 MySQL サーバーが消えました」を修正するにはどうすればよいですか?レコードの挿入中に「一般エラー: 2006 MySQL サーバーが消えました」を解決する方法はじめに:MySQL データベースにデータを挿入すると、「一般エラー: 2006 MySQL サーバーが消えました。」というエラーが発生することがあります。このエラーは、通常、MySQL 構成内の 2 つの変...プログラミング 2024 年 12 月 23 日に公開 MySQL を使用して今日が誕生日のユーザーを見つけるにはどうすればよいですか?MySQL を使用して今日の誕生日を持つユーザーを識別する方法MySQL を使用して今日がユーザーの誕生日かどうかを判断するには、誕生日が一致するすべての行を検索する必要があります。今日の日付。これは、UNIX タイムスタンプとして保存されている誕生日と今日の日付を比較する単純な MySQL クエリ...プログラミング 2024 年 12 月 23 日に公開 一意の ID を保持し、重複した名前を処理しながら、PHP で 2 つの連想配列を結合するにはどうすればよいですか?PHP での連想配列の結合PHP では、2 つの連想配列を 1 つの配列に結合するのが一般的なタスクです。次のリクエストを考えてみましょう:問題の説明:提供されたコードは 2 つの連想配列 $array1 と $array2 を定義します。目標は、両方の配列のすべてのキーと値のペアを統合する新しい配...プログラミング 2024 年 12 月 23 日に公開 Bootstrap 4 ベータ版の列オフセットはどうなりましたか?Bootstrap 4 ベータ: 列オフセットの削除と復元Bootstrap 4 は、ベータ 1 リリースで、その方法に大幅な変更を導入しました。列がオフセットされました。ただし、その後の Beta 2 リリースでは、これらの変更は元に戻されました。offset-md-* から ml-autoBoo...プログラミング 2024 年 12 月 23 日に公開 「if」ステートメントを超えて: 明示的な「bool」変換を伴う型をキャストせずに使用できる場所は他にありますか?キャストなしで bool へのコンテキスト変換が可能クラスは bool への明示的な変換を定義し、そのインスタンス 't' を条件文で直接使用できるようにします。ただし、この明示的な変換では、キャストなしで bool として 't' を使用できる場所はどこですか?コン...プログラミング 2024 年 12 月 23 日に公開 Angular HTTP POST 値が PHP で定義されていないのはなぜですか?それを修正するにはどうすればよいですか?PHP への Angular HTTP POST: 未定義の POST 値の処理AngularJS で PHP エンドポイントに対して HTTP POST リクエストを実行すると、未定義の値が発生する場合がありますサーバー側の POST 値。これは、予期されるデータ形式と、Angular アプリケー...プログラミング 2024 年 12 月 23 日に公開 最初の標準入力ストリームにアクセスできますか?Go では、初期標準入力にアクセスできますか?Go では、os.Stdin を使用して元の標準入力から読み取ると、次のとおり望ましい結果が得られます。このコード スニペットによって:package main import "os" import "log" i...プログラミング 2024 年 12 月 23 日に公開 シンプルなパスワード マネージャー デスクトップ アプリ: Golang の Wails フレームワークへの進出 (パート 2)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...プログラミング 2024 年 12 月 23 日に公開 ES6 React コンポーネント: クラスベースと関数型をいつ使用するか?ES6 クラスベースと機能的な ES6 React コンポーネントのどちらを使用するかを決定するReact を使用する場合、開発者は ES6 クラスベースを使用するかどうかの選択に直面します。コンポーネントまたは機能的な ES6 コンポーネント。最適なアプリ開発には、タイプごとに適切なユースケースを...プログラミング 2024 年 12 月 23 日に公開 PHP で 2 つのフラット配列間の一意の値を見つけるにはどうすればよいですか?フラット配列間の一意の値の検索2 つの配列が与えられた場合、タスクはそれらの 1 つにのみ存在する値を決定することです。この操作は、2 つのセット間の差分を見つけることとして一般に知られています。PHP では、array_merge、array_diff、および array_diff 関数を利用して...プログラミング 2024 年 12 月 23 日に公開 CSS はインラインブロック要素内に改行をネイティブに挿入できますか?CSS インラインブロック要素内での改行の挿入: 理論的探求進化し続ける Web 開発環境において、コンテンツの流れを操作することが依然として最も重要です。頻繁に発生する特定の課題の 1 つは、インライン ブロック要素内での改行の挿入です。次の HTML 構造を考慮してください:<h3 id=...プログラミング 2024 年 12 月 23 日に公開 PHP でタイムゾーン間で時刻と日付を簡単に変換するにはどうすればよいですか?PHP でタイムゾーン間の時刻と日付を変換するPHP を使用すると、異なるタイムゾーン間で時刻と日付を簡単に変換できます。この機能は、グローバル データを処理するアプリケーションや、さまざまな場所のユーザーと作業する場合に特に役立ちます。タイム ゾーン オフセットの取得GMT からの時刻オフセットを...プログラミング 2024 年 12 月 23 日に公開 Windows で Python パッケージ管理用の Pip をインストールして使用する方法?Pip: Windows に Python パッケージをインストールする手間のかからない方法Windows に Python パッケージをインストールするのは、特に次の場合は困難な作業になることがあります。 EasyInstall で問題が発生しました。幸いなことに、EasyInstall の後継で...プログラミング 2024 年 12 月 23 日に公開中国語を勉強する
- 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