{$_(\\\"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:

\\\"A

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 build

This 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 ?!!

","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"}}

極簡密碼管理器桌面應用程式:進軍 Golang 的 Wails 框架(第 2 部分)

發佈於2024-12-23
瀏覽:680

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":

A minimalist password manager desktop app: a foray into Golang

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 app

I'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:

A minimalist password manager desktop app: a foray into Golang

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 build

This 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]刪除
最新教學 更多>
  • 如何在 PHP 中組合兩個關聯數組,同時保留唯一 ID 並處理重複名稱?
    如何在 PHP 中組合兩個關聯數組,同時保留唯一 ID 並處理重複名稱?
    在 PHP 中組合關聯數組在 PHP 中,將兩個關聯數組組合成一個數組是常見任務。考慮以下請求:問題描述:提供的代碼定義了兩個關聯數組,$array1 和 $array2。目標是建立一個新陣列 $array3,它合併兩個陣列中的所有鍵值對。 此外,提供的陣列具有唯一的 ID,而名稱可能重疊。要求是建...
    程式設計 發佈於2024-12-23
  • Bootstrap 4 Beta 中的列偏移發生了什麼事?
    Bootstrap 4 Beta 中的列偏移發生了什麼事?
    Bootstrap 4 Beta:列偏移的刪除和恢復Bootstrap 4 在其Beta 1 版本中引入了重大更改柱子偏移了。然而,隨著 Beta 2 的後續發布,這些變化已經逆轉。 從 offset-md-* 到 ml-auto在 Bootstrap 4 Beta 1 中, offset-md-*...
    程式設計 發佈於2024-12-23
  • 插入資料時如何修復「常規錯誤:2006 MySQL 伺服器已消失」?
    插入資料時如何修復「常規錯誤:2006 MySQL 伺服器已消失」?
    插入記錄時如何解決「一般錯誤:2006 MySQL 伺服器已消失」介紹:將資料插入MySQL 資料庫有時會導致錯誤「一般錯誤:2006 MySQL 伺服器已消失」。當與伺服器的連線遺失時會出現此錯誤,通常是由於 MySQL 配置中的兩個變數之一所致。 解決方案:解決此錯誤的關鍵是調整wait_tim...
    程式設計 發佈於2024-12-23
  • 在 Go 中使用 WebSocket 進行即時通信
    在 Go 中使用 WebSocket 進行即時通信
    构建需要实时更新的应用程序(例如聊天应用程序、实时通知或协作工具)需要一种比传统 HTTP 更快、更具交互性的通信方法。这就是 WebSockets 发挥作用的地方!今天,我们将探讨如何在 Go 中使用 WebSocket,以便您可以向应用程序添加实时功能。 在这篇文章中,我们将介绍: WebSoc...
    程式設計 發佈於2024-12-23
  • HTML 格式標籤
    HTML 格式標籤
    HTML 格式化元素 **HTML Formatting is a process of formatting text for better look and feel. HTML provides us ability to format text without us...
    程式設計 發佈於2024-12-23
  • 大批
    大批
    方法是可以在物件上呼叫的 fns 數組是對象,因此它們在 JS 中也有方法。 slice(begin):將陣列的一部分提取到新數組中,而不改變原始數組。 let arr = ['a','b','c','d','e']; // Usecase: Extract till index ...
    程式設計 發佈於2024-12-23
  • 儘管程式碼有效,為什麼 POST 請求無法擷取 PHP 中的輸入?
    儘管程式碼有效,為什麼 POST 請求無法擷取 PHP 中的輸入?
    解決PHP 中的POST 請求故障在提供的程式碼片段中:action=''而非:action="<?php echo $_SERVER['PHP_SELF'];?>";?>"檢查$_POST陣列:表單提交後使用 var_dump 檢查 $_POST 陣列的內...
    程式設計 發佈於2024-12-23
  • 為什麼我的 Angular HTTP POST 值在 PHP 中未定義,如何修復它?
    為什麼我的 Angular HTTP POST 值在 PHP 中未定義,如何修復它?
    Angular HTTP POST 到PHP:處理未定義的POST 值在AngularJS 中,對PHP 端點執行HTTP POST 請求有時會導致未定義的值伺服器端的POST 值。當預期資料格式與 Angular 應用程式傳送的實際資料不符時,就會發生這種情況。 要解決此問題,確保正確設定 Con...
    程式設計 發佈於2024-12-23
  • Go可以存取初始標準輸入流嗎?
    Go可以存取初始標準輸入流嗎?
    在 Go 中,您可以存取初始標準輸入嗎? 在 Go 中,使用 os.Stdin 從原始標準輸入讀取應該會產生所需的結果,如圖所示通過這個代碼片段:package main import "os" import "log" import "io&quo...
    程式設計 發佈於2024-12-23
  • 極簡密碼管理器桌面應用程式:進軍 Golang 的 Wails 框架(第 2 部分)
    極簡密碼管理器桌面應用程式:進軍 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 React 元件:何時使用基於類別與函數式?
    在ES6 基於類別和函數式ES6 React 元件之間做出選擇使用React 時,開發人員面臨著使用ES6 基於類別的選擇組件或功能ES6 組件。了解每種類型的適當用例對於最佳應用程式開發至關重要。 函數式 ES6 元件:無狀態且簡單函數式元件是無狀態的,這表示它們不維護任何內部狀態。他們只是接收道...
    程式設計 發佈於2024-12-23
  • 如何在 PHP 中找到兩個平面數組之間的唯一值?
    如何在 PHP 中找到兩個平面數組之間的唯一值?
    在平面數組之間查找唯一值給定兩個數組,任務是確定僅存在於其中一個數組中的值。此操作通常稱為尋找兩個集合之間的差異。 在 PHP 中,您可以利用 array_merge、array_diff 和 array_diff 函數來實現此操作。詳細解法如下:$array1 = [64, 98, 112, 92...
    程式設計 發佈於2024-12-23
  • CSS 可以在內聯區塊元素中本機插入換行符號嗎?
    CSS 可以在內聯區塊元素中本機插入換行符號嗎?
    CSS 在行內區塊元素中插入換行符:理論探索在不斷發展的Web 開發領域,這種能力操縱內容流仍然是最重要的。經常出現的一個特殊挑戰涉及在內聯區塊元素中插入換行符。 考慮以下 HTML 結構:<h3 id="features">Features</h3> &...
    程式設計 發佈於2024-12-23
  • 如何在 PHP 中輕鬆轉換時區之間的時間和日期?
    如何在 PHP 中輕鬆轉換時區之間的時間和日期?
    在PHP 中轉換時區之間的時間和日期使用PHP,您可以輕鬆地在不同時區之間轉換時間和日期。此功能在處理全球資料的應用程式或與來自不同位置的使用者一起工作時特別有用。 取得時區偏移量要取得與 GMT 的時間偏移量,您可以使用 DateTimeZone 類別。它提供了時區及其各自偏移量的完整清單。 $t...
    程式設計 發佈於2024-12-23
  • 如何在Windows上安裝並使用Pip進行Python套件管理?
    如何在Windows上安裝並使用Pip進行Python套件管理?
    Pip:在Windows 上安裝Python 套件的輕鬆方式在Windows 上安裝Python 套件可能是一項艱鉅的任務,特別是如果您在使用EasyInstall 時遇到困難。幸運的是,EasyInstall 的後繼者 Pip 提供了更簡化和簡化的解決方案。 在Windows 上逐步安裝Pip若要...
    程式設計 發佈於2024-12-23

免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。

Copyright© 2022 湘ICP备2022001581号-3