├── prebuild
└── index.mjs
├── src
├── renderer
│ ├── i18n
│ │ ├── bs.json
│ │ ├── no.json
│ │ ├── config.ts
│ │ ├── si.json
│ │ ├── cs.json
│ │ ├── sv.json
│ │ ├── es.json
│ │ ├── el.json
│ │ ├── en.json
│ │ ├── it.json
│ │ ├── de.json
│ │ ├── ru.json
│ │ ├── fr.json
│ │ └── ta.json
│ ├── index.tsx
│ ├── preload.d.ts
│ ├── components
│ │ ├── ui
│ │ │ ├── loading.tsx
│ │ │ ├── nav.tsx
│ │ │ ├── typography.tsx
│ │ │ ├── trend-arrow.tsx
│ │ │ ├── label.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── input.tsx
│ │ │ ├── button-popover.tsx
│ │ │ ├── button.tsx
│ │ │ ├── toggle-switch.tsx
│ │ │ ├── select.tsx
│ │ │ └── form.tsx
│ │ ├── tranisition-provider.tsx
│ │ └── theme-provider.tsx
│ ├── index.ejs
│ ├── hooks
│ │ ├── session.ts
│ │ └── useGlucoseAlerts.ts
│ ├── custom.css
│ ├── layouts
│ │ ├── base-layout.tsx
│ │ ├── public-layout.tsx
│ │ └── settings-layout.tsx
│ ├── routes
│ │ └── index.tsx
│ ├── pages
│ │ ├── landing.tsx
│ │ └── settings
│ │ │ ├── account.tsx
│ │ │ ├── alert.tsx
│ │ │ └── general.tsx
│ ├── app.tsx
│ ├── lib
│ │ ├── AudioManager.ts
│ │ ├── utils.ts
│ │ └── linkup.ts
│ ├── stores
│ │ ├── auth.ts
│ │ └── alertStore.ts
│ ├── globals.css
│ └── config
│ │ └── app.ts
├── __tests__
│ └── App.test.tsx
└── main
│ ├── util.ts
│ ├── logoutHandler.ts
│ ├── refreshHandler.ts
│ ├── windowMode.ts
│ ├── preload.ts
│ ├── trayHandler.ts
│ ├── windowState.ts
│ ├── windowHandler.ts
│ └── alertHandler.ts
├── .erb
├── mocks
│ └── fileMock.js
├── img
│ └── erb-logo.png
├── configs
│ ├── .eslintrc
│ ├── webpack.config.eslint.ts
│ ├── webpack.paths.ts
│ ├── webpack.config.base.ts
│ ├── webpack.config.main.dev.ts
│ ├── webpack.config.renderer.dev.dll.ts
│ ├── webpack.config.preload.dev.ts
│ ├── webpack.config.main.prod.ts
│ ├── webpack.config.renderer.prod.ts
│ └── webpack.config.renderer.dev.ts
└── scripts
│ ├── .eslintrc
│ ├── clean.js
│ ├── check-node-env.js
│ ├── check-port-in-use.js
│ ├── link-modules.ts
│ ├── delete-source-maps.js
│ ├── electron-rebuild.js
│ ├── check-build-exists.ts
│ ├── notarize.js
│ └── check-native-dep.js
├── .github
├── FUNDING.yml
├── SECURITY.md
├── dependabot.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── CONTRIBUTING.md
├── workflows
│ ├── codeql-analysis.yml
│ ├── codacy-analysis.yml
│ ├── generate-update-flatpak-sources.yml
│ ├── build-and-release.yml
│ └── build-pipeline.yml
├── SUPPORT.md
├── SETUP.md
├── UPDATING-FLATHUB-RELEASE.md
└── FLATHUB_BUILD_SYNC_QUICKSTART.md
├── assets
├── icon.ico
├── icon.png
├── logo.png
├── icon.icns
├── tray-logo.png
├── sounds
│ └── alert.mp3
├── tray-logo-16.png
├── appx
│ ├── StoreLogo.png
│ ├── Square150x150Logo.png
│ ├── Square44x44Logo.png
│ └── Wide310x150Logo.png
├── tray-logo-16@2x.png
├── LibreLinkUpDesktop_SnapBanner.png
├── entitlements.mac.plist
├── assets.d.ts
└── logo.svg
├── .vscode
├── extensions.json
├── settings.json
└── launch.json
├── snap
└── gui
│ ├── librelinkupdesktop.png
│ └── librelinkupdesktop.desktop
├── postcss.config.js
├── .editorconfig
├── .gitattributes
├── flathub
├── rocks.poopjournal.librelinkupdesktop.desktop
└── rocks.poopjournal.librelinkupdesktop.metainfo.xml
├── components.json
├── release
└── app
│ ├── package-lock.json
│ └── package.json
├── .eslintignore
├── tsconfig.json
├── INTRODUCTION.md
├── .eslintrc.js
├── tailwind.config.js
├── snapcraft.yaml
├── .gitignore
└── README.md
/prebuild/index.mjs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/renderer/i18n/bs.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/.erb/mocks/fileMock.js:
--------------------------------------------------------------------------------
1 | export default 'test-file-stub';
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: ["https://poopjournal.rocks/blog/donate/"]
2 |
--------------------------------------------------------------------------------
/assets/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Crazy-Marvin/LibreLinkUpDesktop/HEAD/assets/icon.ico
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Crazy-Marvin/LibreLinkUpDesktop/HEAD/assets/icon.png
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Crazy-Marvin/LibreLinkUpDesktop/HEAD/assets/logo.png
--------------------------------------------------------------------------------
/assets/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Crazy-Marvin/LibreLinkUpDesktop/HEAD/assets/icon.icns
--------------------------------------------------------------------------------
/assets/tray-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Crazy-Marvin/LibreLinkUpDesktop/HEAD/assets/tray-logo.png
--------------------------------------------------------------------------------
/.erb/img/erb-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Crazy-Marvin/LibreLinkUpDesktop/HEAD/.erb/img/erb-logo.png
--------------------------------------------------------------------------------
/assets/sounds/alert.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Crazy-Marvin/LibreLinkUpDesktop/HEAD/assets/sounds/alert.mp3
--------------------------------------------------------------------------------
/assets/tray-logo-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Crazy-Marvin/LibreLinkUpDesktop/HEAD/assets/tray-logo-16.png
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["dbaeumer.vscode-eslint", "EditorConfig.EditorConfig"]
3 | }
4 |
--------------------------------------------------------------------------------
/assets/appx/StoreLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Crazy-Marvin/LibreLinkUpDesktop/HEAD/assets/appx/StoreLogo.png
--------------------------------------------------------------------------------
/assets/tray-logo-16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Crazy-Marvin/LibreLinkUpDesktop/HEAD/assets/tray-logo-16@2x.png
--------------------------------------------------------------------------------
/assets/appx/Square150x150Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Crazy-Marvin/LibreLinkUpDesktop/HEAD/assets/appx/Square150x150Logo.png
--------------------------------------------------------------------------------
/assets/appx/Square44x44Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Crazy-Marvin/LibreLinkUpDesktop/HEAD/assets/appx/Square44x44Logo.png
--------------------------------------------------------------------------------
/assets/appx/Wide310x150Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Crazy-Marvin/LibreLinkUpDesktop/HEAD/assets/appx/Wide310x150Logo.png
--------------------------------------------------------------------------------
/snap/gui/librelinkupdesktop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Crazy-Marvin/LibreLinkUpDesktop/HEAD/snap/gui/librelinkupdesktop.png
--------------------------------------------------------------------------------
/assets/LibreLinkUpDesktop_SnapBanner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Crazy-Marvin/LibreLinkUpDesktop/HEAD/assets/LibreLinkUpDesktop_SnapBanner.png
--------------------------------------------------------------------------------
/.erb/configs/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "no-console": "off",
4 | "global-require": "off",
5 | "import/no-dynamic-require": "off"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.erb/configs/webpack.config.eslint.ts:
--------------------------------------------------------------------------------
1 | /* eslint import/no-unresolved: off, import/no-self-import: off */
2 |
3 | module.exports = require('./webpack.config.renderer.dev').default;
4 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | /* eslint global-require: off, import/no-extraneous-dependencies: off */
2 |
3 | module.exports = {
4 | plugins: [require('tailwindcss'), require('autoprefixer')],
5 | };
6 |
--------------------------------------------------------------------------------
/.github/SECURITY.md:
--------------------------------------------------------------------------------
1 | # LibreLinkUpDesktop Security
2 | Please report (suspected) security vulnerabilities to marvin@poopjournal.rocks.
3 | It would be great if you could prepare a patch too.
4 | Thanks!
5 |
--------------------------------------------------------------------------------
/.erb/scripts/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "no-console": "off",
4 | "global-require": "off",
5 | "import/no-dynamic-require": "off",
6 | "import/no-extraneous-dependencies": "off"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/renderer/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 | import App from './app'
3 |
4 | const container = document.getElementById('root') as HTMLElement
5 | const root = createRoot(container)
6 | root.render()
7 |
--------------------------------------------------------------------------------
/src/renderer/preload.d.ts:
--------------------------------------------------------------------------------
1 | import { ElectronHandler } from 'main/preload'
2 |
3 | declare global {
4 | // eslint-disable-next-line no-unused-vars
5 | interface Window {
6 | electron: ElectronHandler
7 | }
8 | }
9 |
10 | export {}
11 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text eol=lf
2 | *.exe binary
3 | *.png binary
4 | *.jpg binary
5 | *.jpeg binary
6 | *.ico binary
7 | *.icns binary
8 | *.eot binary
9 | *.otf binary
10 | *.ttf binary
11 | *.woff binary
12 | *.woff2 binary
13 |
--------------------------------------------------------------------------------
/flathub/rocks.poopjournal.librelinkupdesktop.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Type=Application
3 | Name=LibreLinkUpDesktop
4 | Comment=Fetches your blood sugar from LibreLinkUp
5 | Icon=rocks.poopjournal.librelinkupdesktop
6 | Exec=run.sh
7 | Categories=Utility
8 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "npm"
4 | directory: "/"
5 | schedule:
6 | interval: "monthly"
7 |
8 | - package-ecosystem: "github-actions"
9 | directory: "/"
10 | schedule:
11 | interval: "monthly"
12 |
--------------------------------------------------------------------------------
/src/__tests__/App.test.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import { render } from '@testing-library/react';
3 | import App from '../renderer/app';
4 |
5 | describe('App', () => {
6 | it('should render', () => {
7 | expect(render()).toBeTruthy();
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/src/renderer/components/ui/loading.tsx:
--------------------------------------------------------------------------------
1 | export function LoadingScreen() {
2 | return (
3 |
6 | )
7 | }
8 |
--------------------------------------------------------------------------------
/src/renderer/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 | LibreLinkUpDesktop
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/snap/gui/librelinkupdesktop.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Version=0.1.15
3 | Name=LibreLinkUpDesktop
4 | Comment=This is a desktop application that fetches your blood sugar from LibreLinkUp
5 | Exec=${SNAP}/librelinkupdesktop/librelinkupdesktop --no-sandbox
6 | Icon=${SNAP}/meta/gui/librelinkupdesktop.png
7 | Terminal=false
8 | Type=Application
9 | Categories=Utility;Health;
10 |
--------------------------------------------------------------------------------
/.erb/scripts/clean.js:
--------------------------------------------------------------------------------
1 | import { rimrafSync } from 'rimraf';
2 | import fs from 'fs';
3 | import webpackPaths from '../configs/webpack.paths';
4 |
5 | const foldersToRemove = [
6 | webpackPaths.distPath,
7 | webpackPaths.buildPath,
8 | webpackPaths.dllPath,
9 | ];
10 |
11 | foldersToRemove.forEach((folder) => {
12 | if (fs.existsSync(folder)) rimrafSync(folder);
13 | });
14 |
--------------------------------------------------------------------------------
/assets/entitlements.mac.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.cs.allow-unsigned-executable-memory
6 |
7 | com.apple.security.cs.allow-jit
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/renderer/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/lib/utils"
15 | }
16 | }
--------------------------------------------------------------------------------
/src/renderer/components/ui/nav.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react"
2 |
3 | type Props = {
4 | icon: ReactNode
5 | label: string
6 | onClick: () => void
7 | }
8 |
9 | export function NavButton({ icon, label, onClick }: Props) {
10 | return (
11 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/.erb/scripts/check-node-env.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk';
2 |
3 | export default function checkNodeEnv(expectedEnv) {
4 | if (!expectedEnv) {
5 | throw new Error('"expectedEnv" not set');
6 | }
7 |
8 | if (process.env.NODE_ENV !== expectedEnv) {
9 | console.log(
10 | chalk.whiteBright.bgRed.bold(
11 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config`
12 | )
13 | );
14 | process.exit(2);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/release/app/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "librelinkupdesktop",
3 | "version": "0.1.15",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "librelinkupdesktop",
9 | "version": "0.1.15",
10 | "hasInstallScript": true,
11 | "license": "Apache-2.0",
12 | "funding": {
13 | "type": "individual",
14 | "url": "https://poopjournal.rocks/blog/donate/"
15 | }
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/renderer/hooks/session.ts:
--------------------------------------------------------------------------------
1 | import { sendLogout } from "@/lib/utils"
2 | import { useAuthStore } from "@/stores/auth"
3 | import { useNavigate } from "react-router-dom"
4 |
5 | export function useClearSession() {
6 | const navigate = useNavigate()
7 | const logout = useAuthStore((state) => state.logout)
8 |
9 | const clearSession = () => {
10 | logout()
11 | navigate('/')
12 | sendLogout();
13 | }
14 |
15 | return {
16 | clearSession
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/util.ts:
--------------------------------------------------------------------------------
1 | /* eslint import/prefer-default-export: off */
2 | import { URL } from "url"
3 | import path from "path"
4 |
5 | export function resolveHtmlPath(htmlFileName: string) {
6 | if (process.env.NODE_ENV === 'development') {
7 | const port = process.env.PORT || 1212
8 | const url = new URL(`http://localhost:${port}`)
9 | url.pathname = htmlFileName
10 | return url.href
11 | }
12 | return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`
13 | }
14 |
--------------------------------------------------------------------------------
/src/renderer/i18n/no.json:
--------------------------------------------------------------------------------
1 | {
2 | "Welcome": "Dette er et skrivebordsprogram som henter blodsukkeret ditt fra LibreLinkUp",
3 | "Settings": "Innstillinger",
4 | "General": "Generell",
5 | "Account": "Regnskap",
6 |
7 | "First Name": "Fornavn",
8 | "Last Name": "Etternavn",
9 |
10 | "System": "System",
11 | "Dark": "Mørk",
12 | "Light": "Lys",
13 |
14 | "English": "Engelsk",
15 | "Sinhala": "Singalesisk",
16 | "Norwegian": "Norsk",
17 |
18 | "Germany": "Tyskland"
19 | }
20 |
--------------------------------------------------------------------------------
/.erb/scripts/check-port-in-use.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk';
2 | import detectPort from 'detect-port';
3 |
4 | const port = process.env.PORT || '1212';
5 |
6 | detectPort(port, (err, availablePort) => {
7 | if (port !== String(availablePort)) {
8 | throw new Error(
9 | chalk.whiteBright.bgRed.bold(
10 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 npm start`
11 | )
12 | );
13 | } else {
14 | process.exit(0);
15 | }
16 | });
17 |
--------------------------------------------------------------------------------
/src/main/logoutHandler.ts:
--------------------------------------------------------------------------------
1 | import { BrowserWindow, ipcMain } from "electron"
2 |
3 | export const registerLogoutHandler = () => {
4 | ipcMain.on('logout', () => {
5 | BrowserWindow.getAllWindows().forEach(window => {
6 | if (window.webContents) {
7 | window.webContents.send('logout-event');
8 | if (!window.isPrimary) {
9 | window.close();
10 | }
11 | }
12 | });
13 | });
14 | }
15 |
16 | export const destroyLogoutHandler = () => {
17 | ipcMain.removeAllListeners('logout');
18 | }
19 |
--------------------------------------------------------------------------------
/.erb/scripts/link-modules.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import webpackPaths from '../configs/webpack.paths';
3 |
4 | const { srcNodeModulesPath, appNodeModulesPath, erbNodeModulesPath } =
5 | webpackPaths;
6 |
7 | if (!fs.existsSync(srcNodeModulesPath) && fs.existsSync(appNodeModulesPath)) {
8 | fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, 'junction');
9 | }
10 |
11 | if (!fs.existsSync(erbNodeModulesPath) && fs.existsSync(appNodeModulesPath)) {
12 | fs.symlinkSync(appNodeModulesPath, erbNodeModulesPath, 'junction');
13 | }
14 |
--------------------------------------------------------------------------------
/.erb/scripts/delete-source-maps.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 | import { rimrafSync } from 'rimraf';
4 | import webpackPaths from '../configs/webpack.paths';
5 |
6 | export default function deleteSourceMaps() {
7 | if (fs.existsSync(webpackPaths.distMainPath))
8 | rimrafSync(path.join(webpackPaths.distMainPath, '*.js.map'), {
9 | glob: true,
10 | });
11 | if (fs.existsSync(webpackPaths.distRendererPath))
12 | rimrafSync(path.join(webpackPaths.distRendererPath, '*.js.map'), {
13 | glob: true,
14 | });
15 | }
16 |
--------------------------------------------------------------------------------
/src/renderer/custom.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | * {
7 | @apply border-border;
8 | }
9 |
10 | body {
11 | @apply bg-background text-foreground;
12 | }
13 |
14 | ::-webkit-scrollbar {
15 | @apply w-2;
16 | @apply h-2;
17 | }
18 |
19 | ::-webkit-scrollbar-track {
20 | @apply bg-foreground/20;
21 | }
22 |
23 | ::-webkit-scrollbar-thumb {
24 | @apply rounded bg-foreground/40;
25 | }
26 |
27 | ::-webkit-scrollbar-thumb:hover {
28 | @apply bg-foreground/60;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Coverage directory used by tools like istanbul
11 | coverage
12 | .eslintcache
13 |
14 | # Dependency directory
15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
16 | node_modules
17 |
18 | # OSX
19 | .DS_Store
20 |
21 | release/app/dist
22 | release/build
23 | .erb/dll
24 |
25 | .idea
26 | npm-debug.log.*
27 | *.css.d.ts
28 | *.sass.d.ts
29 | *.scss.d.ts
30 |
31 | # eslint ignores hidden directories by default:
32 | # https://github.com/eslint/eslint/issues/8429
33 | !.erb
34 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "incremental": true,
4 | "target": "es2021",
5 | "module": "commonjs",
6 | "lib": ["dom", "es2021"],
7 | "jsx": "react-jsx",
8 | "strict": true,
9 | "sourceMap": true,
10 | "baseUrl": "./src",
11 | "paths": {
12 | "@/*": ["./renderer/*"]
13 | },
14 | "moduleResolution": "node",
15 | "esModuleInterop": true,
16 | "allowSyntheticDefaultImports": true,
17 | "resolveJsonModule": true,
18 | "allowJs": true,
19 | "outDir": ".erb/dll"
20 | },
21 | "exclude": ["test", "release/build", "release/app/dist", ".erb/dll"]
22 | }
23 |
--------------------------------------------------------------------------------
/src/renderer/components/ui/typography.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react"
2 | import { cn } from "@/lib/utils"
3 |
4 | type Props = {
5 | className?: string
6 | children: ReactNode
7 | icon?: ReactNode
8 | onClick?: () => void
9 | }
10 |
11 | export function Heading({ children, icon, className, onClick }: Props) {
12 | return (
13 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/.erb/scripts/electron-rebuild.js:
--------------------------------------------------------------------------------
1 | import { execSync } from 'child_process';
2 | import fs from 'fs';
3 | import { dependencies } from '../../release/app/package.json';
4 | import webpackPaths from '../configs/webpack.paths';
5 |
6 | if (
7 | Object.keys(dependencies || {}).length > 0 &&
8 | fs.existsSync(webpackPaths.appNodeModulesPath)
9 | ) {
10 | const electronRebuildCmd =
11 | '../../node_modules/.bin/electron-rebuild --force --types prod,dev,optional --module-dir .';
12 | const cmd =
13 | process.platform === 'win32'
14 | ? electronRebuildCmd.replace(/\//g, '\\')
15 | : electronRebuildCmd;
16 | execSync(cmd, {
17 | cwd: webpackPaths.appPath,
18 | stdio: 'inherit',
19 | });
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/refreshHandler.ts:
--------------------------------------------------------------------------------
1 | import { BrowserWindow, ipcMain } from "electron"
2 |
3 | export const registerRefreshHandler = () => {
4 | ipcMain.on('refresh-all', () => {
5 | BrowserWindow.getAllWindows().forEach(window => {
6 | if (window.webContents) {
7 | window.webContents.reload();
8 | }
9 | });
10 | });
11 |
12 | ipcMain.on('refresh-primary', () => {
13 | BrowserWindow.getAllWindows().forEach(window => {
14 | if (window.isPrimary && window.webContents) {
15 | window.webContents.reload();
16 | }
17 | });
18 | });
19 | }
20 |
21 | export const destroyRefreshHandler = () => {
22 | ipcMain.removeAllListeners('refresh-all');
23 | ipcMain.removeAllListeners('refresh-primary')
24 | }
25 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.associations": {
3 | ".eslintrc": "jsonc",
4 | ".prettierrc": "jsonc",
5 | ".eslintignore": "ignore"
6 | },
7 |
8 | "eslint.validate": [
9 | "javascript",
10 | "javascriptreact",
11 | "html",
12 | "typescriptreact"
13 | ],
14 |
15 | "javascript.validate.enable": false,
16 | "javascript.format.enable": false,
17 | "typescript.format.enable": false,
18 |
19 | "search.exclude": {
20 | ".git": true,
21 | ".eslintcache": true,
22 | ".erb/dll": true,
23 | "release/{build,app/dist}": true,
24 | "node_modules": true,
25 | "npm-debug.log.*": true,
26 | "test/**/__snapshots__": true,
27 | "package-lock.json": true,
28 | "*.{css,sass,scss}.d.ts": true
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/assets/assets.d.ts:
--------------------------------------------------------------------------------
1 | type Styles = Record;
2 |
3 | declare module '*.svg' {
4 | import React = require('react');
5 |
6 | export const ReactComponent: React.FC>;
7 |
8 | const content: string;
9 | export default content;
10 | }
11 |
12 | declare module '*.png' {
13 | const content: string;
14 | export default content;
15 | }
16 |
17 | declare module '*.jpg' {
18 | const content: string;
19 | export default content;
20 | }
21 |
22 | declare module '*.scss' {
23 | const content: Styles;
24 | export default content;
25 | }
26 |
27 | declare module '*.sass' {
28 | const content: Styles;
29 | export default content;
30 | }
31 |
32 | declare module '*.css' {
33 | const content: Styles;
34 | export default content;
35 | }
36 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Electron: Main",
6 | "type": "node",
7 | "request": "launch",
8 | "protocol": "inspector",
9 | "runtimeExecutable": "npm",
10 | "runtimeArgs": ["run", "start"],
11 | "env": {
12 | "MAIN_ARGS": "--inspect=5858 --remote-debugging-port=9223"
13 | }
14 | },
15 | {
16 | "name": "Electron: Renderer",
17 | "type": "chrome",
18 | "request": "attach",
19 | "port": 9223,
20 | "webRoot": "${workspaceFolder}",
21 | "timeout": 15000
22 | }
23 | ],
24 | "compounds": [
25 | {
26 | "name": "Electron: All",
27 | "configurations": ["Electron: Main", "Electron: Renderer"]
28 | }
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/src/renderer/components/ui/trend-arrow.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ArrowDownIcon,
3 | ArrowRightIcon,
4 | ArrowUpIcon,
5 | ArrowTopRightIcon,
6 | ArrowBottomRightIcon
7 | } from "@radix-ui/react-icons"
8 |
9 | type Props = {
10 | className: string
11 | trend: number
12 | }
13 |
14 | export function TrendArrow({ className, trend }: Props) {
15 | switch(trend)
16 | {
17 | case 1:
18 | return ()
19 | case 2:
20 | return ()
21 |
22 | case 4:
23 | return ()
24 | case 5:
25 | return ()
26 |
27 | default:
28 | return ()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/.erb/scripts/check-build-exists.ts:
--------------------------------------------------------------------------------
1 | // Check if the renderer and main bundles are built
2 | import path from 'path';
3 | import chalk from 'chalk';
4 | import fs from 'fs';
5 | import webpackPaths from '../configs/webpack.paths';
6 |
7 | const mainPath = path.join(webpackPaths.distMainPath, 'main.js');
8 | const rendererPath = path.join(webpackPaths.distRendererPath, 'renderer.js');
9 |
10 | if (!fs.existsSync(mainPath)) {
11 | throw new Error(
12 | chalk.whiteBright.bgRed.bold(
13 | 'The main process is not built yet. Build it by running "npm run build:main"'
14 | )
15 | );
16 | }
17 |
18 | if (!fs.existsSync(rendererPath)) {
19 | throw new Error(
20 | chalk.whiteBright.bgRed.bold(
21 | 'The renderer process is not built yet. Build it by running "npm run build:renderer"'
22 | )
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/renderer/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const labelVariants = cva(
8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
9 | )
10 |
11 | const Label = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef &
14 | VariantProps
15 | >(({ className, ...props }, ref) => (
16 |
21 | ))
22 | Label.displayName = LabelPrimitive.Root.displayName
23 |
24 | export { Label }
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 |
5 | ---
6 |
7 | ## :writing_hand: Describe the bug
8 |
9 |
10 | ## :bomb: Steps to reproduce
11 |
12 | 1. Go to '...'
13 | 2. Click on '....'
14 | 3. Scroll down to '....'
15 | 4. See error
16 |
17 | ## :wrench: Expected behavior
18 |
19 |
20 | ## :camera: Screenshots
21 |
22 |
23 | ## :iphone: Tech info
24 | - Device:
25 | - OS:
26 | - Version:
27 |
28 | ## :page_facing_up: Additional context
29 |
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 | ### :warning: Is your feature request related to a problem? Please describe.
8 |
9 |
10 | ### :bulb: Describe the solution you'd like.
11 |
12 |
13 | ### :bar_chart: Describe alternatives you've considered.
14 |
15 |
16 | ### :page_facing_up: Additional context
17 |
18 |
19 | ### :raising_hand: Do you want to develop this feature yourself?
20 |
21 | - [ ] Yes
22 | - [ ] No
23 |
--------------------------------------------------------------------------------
/src/renderer/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Separator = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(
10 | (
11 | { className, orientation = "horizontal", decorative = true, ...props },
12 | ref
13 | ) => (
14 |
25 | )
26 | )
27 | Separator.displayName = SeparatorPrimitive.Root.displayName
28 |
29 | export { Separator }
30 |
--------------------------------------------------------------------------------
/src/renderer/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/.erb/scripts/notarize.js:
--------------------------------------------------------------------------------
1 | const { notarize } = require('@electron/notarize');
2 | const { build } = require('../../package.json');
3 |
4 | exports.default = async function notarizeMacos(context) {
5 | const { electronPlatformName, appOutDir } = context;
6 | if (electronPlatformName !== 'darwin') {
7 | return;
8 | }
9 |
10 | if (process.env.CI !== 'true') {
11 | console.warn('Skipping notarizing step. Packaging is not running in CI');
12 | return;
13 | }
14 |
15 | if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) {
16 | console.warn(
17 | 'Skipping notarizing step. APPLE_ID and APPLE_ID_PASS env variables must be set'
18 | );
19 | return;
20 | }
21 |
22 | const appName = context.packager.appInfo.productFilename;
23 |
24 | await notarize({
25 | appBundleId: build.appId,
26 | appPath: `${appOutDir}/${appName}.app`,
27 | appleId: process.env.APPLE_ID,
28 | appleIdPassword: process.env.APPLE_ID_PASS,
29 | });
30 | };
31 |
--------------------------------------------------------------------------------
/src/main/windowMode.ts:
--------------------------------------------------------------------------------
1 | import { app } from 'electron';
2 | import fs from 'fs';
3 | import path from 'path';
4 |
5 | export class WindowModeManager {
6 |
7 | private filePath: string;
8 |
9 | constructor(private windowName: string) {
10 | const userDataPath = app.getPath('userData');
11 | this.filePath = path.join(userDataPath, `${windowName}-window-mode.json`);
12 | console.log('filePath', this.filePath);
13 | }
14 |
15 | getWindowMode(): 'overlay' | 'windowed' | 'overlayTransparent' {
16 | try {
17 | const data = fs.readFileSync(this.filePath, 'utf8');
18 | const parsed = JSON.parse(data);
19 | return parsed.windowMode;
20 | } catch (error) {
21 | return 'windowed';
22 | }
23 | }
24 |
25 | setWindowMode(mode: 'overlay' | 'windowed') {
26 | const data = JSON.stringify({ windowMode: mode }, null, 2);
27 | console.log('setWindowMode', this.filePath, data, mode);
28 | fs.writeFileSync(this.filePath, data);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/renderer/layouts/base-layout.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, useEffect } from "react"
2 | import { Toaster } from "sonner"
3 | import { useTranslation } from "react-i18next"
4 | import { ThemeProvider } from "@/components/theme-provider"
5 | import { MainTransition } from "@/components/tranisition-provider"
6 | import { cn } from "@/lib/utils"
7 | import { useAuthStore } from "@/stores/auth"
8 |
9 | type Props = {
10 | className?: string
11 | children: ReactNode
12 | }
13 |
14 | export function BaseLayout({ children, className }: Props) {
15 | const { i18n } = useTranslation()
16 | const language = useAuthStore((state) => state.language)
17 |
18 | useEffect(() => {
19 | i18n.changeLanguage(language)
20 | }, [])
21 |
22 | return (
23 |
24 |
25 |
26 |
27 | {children}
28 |
29 |
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/src/renderer/routes/index.tsx:
--------------------------------------------------------------------------------
1 | import { createMemoryRouter } from "react-router-dom"
2 | import LandingPage from "@/pages/landing"
3 | import LoginPage from "@/pages/login"
4 | import DashboardPage from "@/pages/dashboard"
5 | import SettingsGeneralPage from "@/pages/settings/general"
6 | import SettingsAccountPage from "@/pages/settings/account"
7 | import SettingsAlertPage from "@/pages/settings/alert"
8 |
9 | export default function routes(isloggedIn: boolean) {
10 | return createMemoryRouter([
11 | { path: "/", element: },
12 | { path: "/login", element: isloggedIn ? : },
13 | { path: "/dashboard", element: isloggedIn ? : },
14 | { path: "/settings/general", element: isloggedIn ? : },
15 | { path: "/settings/account", element: isloggedIn ? : },
16 | { path: "/settings/alert", element: isloggedIn ? : },
17 |
18 | ])
19 | }
20 |
--------------------------------------------------------------------------------
/src/renderer/pages/landing.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react"
2 | import { useNavigate } from "react-router-dom"
3 | import { PublicLayout } from "@/layouts/public-layout"
4 | import { clearRedirectTo, getRedirectTo } from "@/lib/utils"
5 | import logo from "../../../assets/logo.png"
6 |
7 | export default function LandingPage() {
8 | const navigate = useNavigate()
9 |
10 | useEffect(() => {
11 | const timer = setTimeout(async () => {
12 | const redirectTo = await getRedirectTo();
13 | if (redirectTo) {
14 | navigate(redirectTo)
15 | clearRedirectTo();
16 | return
17 | }
18 | navigate('/login')
19 | }, 3000);
20 | return () => clearTimeout(timer);
21 | }, [])
22 |
23 | return (
24 |
27 |
28 |

29 |
LibreLinkUpDesktop
30 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/src/renderer/layouts/public-layout.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, useEffect } from "react"
2 | import { ThemeProvider } from "@/components/theme-provider"
3 | import { useTranslation } from "react-i18next"
4 | import { MainTransition } from "@/components/tranisition-provider"
5 | import { cn } from "@/lib/utils"
6 | import { Toaster } from "sonner"
7 | import { useAuthStore } from "@/stores/auth"
8 |
9 | type Props = {
10 | className?: string
11 | children: ReactNode
12 | }
13 |
14 | export function PublicLayout({ children, className }: Props) {
15 | const { i18n } = useTranslation()
16 | const language = useAuthStore((state) => state.language)
17 |
18 | useEffect(() => {
19 | i18n.changeLanguage(language)
20 | }, [])
21 |
22 | return (
23 |
24 |
25 |
26 |
29 | {children}
30 |
31 |
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/src/renderer/components/tranisition-provider.tsx:
--------------------------------------------------------------------------------
1 | import { CSSProperties, ReactNode } from "react"
2 | import { motion } from "framer-motion"
3 |
4 | type MainTransitionProps = {
5 | children: ReactNode
6 | className?: string
7 | style?: CSSProperties | undefined
8 | }
9 |
10 | export function MainTransition({ children, className, style }: MainTransitionProps) {
11 | return (
12 |
19 | {children}
20 |
21 | )
22 | }
23 |
24 | type HeaderTransitionProps = {
25 | children: ReactNode
26 | }
27 |
28 | export function HeaderTransition({ children }: HeaderTransitionProps) {
29 | return (
30 |
36 | {children}
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/INTRODUCTION.md:
--------------------------------------------------------------------------------
1 | # LibreLinkUpDekstop
2 | Application to launcher the products the company offers.
3 |
4 | ## Building the application.
5 | Install NodeJS 20 on your machine. And goto the project root and run following commands.
6 | ```
7 | npm run install
8 | ```
9 | In order to generate the executables. Run following.
10 | ```
11 | npm run package
12 | ```
13 | To generate executables for all platforms run following.
14 | ```
15 | npm run package-all
16 | ```
17 |
18 | Note: Executables can be found on `release/build` folder
19 |
20 | ## Customizing the application
21 | - Localization is located on `src/renderer/i18n` folder.
22 | - App configuration can be found on `src/renderer/config` folder
23 |
24 |
25 | # Release History.
26 | Version 1.0.2
27 | - Fix the Ubuntu app label.
28 | - Fix app name typos.
29 | - Fix trend arrow.
30 | - Added norwegian language support.
31 |
32 | Version 1.0.1
33 | - Full localization on settings page.
34 | - Fix the color codes for dashboard.
35 | - Fix the arrow direction for the reading.
36 | - Fix the app label.
37 | - Fix the Typo on App Name.
38 | - Added persistance storage.
39 |
40 | Version 1.0.0
41 | - Initial Release
42 |
--------------------------------------------------------------------------------
/src/main/preload.ts:
--------------------------------------------------------------------------------
1 | // Disable no-unused-vars, broken for spread args
2 | /* eslint no-unused-vars: off */
3 | import { contextBridge, ipcRenderer, IpcRendererEvent } from "electron"
4 |
5 | export type Channels = string
6 |
7 | const electronHandler = {
8 | ipcRenderer: {
9 | invoke(channel: Channels, ...args: unknown[]): Promise {
10 | return ipcRenderer.invoke(channel, ...args);
11 | },
12 | sendMessage(channel: Channels, ...args: unknown[]) {
13 | ipcRenderer.send(channel, ...args)
14 | },
15 | on(channel: Channels, func: (...args: unknown[]) => void) {
16 | const subscription = (_event: IpcRendererEvent, ...args: unknown[]) =>
17 | func(...args)
18 | ipcRenderer.on(channel, subscription)
19 |
20 | return () => {
21 | ipcRenderer.removeListener(channel, subscription)
22 | }
23 | },
24 | once(channel: Channels, func: (...args: unknown[]) => void) {
25 | ipcRenderer.once(channel, (_event, ...args) => func(...args))
26 | },
27 | },
28 | }
29 |
30 | contextBridge.exposeInMainWorld('electron', electronHandler)
31 |
32 | export type ElectronHandler = typeof electronHandler
33 |
--------------------------------------------------------------------------------
/release/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "librelinkupdesktop",
3 | "version": "0.1.15",
4 | "description": "This is a desktop application that fetches your blood sugar from LibreLinkUp",
5 | "license": "Apache-2.0",
6 | "author": "Crazy Marvin & Contributors (especially Yuran) (https://crazymarvin.com/librelinkupdesktop/)",
7 | "homepage": "https://github.com/Crazy-Marvin/LibreLinkUpDesktop",
8 | "bugs": {
9 | "url": "https://github.com/Crazy-Marvin/LibreLinkUpDesktop/issues",
10 | "email": "marvin@poopjournal.rocks"
11 | },
12 | "funding": {
13 | "type": "individual",
14 | "url": "https://poopjournal.rocks/blog/donate/"
15 | },
16 | "keywords": [
17 | "diabetes",
18 | "librelink",
19 | "librelinkup",
20 | "blood sugar",
21 | "health",
22 | "desktop",
23 | "electron"
24 | ],
25 | "main": "./dist/main/main.js",
26 | "scripts": {
27 | "rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js",
28 | "postinstall": "npm run rebuild && npm run link-modules",
29 | "link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts"
30 | },
31 | "dependencies": {}
32 | }
33 |
--------------------------------------------------------------------------------
/src/renderer/app.tsx:
--------------------------------------------------------------------------------
1 | import { RouterProvider } from "react-router-dom"
2 | import { AnimatePresence } from "framer-motion"
3 | import { useAuthStore } from "@/stores/auth"
4 | import routes from "@/routes"
5 | import "@/globals.css"
6 | import "@/custom.css"
7 | import "@/i18n/config"
8 | import { useEffect } from "react"
9 | import { getWindowMode, setLocalStorageWindowMode } from "@/lib/utils";
10 |
11 | export default function App() {
12 | const token = useAuthStore((state) => state.token)
13 |
14 | useEffect(() => {
15 | const handleLogout = () => {
16 | window.location.reload();
17 | };
18 |
19 | const unsubscribe = window.electron.ipcRenderer.on('logout-event', handleLogout);
20 |
21 | return () => {
22 | unsubscribe();
23 | };
24 | }, []);
25 |
26 |
27 | useEffect(() => {
28 | async function fetchAndStoreWindowMode() {
29 | const mode = await getWindowMode();
30 | console.log('fetchWindowMode', mode);
31 |
32 | setLocalStorageWindowMode(mode);
33 | }
34 |
35 | fetchAndStoreWindowMode();
36 | }, []);
37 |
38 | return (
39 |
40 |
41 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/src/renderer/lib/AudioManager.ts:
--------------------------------------------------------------------------------
1 | export class AudioManager {
2 | private static instance: AudioManager;
3 | private audioInstance: HTMLAudioElement | null = null;
4 | private isPlaying: boolean = false;
5 |
6 | private constructor() {}
7 |
8 | public static getInstance(): AudioManager {
9 | if (!AudioManager.instance) {
10 | AudioManager.instance = new AudioManager();
11 | }
12 | return AudioManager.instance;
13 | }
14 |
15 | public async playAudio(audioFilePath: string): Promise {
16 | try {
17 |
18 | if (this.isPlaying) {
19 | console.log("Audio is already playing. Skipping...");
20 | return;
21 | }
22 |
23 | this.audioInstance = new Audio(audioFilePath);
24 |
25 | this.audioInstance.onplay = () => {
26 | this.isPlaying = true;
27 | console.log("Audio started playing.");
28 | };
29 |
30 | this.audioInstance.onended = () => {
31 | this.isPlaying = false;
32 | console.log("Audio playback finished.");
33 | };
34 |
35 | await this.audioInstance.play();
36 | } catch (err) {
37 | console.error("Error playing audio:", err);
38 | this.isPlaying = false;
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/trayHandler.ts:
--------------------------------------------------------------------------------
1 | import { ipcMain, BrowserWindow } from "electron";
2 | import { createTray, updateTrayNumber, destroyTray } from './../renderer/lib/trayManager';
3 |
4 | let mainWindow: BrowserWindow | null = null;
5 |
6 | export const registerTrayHandler = () => {
7 |
8 | ipcMain.on('update-tray-number', (event, number: number, unit: string, targetLow?: number, targetHigh?: number) => {
9 | try {
10 | updateTrayNumber(number, unit, targetLow, targetHigh);
11 | } catch (error) {
12 | console.error('Error updating tray number:', error);
13 | if (mainWindow) {
14 | createTray(mainWindow);
15 | updateTrayNumber(number, unit, targetLow, targetHigh);
16 | }
17 | }
18 | });
19 |
20 | ipcMain.on('create-tray', (event) => {
21 | if (mainWindow) {
22 | createTray(mainWindow);
23 | }
24 | });
25 |
26 | ipcMain.on('destroy-tray', (event) => {
27 | destroyTray();
28 | });
29 | };
30 |
31 | export const destroyTrayHandler = () => {
32 | ipcMain.removeAllListeners('update-tray-number');
33 | ipcMain.removeAllListeners('create-tray');
34 | ipcMain.removeAllListeners('destroy-tray');
35 | };
36 |
37 | export const setTrayMainWindow = (window: BrowserWindow) => {
38 | mainWindow = window;
39 | };
40 |
--------------------------------------------------------------------------------
/.erb/configs/webpack.paths.ts:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const rootPath = path.join(__dirname, '../..');
4 |
5 | const dllPath = path.join(__dirname, '../dll');
6 |
7 | const srcPath = path.join(rootPath, 'src');
8 | const srcMainPath = path.join(srcPath, 'main');
9 | const srcRendererPath = path.join(srcPath, 'renderer');
10 |
11 | const releasePath = path.join(rootPath, 'release');
12 | const appPath = path.join(releasePath, 'app');
13 | const appPackagePath = path.join(appPath, 'package.json');
14 | const appNodeModulesPath = path.join(appPath, 'node_modules');
15 | const srcNodeModulesPath = path.join(srcPath, 'node_modules');
16 |
17 | const distPath = path.join(appPath, 'dist');
18 | const distMainPath = path.join(distPath, 'main');
19 | const distRendererPath = path.join(distPath, 'renderer');
20 |
21 | const buildPath = path.join(releasePath, 'build');
22 |
23 | const erbNodeModulesPath = path.resolve(__dirname, '../../node_modules');
24 |
25 | export default {
26 | rootPath,
27 | dllPath,
28 | srcPath,
29 | srcMainPath,
30 | srcRendererPath,
31 | releasePath,
32 | appPath,
33 | appPackagePath,
34 | appNodeModulesPath,
35 | srcNodeModulesPath,
36 | distPath,
37 | distMainPath,
38 | distRendererPath,
39 | buildPath,
40 | erbNodeModulesPath,
41 | };
42 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to LibreLinkUpDesktop
2 |
3 | Thank you for choosing to contribute to our project!
4 | We appreciate your input and want to make contributing to this project
5 | as simple and transparent as possible, whether it's:
6 |
7 | - Reporting a bug
8 | - Discussing the current state of the code
9 | - Submitting a fix
10 | - Proposing new features
11 | - Becoming a maintainer
12 |
13 | Check out the
14 | [README](https://github.com/Crazy-Marvin/LibreLinkUpDesktop/blob/trunk/README.md)
15 | file for an overview of the project.
16 |
17 | ## Any contributions you make will be under the Apache License 2.0
18 |
19 | When you submit code changes, they are subject to the same
20 | [Apache License](https://www.apache.org/licenses/LICENSE-2.0)
21 | as the project.
22 | If you have any concerns, please contact the project maintainers.
23 |
24 | ## How to Contribute
25 |
26 | These are the steps you should take to contribute to this project;
27 |
28 | - Create an issue on the Github repository to discuss the proposed change.
29 | - Fork the repository and contribute to the forked repo.
30 | - Send a pull request to be reviewed.
31 |
32 | Following a successful PR review, the project maintainer will
33 | merge your contribution into the project.
34 |
35 | Hooray! You have successfully contributed to LibreLinkUpDesktop!
36 |
--------------------------------------------------------------------------------
/src/renderer/stores/auth.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand'
2 | import { persist, createJSONStorage } from 'zustand/middleware'
3 |
4 | type AuthStore = {
5 | token: string|null
6 | accountId: string|null
7 | country: string|null
8 | language: string|null
9 | resultUnit: string
10 | setCountry: (value: string) => void
11 | setLanguage: (value: string) => void
12 | setResultUnit: (value: string) => void
13 | login: (token: string, country: string, language: string, accountId: string) => void
14 | logout: () => void
15 | }
16 |
17 | const useAuthStore = create()(
18 | persist(
19 | (set) => ({
20 | token: null,
21 | accountId: null,
22 | country: null,
23 | language: null,
24 | resultUnit: 'mg/dL',
25 | login: (token: string, country: string, language: string, accountId: string) => set(() => ({ token, country, language, accountId })),
26 | logout: () => set(() => ({ token: null })),
27 | setCountry: (value) => set(() => ({ country: value })),
28 | setLanguage: (value) => set(() => ({ language: value })),
29 | setResultUnit: (value) => set(() => ({ resultUnit: value })),
30 | }), {
31 | name: 'auth-storage',
32 | storage: createJSONStorage(() => localStorage),
33 | },
34 | )
35 | )
36 |
37 | export {
38 | useAuthStore
39 | }
40 |
--------------------------------------------------------------------------------
/src/renderer/i18n/config.ts:
--------------------------------------------------------------------------------
1 | import i18next from "i18next"
2 | import { initReactI18next } from "react-i18next"
3 | import en from "./en.json"
4 | import si from "./si.json"
5 | import no from "./no.json"
6 | import de from "./de.json"
7 | import es from "./es.json"
8 | import cs from "./cs.json"
9 | import ru from "./ru.json"
10 | import bs from "./bs.json"
11 | import el from "./el.json"
12 | import fr from "./fr.json"
13 | import sv from "./sv.json"
14 | import it from "./it.json"
15 | import ta from "./ta.json"
16 |
17 | i18next.use(initReactI18next).init({
18 | // lng: 'en', // if you're using a language detector, do not define the lng option
19 | fallbackLng: "en",
20 | debug: true,
21 | resources: {
22 | en: {
23 | translation: en,
24 | },
25 | si: {
26 | translation: si,
27 | },
28 | no: {
29 | translation: no,
30 | },
31 | de: {
32 | translation: de,
33 | },
34 | es: {
35 | translation: es,
36 | },
37 | cs: {
38 | translation: cs,
39 | },
40 | ru: {
41 | translation: ru,
42 | },
43 | bs: {
44 | translation: bs,
45 | },
46 | el: {
47 | translation: el,
48 | },
49 | fr: {
50 | translation: fr,
51 | },
52 | sv: {
53 | translation: sv,
54 | },
55 | it: {
56 | translation: it,
57 | },
58 | ta: {
59 | translation: ta,
60 | },
61 | },
62 | })
63 |
--------------------------------------------------------------------------------
/src/renderer/components/ui/button-popover.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as Popover from '@radix-ui/react-popover';
3 | import { cn } from '@/lib/utils';
4 | import { Cross2Icon } from '@radix-ui/react-icons';
5 |
6 | interface ButtonPopoverChildren {
7 | trigger: React.ReactElement;
8 | content: React.ReactNode;
9 | }
10 |
11 | interface ButtonPopoverProps
12 | extends Omit<
13 | React.ComponentPropsWithoutRef,
14 | 'children'
15 | > {
16 | children: ButtonPopoverChildren;
17 | className?: string;
18 | contentProps?: React.ComponentPropsWithoutRef;
19 | }
20 |
21 | const ButtonPopover = React.forwardRef(
22 | ({ children, className, ...props }, ref) => (
23 |
24 | {children.trigger}
25 |
26 |
31 | {children.content}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | )
40 | );
41 |
42 | ButtonPopover.displayName = 'ButtonPopover';
43 |
44 | export { ButtonPopover };
45 |
--------------------------------------------------------------------------------
/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [development]
6 | pull_request:
7 | # The branches below must be a subset of the branches above
8 | branches: [trunk]
9 | schedule:
10 | - cron: '0 9 * * 4'
11 |
12 | jobs:
13 | analyse:
14 | name: Analyse
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - name: Checkout repository
19 | uses: actions/checkout@v5
20 | with:
21 | # We must fetch at least the immediate parents so that if this is
22 | # a pull request then we can checkout the head.
23 | fetch-depth: 2
24 |
25 | # Initializes the CodeQL tools for scanning.
26 | - name: Initialize CodeQL
27 | uses: github/codeql-action/init@v4
28 | # Override language selection by uncommenting this and choosing your languages
29 | # with:
30 | # languages: go, javascript, csharp, python, cpp, java
31 |
32 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
33 | # If this step fails, then you should remove it and run the build manually (see below)
34 | - name: Autobuild
35 | uses: github/codeql-action/autobuild@v4
36 |
37 | # ℹ️ Command-line programs to run using the OS shell.
38 | # 📚 https://git.io/JvXDl
39 |
40 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
41 | # and modify them (or add more) to build your code if your project
42 | # uses a compiled language
43 |
44 | #- run: |
45 | # make bootstrap
46 | # make release
47 |
48 | - name: Perform CodeQL Analysis
49 | uses: github/codeql-action/analyze@v4
50 |
--------------------------------------------------------------------------------
/.erb/configs/webpack.config.base.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Base webpack config used across other specific configs
3 | */
4 |
5 | import webpack from 'webpack';
6 | import TsconfigPathsPlugins from 'tsconfig-paths-webpack-plugin';
7 | import webpackPaths from './webpack.paths';
8 | import { dependencies as externals } from '../../release/app/package.json';
9 |
10 | const configuration: webpack.Configuration = {
11 | externals: [...Object.keys(externals || {})],
12 |
13 | stats: 'errors-only',
14 |
15 | module: {
16 | rules: [
17 | {
18 | test: /\.[jt]sx?$/,
19 | exclude: /node_modules/,
20 | use: {
21 | loader: 'ts-loader',
22 | options: {
23 | // Remove this line to enable type checking in webpack builds
24 | transpileOnly: true,
25 | compilerOptions: {
26 | module: 'esnext',
27 | },
28 | },
29 | },
30 | },
31 | ],
32 | },
33 |
34 | output: {
35 | path: webpackPaths.srcPath,
36 | // https://github.com/webpack/webpack/issues/1114
37 | library: {
38 | type: 'commonjs2',
39 | },
40 | },
41 |
42 | /**
43 | * Determine the array of extensions that should be used to resolve modules.
44 | */
45 | resolve: {
46 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'],
47 | modules: [webpackPaths.srcPath, 'node_modules'],
48 | // There is no need to add aliases here, the paths in tsconfig get mirrored
49 | plugins: [new TsconfigPathsPlugins()],
50 | },
51 |
52 | plugins: [
53 | new webpack.EnvironmentPlugin({
54 | NODE_ENV: 'production',
55 | }),
56 | ],
57 | };
58 |
59 | export default configuration;
60 |
--------------------------------------------------------------------------------
/.github/SUPPORT.md:
--------------------------------------------------------------------------------
1 | Hi! 👋
2 |
3 | We’re excited that you’re using **LibreLinkUpDesktop** and we’d love to help.
4 | To help us help you, please read through the following guidelines.
5 |
6 | Please understand that people involved with this project often do so for fun,
7 | next to their day job; you are not entitled to free customer service.
8 |
9 | ## Help us help you!
10 |
11 | Spending time framing a question and adding support links or resources makes it
12 | much easier for us to help.
13 | It’s easy to fall into the trap of asking something too specific when you’re
14 | close to a problem.
15 | Then, those trying to help you out have to spend a lot of time asking additional
16 | questions to understand what you are hoping to achieve.
17 |
18 | Spending the extra time up front can help save everyone time in the long run.
19 |
20 | * Try to define what you need help with:
21 | * Is there something in particular you want to do?
22 | * What problem are you encountering and what steps have you taken to try
23 | and fix it?
24 | * Is there a concept you’re not understanding?
25 | * Learn about the [rubber duck debugging method](https://rubberduckdebugging.com/)
26 | * Avoid falling for the [XY problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem/66378#66378)
27 | * Search on GitHub to see if a similar question has been asked
28 | * If possible, provide sample code, a [CodeSandbox](https://codesandbox.io/), or a video/GIF
29 | * The more time you put into asking your question, the better we can help you
30 |
31 | ## Contributions
32 |
33 | See [`contributing.md`](https://github.com/Crazy-Marvin/LibreLinkUpDesktop/blob/trunk/.github/CONTRIBUTING.md) on how to contribute. Quality PRs are really appreaciated!
34 |
35 |
--------------------------------------------------------------------------------
/src/renderer/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 240 10% 3.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 240 10% 3.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 240 10% 3.9%;
13 | --primary: 12 100% 50%;
14 | --primary-foreground: 355.7 100% 97.3%;
15 | --secondary: 240 4.8% 95.9%;
16 | --secondary-foreground: 240 5.9% 10%;
17 | --muted: 240 4.8% 95.9%;
18 | --muted-foreground: 240 3.8% 46.1%;
19 | --accent: 240 4.8% 95.9%;
20 | --accent-foreground: 240 5.9% 10%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 0 0% 98%;
23 | --border: 240 5.9% 90%;
24 | --input: 240 5.9% 90%;
25 | --ring: 12 100% 50%;
26 | --radius: 0.5rem;
27 | }
28 |
29 | .dark {
30 | --background: 20 14.3% 4.1%;
31 | --foreground: 0 0% 95%;
32 | --card: 24 9.8% 10%;
33 | --card-foreground: 0 0% 95%;
34 | --popover: 0 0% 9%;
35 | --popover-foreground: 0 0% 95%;
36 | --primary: 12 100% 50%;
37 | --primary-foreground: 355.7 100% 97.3%;
38 | --secondary: 240 3.7% 15.9%;
39 | --secondary-foreground: 0 0% 98%;
40 | --muted: 0 0% 15%;
41 | --muted-foreground: 240 5% 64.9%;
42 | --accent: 12 6.5% 15.1%;
43 | --accent-foreground: 0 0% 98%;
44 | --destructive: 0 62.8% 30.6%;
45 | --destructive-foreground: 0 85.7% 97.3%;
46 | --border: 240 3.7% 15.9%;
47 | --input: 240 3.7% 15.9%;
48 | --ring: 12 100% 50%;
49 | }
50 | }
51 |
52 | .draggable{
53 | -webkit-app-region: drag;
54 | }
55 |
56 | .no-draggable{
57 | -webkit-app-region: no-drag;
58 | }
59 |
60 | .overlay-shadow{
61 | text-shadow: 2px 1px 5px rgba(0, 0, 0, 0.8);
62 | }
63 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: 'erb',
3 | plugins: ['@typescript-eslint'],
4 | rules: {
5 | // A temporary hack related to IDE not resolving correct package.json
6 | 'react/button-has-type': 'off',
7 | 'import/prefer-default-export': 'off',
8 | 'import/no-extraneous-dependencies': 'off',
9 | 'react/react-in-jsx-scope': 'off',
10 | 'react/jsx-filename-extension': 'off',
11 | 'import/extensions': 'off',
12 | 'import/no-unresolved': 'off',
13 | 'import/no-import-module-exports': 'off',
14 | 'no-shadow': 'off',
15 | '@typescript-eslint/no-shadow': 'error',
16 | 'no-unused-vars': 'off',
17 | '@typescript-eslint/no-unused-vars': 'error',
18 | 'prettier/prettier': 'off',
19 | 'react/self-closing-comp': 'off',
20 | 'react/require-default-props': 'off',
21 | 'react/jsx-props-no-spreading': 'off',
22 | 'jsx-a11y/alt-text': 'off',
23 | 'no-restricted-syntax': 'off',
24 | 'react/no-array-index-key': 'off',
25 | 'import/no-dynamic-require': 'off',
26 | 'global-require': 'off',
27 | 'react/jsx-no-constructed-context-values': 'off',
28 | 'react-hooks/exhaustive-deps': 'off',
29 | },
30 | parserOptions: {
31 | ecmaVersion: 2020,
32 | sourceType: 'module',
33 | project: './tsconfig.json',
34 | tsconfigRootDir: __dirname,
35 | createDefaultProgram: true,
36 | },
37 | settings: {
38 | 'import/resolver': {
39 | // See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below
40 | node: {},
41 | webpack: {
42 | config: require.resolve('./.erb/configs/webpack.config.eslint.ts'),
43 | },
44 | typescript: {},
45 | },
46 | 'import/parsers': {
47 | '@typescript-eslint/parser': ['.ts', '.tsx'],
48 | },
49 | },
50 | };
51 |
--------------------------------------------------------------------------------
/.github/SETUP.md:
--------------------------------------------------------------------------------
1 | # LibreLinkUpDesktop Setup
2 |
3 | ## Building the application.
4 |
5 | Install Node.js 20 on your machine and go to the project root and run following command:
6 |
7 | ```bash
8 | npm install --legacy-peer-deps
9 | ```
10 |
11 | In order to generate the executables run following command:
12 |
13 | ```bash
14 | npm run package
15 | ```
16 |
17 | To generate executables for all platforms run this:
18 |
19 | ```bash
20 | npm run package-all
21 | ```
22 |
23 | Note: Executables can be found in `release/build` folder.
24 |
25 | A [release](https://github.com/Crazy-Marvin/LibreLinkUpDesktop/releases)
26 | should include those executables:
27 | - AppImage
28 | - snap
29 | - deb
30 | - MSI
31 | - EXE
32 | - AppX (Windows Store)
33 | - portable
34 | - pkg
35 |
36 | The ```version``` in the ```/release/app/package.json``` needs to be
37 | increased following the rules of [Semantic Versioning](https://semver.org/).
38 |
39 | ## Customizing the application
40 |
41 | - Localization is located on `src/renderer/i18n` folder.
42 | - App configuration can be found on `src/renderer/config` folder
43 |
44 | ## Store releases
45 |
46 | We officially support three stores:
47 |
48 | - **[Flathub](https://flathub.org/apps/rocks.poopjournal.librelinkupdesktop)**
49 |
50 | [Releasing Guide](./UPDATING-FLATHUB-RELEASE.md)
51 |
52 | - **[Snapcraft](https://snapcraft.io/librelinkupdesktop)**
53 |
54 | To release a new update, modify the version string in the `snapcraft.yaml` file (both in the version key and the electron-packager command).
55 |
56 | - **[Microsoft Store](https://www.microsoft.com/store/apps/9N5RKKLQM5C9)**
57 |
58 | TBD
59 |
60 | Note: Other (unofficial) releases might be done by the community. Please let us know by commenting in issue [#253](https://github.com/Crazy-Marvin/LibreLinkUpDesktop/issues/253) just so we know about it.
61 |
--------------------------------------------------------------------------------
/.erb/configs/webpack.config.main.dev.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Webpack config for development electron main process
3 | */
4 |
5 | import path from 'path';
6 | import webpack from 'webpack';
7 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
8 | import { merge } from 'webpack-merge';
9 | import checkNodeEnv from '../scripts/check-node-env';
10 | import baseConfig from './webpack.config.base';
11 | import webpackPaths from './webpack.paths';
12 |
13 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's
14 | // at the dev webpack config is not accidentally run in a production environment
15 | if (process.env.NODE_ENV === 'production') {
16 | checkNodeEnv('development');
17 | }
18 |
19 | const configuration: webpack.Configuration = {
20 | devtool: 'inline-source-map',
21 |
22 | mode: 'development',
23 |
24 | target: 'electron-main',
25 |
26 | entry: {
27 | main: path.join(webpackPaths.srcMainPath, 'main.ts'),
28 | preload: path.join(webpackPaths.srcMainPath, 'preload.ts'),
29 | },
30 |
31 | output: {
32 | path: webpackPaths.dllPath,
33 | filename: '[name].bundle.dev.js',
34 | library: {
35 | type: 'umd',
36 | },
37 | },
38 |
39 | plugins: [
40 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
41 | // @ts-ignore
42 | new BundleAnalyzerPlugin({
43 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
44 | analyzerPort: 8888,
45 | }),
46 |
47 | new webpack.DefinePlugin({
48 | 'process.type': '"browser"',
49 | }),
50 | ],
51 |
52 | /**
53 | * Disables webpack processing of __dirname and __filename.
54 | * If you run the bundle in node.js it falls back to these values of node.js.
55 | * https://github.com/webpack/webpack/issues/2010
56 | */
57 | node: {
58 | __dirname: false,
59 | __filename: false,
60 | },
61 | };
62 |
63 | export default merge(baseConfig, configuration);
64 |
--------------------------------------------------------------------------------
/src/main/windowState.ts:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow, Rectangle } from 'electron';
2 | import fs from 'fs';
3 | import path from 'path';
4 |
5 | export interface WindowState {
6 | width: number;
7 | height: number;
8 | x?: number;
9 | y?: number;
10 | }
11 |
12 | export class WindowStateManager {
13 | private stateFilePath: string;
14 |
15 | private state: WindowState;
16 |
17 | constructor(private windowName: string, private defaultState: WindowState, private windowMode: 'overlay' | 'overlayTransparent' | 'windowed') {
18 | const userDataPath = app.getPath('userData');
19 | this.stateFilePath = path.join(userDataPath, `${windowName}-${windowMode}-window-state.json`);
20 | this.state = this.readState();
21 | }
22 |
23 | private readState(): WindowState {
24 | try {
25 | return JSON.parse(fs.readFileSync(this.stateFilePath, 'utf-8'));
26 | } catch (error) {
27 | return this.defaultState;
28 | }
29 | }
30 |
31 | private saveState(state: WindowState): void {
32 | fs.writeFileSync(this.stateFilePath, JSON.stringify(state));
33 | }
34 |
35 | public manage(window: BrowserWindow): void {
36 | this.restore(window);
37 |
38 | let resizeTimeout: ReturnType | null = null;
39 | window.on('resize', () => {
40 | if (resizeTimeout !== null) {
41 | clearTimeout(resizeTimeout);
42 | }
43 | resizeTimeout = setTimeout(() => {
44 | this.save(window.getBounds());
45 | }, 500);
46 | });
47 |
48 | window.on('move', () => {
49 | this.save(window.getBounds());
50 | });
51 | }
52 |
53 | private restore(window: BrowserWindow): void {
54 | window.setBounds(this.state);
55 | }
56 |
57 | private save(bounds: Rectangle): void {
58 | this.state = {
59 | ...this.state,
60 | ...bounds,
61 | };
62 | this.saveState(this.state);
63 | }
64 |
65 | public getState(): WindowState {
66 | return this.state;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/renderer/components/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, createContext, useContext, useEffect, useState } from "react"
2 |
3 | export type ThemeType = "dark" | "light" | "system"
4 |
5 | type ThemeProviderProps = {
6 | children: ReactNode
7 | defaultTheme?: ThemeType
8 | storageKey?: string
9 | }
10 |
11 | type ThemeProviderState = {
12 | theme: ThemeType
13 | setTheme: (theme: ThemeType) => void
14 | }
15 |
16 | const initialState: ThemeProviderState = {
17 | theme: "system",
18 | setTheme: (t: ThemeType) => {
19 | localStorage.setItem('vite-ui-theme', t)
20 | },
21 | }
22 |
23 | const ThemeProviderContext = createContext(initialState)
24 |
25 | export function ThemeProvider({
26 | children,
27 | defaultTheme = "system",
28 | storageKey = "vite-ui-theme",
29 | ...props
30 | }: ThemeProviderProps) {
31 | const [theme, setTheme] = useState(
32 | () => (localStorage.getItem(storageKey) as ThemeType) || defaultTheme
33 | )
34 |
35 | useEffect(() => {
36 | const root = window.document.documentElement
37 |
38 | root.classList.remove("light", "dark")
39 |
40 | if (theme === "system") {
41 | const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
42 | .matches
43 | ? "dark"
44 | : "light"
45 |
46 | root.classList.add(systemTheme)
47 | return
48 | }
49 |
50 | root.classList.add(theme)
51 | }, [theme])
52 |
53 | const value = {
54 | theme,
55 | setTheme: (t: ThemeType) => {
56 | localStorage.setItem(storageKey, t)
57 | setTheme(t)
58 | },
59 | }
60 |
61 | return (
62 |
63 | {children}
64 |
65 | )
66 | }
67 |
68 | export const useTheme = () => {
69 | const context = useContext(ThemeProviderContext)
70 |
71 | if (context === undefined)
72 | throw new Error("useTheme must be used within a ThemeProvider")
73 |
74 | return context
75 | }
76 |
--------------------------------------------------------------------------------
/src/renderer/pages/settings/account.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react"
2 | import { useTranslation } from "react-i18next"
3 | import SettingsLayout from "@/layouts/settings-layout"
4 | import { getConnection } from "@/lib/linkup"
5 | import { useAuthStore } from "@/stores/auth"
6 | import { Input } from "@/components/ui/input"
7 | import { Button } from "@/components/ui/button"
8 | import { useClearSession } from "@/hooks/session"
9 |
10 | export default function SettingsAccountPage() {
11 | const { clearSession } = useClearSession()
12 | const { t } = useTranslation()
13 | const [connection, setConnection] = useState({})
14 | const token = useAuthStore((state) => state.token)
15 | const country = useAuthStore((state) => state.country)
16 | const accountId = useAuthStore((state) => state.accountId)
17 |
18 | const getConnectionData = async () => {
19 | try {
20 | const data = await getConnection({
21 | token: token ?? '',
22 | country: country ?? '',
23 | accountId: accountId ?? '',
24 | });
25 |
26 | if (data === null) {
27 | clearSession();
28 | return;
29 | }
30 |
31 | setConnection(data);
32 | } catch (error) {
33 | console.log('Unable to getConnection: ', error);
34 | }
35 | };
36 |
37 | useEffect(() => {
38 | getConnectionData()
39 | }, [])
40 |
41 | return (
42 |
43 |
44 |
45 |
{t('First Name')}
46 |
47 |
48 |
49 |
{t('Last Name')}
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/windowHandler.ts:
--------------------------------------------------------------------------------
1 | import { BrowserWindow, ipcMain, app } from 'electron';
2 | import path from 'path';
3 | import { resolveHtmlPath } from './util';
4 | import { WindowModeManager } from './windowMode';
5 |
6 | interface WindowCache {
7 | [url: string]: BrowserWindow | undefined;
8 | }
9 |
10 | export const registerWindowHandlers = () => {
11 | const windowCache: WindowCache = {};
12 |
13 | const windowModeManager = new WindowModeManager('main-window');
14 |
15 |
16 | // 👉 register window handlers
17 | ipcMain.on('open-new-window', (event, url, width, height) => {
18 | // Check if the window for this URL already exists
19 | if (windowCache[url]) {
20 | // Focus the existing window if it exists
21 | if (windowCache[url]) {
22 | windowCache[url]?.focus();
23 | }
24 | return;
25 | }
26 |
27 | const newWindow = new BrowserWindow({
28 | width,
29 | height,
30 | webPreferences: {
31 | webSecurity: false,
32 | preload: app.isPackaged
33 | ? path.join(__dirname, 'preload.js')
34 | : path.join(__dirname, '../../.erb/dll/preload.js'),
35 | },
36 | });
37 |
38 | newWindow.loadURL(resolveHtmlPath('index.html'));
39 | windowCache[url] = newWindow;
40 |
41 |
42 | newWindow.on('closed', () => {
43 | windowCache[url] = undefined;
44 | });
45 |
46 | });
47 |
48 | ipcMain.on('set-window-mode', (event, mode: 'overlay' | 'windowed') => {
49 | windowModeManager.setWindowMode(mode);
50 |
51 | // in order to change window options we need to restart
52 | app.relaunch();
53 | app.exit();
54 | });
55 |
56 | ipcMain.handle('get-window-mode', () => {
57 | return windowModeManager.getWindowMode();
58 | });
59 | };
60 |
61 | export const destroyWindowHandlers = () => {
62 | // 👉 destroy window handlers
63 | ipcMain.removeAllListeners('open-new-window');
64 | ipcMain.removeAllListeners('set-window-mode');
65 | ipcMain.removeAllListeners('get-window-mode');
66 | };
67 |
--------------------------------------------------------------------------------
/src/renderer/stores/alertStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 | import { persist, createJSONStorage } from 'zustand/middleware';
3 |
4 | type AlertSettingsStore = {
5 | bringToFrontEnabled: boolean;
6 | flashWindowEnabled: boolean;
7 | audioAlertEnabled: boolean;
8 | useCustomSound: boolean;
9 | overrideThreshold: boolean;
10 | customTargetLow: number | null;
11 | customTargetHigh: number | null;
12 | setBringToFrontEnabled: (value: boolean) => void;
13 | setFlashWindowEnabled: (value: boolean) => void;
14 | setAudioAlertEnabled: (value: boolean) => void;
15 | setUserCustomSoundEnabled: (value: boolean) => void;
16 | setOverrideThreshold: (value: boolean) => void;
17 | setCustomTargetLow: (value: number | null) => void;
18 | setCustomTargetHigh: (value: number | null) => void;
19 | };
20 |
21 | const useAlertStore = create()(
22 | persist(
23 | (set) => ({
24 | // Initial state
25 | bringToFrontEnabled: true,
26 | flashWindowEnabled: true,
27 | audioAlertEnabled: true,
28 | useCustomSound: false,
29 | overrideThreshold: false,
30 | customTargetLow: null,
31 | customTargetHigh: null,
32 | // Setters
33 | setBringToFrontEnabled: (value: boolean) => set(() => ({ bringToFrontEnabled: value })),
34 | setFlashWindowEnabled: (value: boolean) => set(() => ({ flashWindowEnabled: value })),
35 | setAudioAlertEnabled: (value: boolean) => set(() => ({ audioAlertEnabled: value })),
36 | setUserCustomSoundEnabled: (value: boolean) => set(() => ({ useCustomSound: value })),
37 | setOverrideThreshold: (value: boolean) => set(() => ({ overrideThreshold: value })),
38 | setCustomTargetLow: (value: number | null) => set(() => ({ customTargetLow: value })),
39 | setCustomTargetHigh: (value: number | null) => set(() => ({ customTargetHigh: value })),
40 | }),
41 | {
42 | name: 'alert-settings-storage',
43 | storage: createJSONStorage(() => localStorage),
44 | }
45 | )
46 | );
47 |
48 | export { useAlertStore };
49 |
--------------------------------------------------------------------------------
/.erb/configs/webpack.config.renderer.dev.dll.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Builds the DLL for development electron renderer process
3 | */
4 |
5 | import webpack from 'webpack';
6 | import path from 'path';
7 | import { merge } from 'webpack-merge';
8 | import baseConfig from './webpack.config.base';
9 | import webpackPaths from './webpack.paths';
10 | import { dependencies } from '../../package.json';
11 | import checkNodeEnv from '../scripts/check-node-env';
12 |
13 | checkNodeEnv('development');
14 |
15 | const dist = webpackPaths.dllPath;
16 |
17 | const configuration: webpack.Configuration = {
18 | context: webpackPaths.rootPath,
19 |
20 | devtool: 'eval',
21 |
22 | mode: 'development',
23 |
24 | target: 'electron-renderer',
25 |
26 | externals: ['fsevents', 'crypto-browserify'],
27 |
28 | /**
29 | * Use `module` from `webpack.config.renderer.dev.js`
30 | */
31 | module: require('./webpack.config.renderer.dev').default.module,
32 |
33 | entry: {
34 | renderer: Object.keys(dependencies || {}),
35 | },
36 |
37 | output: {
38 | path: dist,
39 | filename: '[name].dev.dll.js',
40 | library: {
41 | name: 'renderer',
42 | type: 'var',
43 | },
44 | },
45 |
46 | plugins: [
47 | new webpack.DllPlugin({
48 | path: path.join(dist, '[name].json'),
49 | name: '[name]',
50 | }),
51 |
52 | /**
53 | * Create global constants which can be configured at compile time.
54 | *
55 | * Useful for allowing different behaviour between development builds and
56 | * release builds
57 | *
58 | * NODE_ENV should be production so that modules do not perform certain
59 | * development checks
60 | */
61 | new webpack.EnvironmentPlugin({
62 | NODE_ENV: 'development',
63 | }),
64 |
65 | new webpack.LoaderOptionsPlugin({
66 | debug: true,
67 | options: {
68 | context: webpackPaths.srcPath,
69 | output: {
70 | path: webpackPaths.dllPath,
71 | },
72 | },
73 | }),
74 | ],
75 | };
76 |
77 | export default merge(baseConfig, configuration);
78 |
--------------------------------------------------------------------------------
/src/renderer/config/app.ts:
--------------------------------------------------------------------------------
1 | type DropdownConfigType = {
2 | value: string
3 | label: string
4 | }
5 |
6 | const countries: DropdownConfigType[] = [
7 | { value: 'global', label: 'Global' },
8 | { value: 'de', label: 'Germany' },
9 | { value: 'eu', label: 'European Union' },
10 | { value: 'eu2', label: 'European Union 2' },
11 | { value: 'us', label: 'United States' },
12 | { value: 'ap', label: 'Asia/Pacific' },
13 | { value: 'ca', label: 'Canada' },
14 | { value: 'jp', label: 'Japan' },
15 | { value: 'ae', label: 'United Arab Emirates' },
16 | { value: 'fr', label: 'France' },
17 | { value: 'au', label: 'Australia' },
18 | ]
19 |
20 | const languages: DropdownConfigType[] = [
21 | { value: 'bs', label: 'Bosnian' },
22 | { value: 'cs', label: 'Czech' },
23 | { value: 'de', label: 'German' },
24 | { value: 'el', label: 'Greek' },
25 | { value: 'en', label: 'English' },
26 | { value: 'fr', label: 'French' },
27 | { value: 'it', label: 'Italian' },
28 | { value: 'no', label: 'Norwegian' },
29 | { value: 'ru', label: 'Russian' },
30 | { value: 'si', label: 'Sinhala' },
31 | { value: 'es', label: 'Spanish' },
32 | { value: 'sv', label: 'Swedish' },
33 | { value: 'ta', label: 'Tamil' }
34 | ]
35 |
36 |
37 | const themes: DropdownConfigType[] = [
38 | {
39 | label: 'Dark',
40 | value: 'dark',
41 | },
42 | {
43 | label: 'Light',
44 | value: 'light',
45 | },
46 | {
47 | label: 'System',
48 | value: 'system',
49 | },
50 | ];
51 |
52 | const resultUnits: DropdownConfigType[] = [
53 | {
54 | label: 'mg/dL',
55 | value: 'mg/dL',
56 | },
57 | {
58 | label: 'mmol/L',
59 | value: 'mmol/L',
60 | },
61 | ];
62 |
63 | const windowModes: DropdownConfigType[] = [
64 | {
65 | label: 'Overlay',
66 | value: 'overlay',
67 | },
68 | {
69 | label: 'Overlay (Transparent)',
70 | value: 'overlayTransparent',
71 | },
72 | {
73 | label: 'Windowed',
74 | value: 'windowed',
75 | },
76 | ];
77 |
78 | export {
79 | countries,
80 | languages,
81 | themes,
82 | resultUnits,
83 | windowModes,
84 | }
85 |
--------------------------------------------------------------------------------
/.github/workflows/codacy-analysis.yml:
--------------------------------------------------------------------------------
1 | # This workflow checks out code, performs a Codacy security scan
2 | # and integrates the results with the
3 | # GitHub Advanced Security code scanning feature. For more information on
4 | # the Codacy security scan action usage and parameters, see
5 | # https://github.com/codacy/codacy-analysis-cli-action.
6 | # For more information on Codacy Analysis CLI in general, see
7 | # https://github.com/codacy/codacy-analysis-cli.
8 |
9 | name: Codacy Security Scan
10 |
11 | on:
12 | push:
13 | branches: [ development, master, trunk ]
14 | pull_request:
15 | # The branches below must be a subset of the branches above
16 | branches: [ development ]
17 | schedule:
18 | - cron: '25 8 * * 3'
19 |
20 | jobs:
21 | codacy-security-scan:
22 | name: Codacy Security Scan
23 | runs-on: ubuntu-latest
24 | steps:
25 | # Checkout the repository to the GitHub Actions runner
26 | - name: Checkout code
27 | uses: actions/checkout@v5
28 |
29 | # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
30 | - name: Run Codacy Analysis CLI
31 | uses: codacy/codacy-analysis-cli-action@v4.4.7
32 | with:
33 | # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository
34 | # You can also omit the token and run the tools that support default configurations
35 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
36 | verbose: true
37 | output: results.sarif
38 | format: sarif
39 | # Adjust severity of non-security issues
40 | gh-code-scanning-compat: true
41 | # Force 0 exit code to allow SARIF file generation
42 | # This will handover control about PR rejection to the GitHub side
43 | max-allowed-issues: 2147483647
44 |
45 | # Upload the SARIF file generated in the previous step
46 | - name: Upload SARIF results file
47 | uses: github/codeql-action/upload-sarif@v4
48 | with:
49 | sarif_file: results.sarif
50 |
--------------------------------------------------------------------------------
/src/renderer/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | transparent:
21 | "bg-background text-foreground hover:text-primary hover:bg-primary/10",
22 | ghost: "hover:bg-accent hover:text-accent-foreground",
23 | link: "text-primary underline-offset-4 hover:underline",
24 | },
25 | size: {
26 | default: "h-9 px-4 py-2",
27 | sm: "h-8 rounded-md px-3 text-xs",
28 | lg: "h-10 rounded-md px-8",
29 | icon: "h-9 w-9",
30 | },
31 | },
32 | defaultVariants: {
33 | variant: "default",
34 | size: "default",
35 | },
36 | }
37 | )
38 |
39 | export interface ButtonProps
40 | extends React.ButtonHTMLAttributes,
41 | VariantProps {
42 | asChild?: boolean
43 | }
44 |
45 | const Button = React.forwardRef(
46 | ({ className, variant, size, asChild = false, ...props }, ref) => {
47 | const Comp = asChild ? Slot : "button"
48 | return (
49 |
54 | )
55 | }
56 | )
57 | Button.displayName = "Button"
58 |
59 | export { Button, buttonVariants }
60 |
--------------------------------------------------------------------------------
/.erb/configs/webpack.config.preload.dev.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import webpack from 'webpack';
3 | import { merge } from 'webpack-merge';
4 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
5 | import baseConfig from './webpack.config.base';
6 | import webpackPaths from './webpack.paths';
7 | import checkNodeEnv from '../scripts/check-node-env';
8 |
9 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's
10 | // at the dev webpack config is not accidentally run in a production environment
11 | if (process.env.NODE_ENV === 'production') {
12 | checkNodeEnv('development');
13 | }
14 |
15 | const configuration: webpack.Configuration = {
16 | devtool: 'inline-source-map',
17 |
18 | mode: 'development',
19 |
20 | target: 'electron-preload',
21 |
22 | entry: path.join(webpackPaths.srcMainPath, 'preload.ts'),
23 |
24 | output: {
25 | path: webpackPaths.dllPath,
26 | filename: 'preload.js',
27 | library: {
28 | type: 'umd',
29 | },
30 | },
31 |
32 | plugins: [
33 | new BundleAnalyzerPlugin({
34 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
35 | }),
36 |
37 | /**
38 | * Create global constants which can be configured at compile time.
39 | *
40 | * Useful for allowing different behaviour between development builds and
41 | * release builds
42 | *
43 | * NODE_ENV should be production so that modules do not perform certain
44 | * development checks
45 | *
46 | * By default, use 'development' as NODE_ENV. This can be overriden with
47 | * 'staging', for example, by changing the ENV variables in the npm scripts
48 | */
49 | new webpack.EnvironmentPlugin({
50 | NODE_ENV: 'development',
51 | }),
52 |
53 | new webpack.LoaderOptionsPlugin({
54 | debug: true,
55 | }),
56 | ],
57 |
58 | /**
59 | * Disables webpack processing of __dirname and __filename.
60 | * If you run the bundle in node.js it falls back to these values of node.js.
61 | * https://github.com/webpack/webpack/issues/2010
62 | */
63 | node: {
64 | __dirname: false,
65 | __filename: false,
66 | },
67 |
68 | watch: true,
69 | };
70 |
71 | export default merge(baseConfig, configuration);
72 |
--------------------------------------------------------------------------------
/.erb/scripts/check-native-dep.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import chalk from 'chalk';
3 | import { execSync } from 'child_process';
4 | import { dependencies } from '../../package.json';
5 |
6 | if (dependencies) {
7 | const dependenciesKeys = Object.keys(dependencies);
8 | const nativeDeps = fs
9 | .readdirSync('node_modules')
10 | .filter((folder) => fs.existsSync(`node_modules/${folder}/binding.gyp`));
11 | if (nativeDeps.length === 0) {
12 | process.exit(0);
13 | }
14 | try {
15 | // Find the reason for why the dependency is installed. If it is installed
16 | // because of a devDependency then that is okay. Warn when it is installed
17 | // because of a dependency
18 | const { dependencies: dependenciesObject } = JSON.parse(
19 | execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString()
20 | );
21 | const rootDependencies = Object.keys(dependenciesObject);
22 | const filteredRootDependencies = rootDependencies.filter((rootDependency) =>
23 | dependenciesKeys.includes(rootDependency)
24 | );
25 | if (filteredRootDependencies.length > 0) {
26 | const plural = filteredRootDependencies.length > 1;
27 | console.log(`
28 | ${chalk.whiteBright.bgYellow.bold(
29 | 'Webpack does not work with native dependencies.'
30 | )}
31 | ${chalk.bold(filteredRootDependencies.join(', '))} ${
32 | plural ? 'are native dependencies' : 'is a native dependency'
33 | } and should be installed inside of the "./release/app" folder.
34 | First, uninstall the packages from "./package.json":
35 | ${chalk.whiteBright.bgGreen.bold('npm uninstall your-package')}
36 | ${chalk.bold(
37 | 'Then, instead of installing the package to the root "./package.json":'
38 | )}
39 | ${chalk.whiteBright.bgRed.bold('npm install your-package')}
40 | ${chalk.bold('Install the package to "./release/app/package.json"')}
41 | ${chalk.whiteBright.bgGreen.bold(
42 | 'cd ./release/app && npm install your-package'
43 | )}
44 | Read more about native dependencies at:
45 | ${chalk.bold(
46 | 'https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure'
47 | )}
48 | `);
49 | process.exit(1);
50 | }
51 | } catch (e) {
52 | console.log('Native dependencies could not be checked');
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/renderer/i18n/si.json:
--------------------------------------------------------------------------------
1 | {
2 | "Welcome": "මෙය ඔබගේ රුධිර සීනි ලබා ගන්නා ඩෙස්ක්ටොප් යෙදුමකි",
3 | "Settings": "සැකසුම්",
4 | "General": "පොදු",
5 | "Account": "ගිණුම",
6 | "First Name": "මුල් නම",
7 | "Last Name": "අවසන් නම",
8 | "System": "පද්ධති",
9 | "Dark": "අඳුරු",
10 | "Light": "ආලෝකය",
11 | "English": "ඉංග්රීසි",
12 | "Sinhala": "සිංහල",
13 | "Norwegian": "නෝර්වීජියානු",
14 | "Germany": "ජර්මනිය",
15 | "getCredentialsTitle": "පිවිසුම් විස්තර ලබා ගන්නේ කෙසේද",
16 | "getCredentialsStep1": "බෙදාගන්නා පුද්ගලයා ලෙස, ඔබේ <1>Libre smartphone app1> විවෘත කරන්න, <3>Connected Apps3> වෙත යන්න, <5>Manage5> මත ක්ලික් කරන්න, <7>Add connection7> එක මත ක්ලික් කරන්න සහ ඔබ LibreLinkUpDesktop සමඟ භාවිතා කිරීමට බලාපොරොත්තු වන ගිණුම සඳහා විස්තර ඇතුළත් කරන්න.",
17 | "getCredentialsStep1Link": "https://play.google.com/store/apps/details?id=com.freestylelibre3.app.de",
18 | "getCredentialsStep2": "ඔබගේ මුරපද කළමනාකරු තුළ එම අක්තපත්ර සුරකින්න. ඔබට ඒවා ඔබ වෙනුවෙන් භාවිතා කළ හැකිය, නැතහොත් ඔබට ඒවා යමෙකු සමඟ බෙදා ගත හැකිය.",
19 | "getCredentialsStep3": "මෙම පිවිසුම් පිටුවේ එම අක්තපත්ර ඇතුළත් කරන්න.",
20 | "getCredentialsStep4": "එපමනයි. 😄",
21 | "Username": "පරිශීලක නාමය",
22 | "Password": "මුරපදය",
23 | "Country": "රට",
24 | "Language": "භාෂාව",
25 | "SelectCountry": "රට තෝරන්න",
26 | "SelectLanguage": "භාෂාව තෝරන්න",
27 | "Australia": "ඕස්ට්රේලියාව",
28 | "German": "ජර්මන්",
29 | "Japan": "ජපානය",
30 | "Canada": "කැනඩාව",
31 | "Theme": "තේමාව",
32 | "Unit": "ඒකකය",
33 | "Windowed": "ජනේල සහිත",
34 | "Overlay": "අතිච්ඡාදනය",
35 | "Overlay (Transparent)": "ආවරණය (විනිවිද පෙනෙන)",
36 | "Spanish": "ස්පාඤ්ඤ",
37 | "Russian": "රුසියානු",
38 | "Greek": "ග්රීක",
39 | "SelectMode": "මාදිලිය තෝරන්න",
40 | "SelectUnit": "ඒකකය තෝරන්න",
41 | "Czech": "චෙක්",
42 | "Bosnian": "බොස්නියානු",
43 | "SelectTheme": "තේමාව තෝරන්න",
44 | "WindowMode": "කවුළු මාදිලිය",
45 | "United Arab Emirates": "එක්සත් අරාබි එමීර් රාජ්යය",
46 | "France": "ප්රංශය",
47 | "United States": "එක්සත් ජනපදය",
48 | "European Union": "යුරෝපනු සංගමය",
49 | "European Union 2": "යුරෝපා සංගමය 2",
50 | "Asia/Pacific": "ආසියා/පැසිෆික්",
51 | "Login": "ඇතුල් වන්න"
52 | }
53 |
--------------------------------------------------------------------------------
/src/renderer/i18n/cs.json:
--------------------------------------------------------------------------------
1 | {
2 | "First Name": "Křestní jméno",
3 | "System": "Systém",
4 | "Dark": "Tmavý",
5 | "German": "Němčina",
6 | "European Union": "Evropská Unie",
7 | "European Union 2": "Evropská Unie 2",
8 | "Asia/Pacific": "Asie/Pacifik",
9 | "United Arab Emirates": "Spojené Arabské Emiráty",
10 | "France": "Francie",
11 | "Australia": "Austrálie",
12 | "getCredentialsTitle": "Jak získat přihlašovací údaje",
13 | "getCredentialsStep1Link": "https://play.google.com/store/apps/details?id=com.freestylelibre.app.cz",
14 | "getCredentialsStep4": "To je vše 😄",
15 | "Username": "Uživatelské jméno",
16 | "Country": "Země",
17 | "SelectCountry": "Vyberte zemi",
18 | "SelectLanguage": "Vyberte jazyk",
19 | "Login": "Přihlášení",
20 | "Unit": "Jednotky",
21 | "SelectUnit": "Vyberte jednotky",
22 | "SelectMode": "Vyberte režim",
23 | "WindowMode": "Režim okna",
24 | "Overlay": "Překrytí",
25 | "Overlay (Transparent)": "Překrytí (Transparentní)",
26 | "Windowed": "Okna",
27 | "SelectTheme": "Vyberte vzhled",
28 | "Welcome": "Desktopová aplikace pro sledování hladiny glykemie z LibreLinkUp",
29 | "Settings": "Možnosti",
30 | "Light": "Světlý",
31 | "General": "Obecné",
32 | "Account": "Účet",
33 | "Norwegian": "Norsky",
34 | "Last Name": "Příjmení",
35 | "English": "Angličtina",
36 | "Sinhala": "Sinhala",
37 | "Japan": "Japonsko",
38 | "Germany": "Německo",
39 | "Canada": "Kanada",
40 | "United States": "Spojené státy",
41 | "getCredentialsStep1": "Osoba, která údaje sdílí, si otevře <1>LibreLink mobilní aplikaci1>, přejde na <3>Propojené aplikace3>, klikne na <5>Spravovat5> u volby LibreLinkUp, vybere <7>Přidat připojení7> a zadá podrobnosti účtu, který chce používat s LibreLinkUpDesktop aplikací.",
42 | "getCredentialsStep3": "Přihlašovací údaje zadejte na přihlašovací stránce.",
43 | "getCredentialsStep2": "Uložte si přihlašovací údaje do správce hesel. Můžete je použít pro sebe nebo je můžete s někým sdílet.",
44 | "Password": "Heslo",
45 | "Language": "Jazyk",
46 | "Theme": "Vzhled",
47 | "Spanish": "Španělština",
48 | "Czech": "Čeština",
49 | "Bosnian": "Bosenský",
50 | "Greek": "Řecký",
51 | "Russian": "Ruština"
52 | }
53 |
--------------------------------------------------------------------------------
/src/renderer/layouts/settings-layout.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@/components/ui/button';
2 | import { BaseLayout } from '@/layouts/base-layout';
3 | import { cn } from '@/lib/utils';
4 | import {
5 | ArrowLeftIcon,
6 | MixerVerticalIcon,
7 | PersonIcon,
8 | BellIcon,
9 | } from '@radix-ui/react-icons';
10 | import { ReactNode } from 'react';
11 | import { useLocation, useNavigate } from 'react-router-dom';
12 | import { useTranslation } from 'react-i18next';
13 |
14 | type Props = {
15 | children: ReactNode;
16 | };
17 |
18 | function SidebarButton({
19 | label,
20 | url,
21 | icon,
22 | }: {
23 | label: string;
24 | url: string;
25 | icon: ReactNode;
26 | }) {
27 | const navigate = useNavigate();
28 | const location = useLocation();
29 | const isActive = url === location.pathname;
30 |
31 | return (
32 |
43 | );
44 | }
45 |
46 | export default function SettingsLayout({ children }: Props) {
47 | const navigate = useNavigate();
48 | const { t } = useTranslation();
49 |
50 | return (
51 |
52 | {/* */}
58 |
59 |
60 | }
64 | />
65 |
66 | }
70 | />
71 | }
75 | />
76 |
77 |
{children}
78 |
79 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/src/renderer/i18n/sv.json:
--------------------------------------------------------------------------------
1 | {
2 | "Settings": "Inställningar",
3 | "Welcome": "Dettar är ett program som hämtar dina blodsockervärden från LibreLinkUp",
4 | "General": "Generella",
5 | "Account": "Konto",
6 | "First Name": "Förnamn",
7 | "Last Name": "Efternamn",
8 | "System": "System",
9 | "Dark": "Mörkt",
10 | "Light": "Ljust",
11 | "German": "Tyska",
12 | "English": "Engelska",
13 | "Norwegian": "Norska",
14 | "Spanish": "Spanska",
15 | "Czech": "Tjeckiska",
16 | "Russian": "Ryska",
17 | "Bosnian": "Bosniska",
18 | "Greek": "Grekiska",
19 | "Germany": "Tyskland",
20 | "European Union": "Europeiska Unionen",
21 | "European Union 2": "Europeiska Unionen 2",
22 | "United States": "Amerikas förenade stater",
23 | "Canada": "Kanada",
24 | "Japan": "Japan",
25 | "United Arab Emirates": "Förenade arab emiraten",
26 | "France": "Frankrike",
27 | "Australia": "Australien",
28 | "getCredentialsStep1Link": "https://play.google.com/store/apps/details?id=com.freestylelibre3.app.se",
29 | "getCredentialsTitle": "Inloggningsuppgifter",
30 | "getCredentialsStep4": "Det är allt som behövs. 😄",
31 | "Username": "Användarnamn",
32 | "Password": "Lösenord",
33 | "Login": "Logga in",
34 | "Country": "Land",
35 | "getCredentialsStep2": "Spara gärna dina inställningar i din lösenordshanterare. Du kan även dela dina inställningar till någon annan som skall kunna följa dina värden.",
36 | "getCredentialsStep3": "Skriv in användaruppgifterna på denna inloggningssida.",
37 | "Sinhala": "Singalesiska",
38 | "Asia/Pacific": "Asien - Stilla havs regionen",
39 | "SelectCountry": "Välj land",
40 | "SelectLanguage": "Välj språk",
41 | "Theme": "Tema",
42 | "Unit": "Enhet",
43 | "SelectTheme": "Välj tema",
44 | "SelectUnit": "Välj enhet",
45 | "SelectMode": "Välj mode",
46 | "WindowMode": "Fönster mode",
47 | "Overlay (Transparent)": "Överlagring (genomskinlig)",
48 | "Windowed": "Fristående fönster",
49 | "Overlay": "Överlagrad",
50 | "Language": "Språk",
51 | "getCredentialsStep1": "Som utdelare, öppna <1>Libre smartphone app1>, gå till <3>Connected Apps3>, klicka på <5>Manage5> på raden för LibreLinkUp, klicka på <7>Add connection7> och mata in egenskaperna för den som du vill skall kunna använda LibreLinkUpDesktop."
52 | }
53 |
--------------------------------------------------------------------------------
/.erb/configs/webpack.config.main.prod.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Webpack config for production electron main process
3 | */
4 |
5 | import path from 'path';
6 | import webpack from 'webpack';
7 | import { merge } from 'webpack-merge';
8 | import TerserPlugin from 'terser-webpack-plugin';
9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
10 | import baseConfig from './webpack.config.base';
11 | import webpackPaths from './webpack.paths';
12 | import checkNodeEnv from '../scripts/check-node-env';
13 | import deleteSourceMaps from '../scripts/delete-source-maps';
14 |
15 | checkNodeEnv('production');
16 | deleteSourceMaps();
17 |
18 | const configuration: webpack.Configuration = {
19 | devtool: 'source-map',
20 |
21 | mode: 'production',
22 |
23 | target: 'electron-main',
24 |
25 | entry: {
26 | main: path.join(webpackPaths.srcMainPath, 'main.ts'),
27 | preload: path.join(webpackPaths.srcMainPath, 'preload.ts'),
28 | },
29 |
30 | output: {
31 | path: webpackPaths.distMainPath,
32 | filename: '[name].js',
33 | library: {
34 | type: 'umd',
35 | },
36 | },
37 |
38 | optimization: {
39 | minimizer: [
40 | new TerserPlugin({
41 | parallel: true,
42 | }),
43 | ],
44 | },
45 |
46 | plugins: [
47 | new BundleAnalyzerPlugin({
48 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
49 | analyzerPort: 8888,
50 | }),
51 |
52 | /**
53 | * Create global constants which can be configured at compile time.
54 | *
55 | * Useful for allowing different behaviour between development builds and
56 | * release builds
57 | *
58 | * NODE_ENV should be production so that modules do not perform certain
59 | * development checks
60 | */
61 | new webpack.EnvironmentPlugin({
62 | NODE_ENV: 'production',
63 | DEBUG_PROD: false,
64 | START_MINIMIZED: false,
65 | }),
66 |
67 | new webpack.DefinePlugin({
68 | 'process.type': '"browser"',
69 | }),
70 | ],
71 |
72 | /**
73 | * Disables webpack processing of __dirname and __filename.
74 | * If you run the bundle in node.js it falls back to these values of node.js.
75 | * https://github.com/webpack/webpack/issues/2010
76 | */
77 | node: {
78 | __dirname: false,
79 | __filename: false,
80 | },
81 | };
82 |
83 | export default merge(baseConfig, configuration);
84 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | './src/renderer/**/*.{js,jsx,ts,tsx}',
6 | ],
7 | theme: {
8 | container: {
9 | center: true,
10 | padding: "2rem",
11 | screens: {
12 | "2xl": "1400px",
13 | },
14 | },
15 | extend: {
16 | screens: {
17 | '2xs': "250px",
18 | 'xs': "400px",
19 | },
20 | colors: {
21 | border: "hsl(var(--border))",
22 | input: "hsl(var(--input))",
23 | ring: "hsl(var(--ring))",
24 | background: "hsl(var(--background))",
25 | foreground: "hsl(var(--foreground))",
26 | primary: {
27 | DEFAULT: "hsl(var(--primary))",
28 | foreground: "hsl(var(--primary-foreground))",
29 | },
30 | secondary: {
31 | DEFAULT: "hsl(var(--secondary))",
32 | foreground: "hsl(var(--secondary-foreground))",
33 | },
34 | destructive: {
35 | DEFAULT: "hsl(var(--destructive))",
36 | foreground: "hsl(var(--destructive-foreground))",
37 | },
38 | muted: {
39 | DEFAULT: "hsl(var(--muted))",
40 | foreground: "hsl(var(--muted-foreground))",
41 | },
42 | accent: {
43 | DEFAULT: "hsl(var(--accent))",
44 | foreground: "hsl(var(--accent-foreground))",
45 | },
46 | popover: {
47 | DEFAULT: "hsl(var(--popover))",
48 | foreground: "hsl(var(--popover-foreground))",
49 | },
50 | card: {
51 | DEFAULT: "hsl(var(--card))",
52 | foreground: "hsl(var(--card-foreground))",
53 | },
54 | },
55 | borderRadius: {
56 | lg: "var(--radius)",
57 | md: "calc(var(--radius) - 2px)",
58 | sm: "calc(var(--radius) - 4px)",
59 | },
60 | keyframes: {
61 | "accordion-down": {
62 | from: { height: 0 },
63 | to: { height: "var(--radix-accordion-content-height)" },
64 | },
65 | "accordion-up": {
66 | from: { height: "var(--radix-accordion-content-height)" },
67 | to: { height: 0 },
68 | },
69 | },
70 | animation: {
71 | "accordion-down": "accordion-down 0.2s ease-out",
72 | "accordion-up": "accordion-up 0.2s ease-out",
73 | },
74 | },
75 | },
76 | plugins: [require("tailwindcss-animate")],
77 | }
78 |
--------------------------------------------------------------------------------
/src/renderer/i18n/es.json:
--------------------------------------------------------------------------------
1 | {
2 | "Light": "Claro",
3 | "German": "Alemán",
4 | "getCredentialsStep1": "Como persona con acceso compartido, abre tu aplicación <1>FreeStyle LibreLink1> en tu dispositivo móvil, en el menú de navegación ve a <3>Aplicaciones Conectadas3>, haz clic sobre <5>Gestionar5> junto a LibreLinkUp, haz clic sobre <7>Añadir conexión7> e introduce los detalles de la cuenta que deseas usar con LibreLinkUpDesktop (esta aplicación).",
5 | "Login": "Entrar",
6 | "Country": "País",
7 | "Language": "Idioma",
8 | "SelectCountry": "Selecciona País",
9 | "English": "Inglés",
10 | "Sinhala": "Cingalés",
11 | "Norwegian": "Noruego",
12 | "Germany": "Alemania",
13 | "European Union": "Unión Europea",
14 | "European Union 2": "Unión Europea 2",
15 | "United States": "Estados Unidos",
16 | "Asia/Pacific": "Asia/Pacifico",
17 | "Canada": "Canada",
18 | "Japan": "Japón",
19 | "United Arab Emirates": "Emiratos Árabes unidos",
20 | "France": "Francia",
21 | "Australia": "Australia",
22 | "getCredentialsTitle": "Cómo obtener las credenciales",
23 | "getCredentialsStep1Link": "https://play.google.com/store/apps/details?id=com.freestylelibre3.app.es",
24 | "getCredentialsStep2": "Guarda las credenciales en tu gestor de contraseñas. Puedes usarlas tu mismo o compartirla con alguien.",
25 | "getCredentialsStep3": "Introduce estas credenciales en esta pagina de registro.",
26 | "getCredentialsStep4": "Eso es todo. 😄",
27 | "Username": "Usuario",
28 | "Password": "Contraseña",
29 | "SelectLanguage": "Selecciona Idioma",
30 | "Welcome": "Esta es una aplicación de escritorio que recupera tus valores de azúcar en sangre desde LibreLinkUp",
31 | "General": "General",
32 | "Settings": "Configuración",
33 | "Account": "Cuenta",
34 | "First Name": "Nombre",
35 | "Dark": "Oscuro",
36 | "Last Name": "Apellido/s",
37 | "System": "Sistema",
38 | "Unit": "Unidad",
39 | "Theme": "Tema",
40 | "Overlay": "Superposición",
41 | "WindowMode": "Modo ventana",
42 | "SelectMode": "Seleccionar modo",
43 | "SelectTheme": "Seleccionar tema",
44 | "SelectUnit": "Seleccionar unidad",
45 | "Overlay (Transparent)": "Superposición (Transparente)",
46 | "Windowed": "Con ventanas",
47 | "Bosnian": "Bosnio",
48 | "Greek": "Griego",
49 | "Spanish": "Español",
50 | "Czech": "Checo",
51 | "Russian": "Ruso"
52 | }
53 |
--------------------------------------------------------------------------------
/src/renderer/i18n/el.json:
--------------------------------------------------------------------------------
1 | {
2 | "Theme": "Θέμα",
3 | "Unit": "Μονάδα",
4 | "SelectTheme": "Επιλέξτε Θέμα",
5 | "Overlay (Transparent)": "Επικάλυψη (Διαφανές)",
6 | "Windowed": "Παράθυρο",
7 | "Spanish": "ισπανικά",
8 | "Czech": "Τσέχος",
9 | "Russian": "ρωσικός",
10 | "Bosnian": "Βόσνιος",
11 | "Greek": "ελληνικά",
12 | "Canada": "Καναδάς",
13 | "Japan": "Ιαπωνία",
14 | "United Arab Emirates": "Ηνωμένα Αραβικά Εμιράτα",
15 | "France": "Γαλλία",
16 | "Australia": "Αυστραλία",
17 | "getCredentialsStep3": "Εισαγάγετε αυτά τα διαπιστευτήρια σε αυτήν τη σελίδα σύνδεσης.",
18 | "SelectUnit": "Επιλέξτε Μονάδα",
19 | "SelectMode": "Επιλέξτε Λειτουργία",
20 | "WindowMode": "Λειτουργία παραθύρου",
21 | "Overlay": "Επικάλυμμα",
22 | "Welcome": "Αυτή είναι μια εφαρμογή επιτραπέζιου υπολογιστή που λαμβάνει το σάκχαρό σας από το LibreLinkUp",
23 | "Settings": "Ρυθμίσεις",
24 | "General": "Γενικός",
25 | "Account": "Λογαριασμός",
26 | "First Name": "Ονομα",
27 | "Last Name": "Επώνυμο",
28 | "System": "Σύστημα",
29 | "Asia/Pacific": "Ασία/Ειρηνικό",
30 | "Dark": "Σκοτάδι",
31 | "Light": "Φως",
32 | "German": "Γερμανός",
33 | "English": "αγγλικός",
34 | "Sinhala": "Σινχαλά",
35 | "Norwegian": "Νορβηγός",
36 | "Germany": "Γερμανία",
37 | "European Union": "Ευρωπαϊκή Ένωση",
38 | "European Union 2": "Ευρωπαϊκή Ένωση 2",
39 | "United States": "Ηνωμένες Πολιτείες",
40 | "getCredentialsStep2": "Αποθηκεύστε αυτά τα διαπιστευτήρια μέσα στον διαχειριστή κωδικών πρόσβασης. Μπορείτε να τα χρησιμοποιήσετε για τον εαυτό σας ή να τα μοιραστείτε με κάποιον.",
41 | "getCredentialsStep1Link": "https://play.google.com/store/apps/details?id=com.freestylelibre.app.gr",
42 | "getCredentialsStep4": "Αυτό είναι όλο. 😄",
43 | "getCredentialsTitle": "Πώς να αποκτήσετε διαπιστευτήρια",
44 | "getCredentialsStep1": "Ως άτομο που κάνει κοινή χρήση, ανοίξτε την <1>εφαρμογή Libre smartphone1>, μεταβείτε στις <3>Συνδεδεμένες εφαρμογές3>, κάντε κλικ στην επιλογή <5>Διαχείριση5> δίπλα στο LibreLinkUp, κάντε κλικ στην επιλογή <7>Προσθήκη σύνδεσης 7> και εισαγάγετε τις λεπτομέρειες για τον λογαριασμό που θέλετε να χρησιμοποιήσετε με το LibreLinkUpDesktop.",
45 | "Login": "Συνδεθείτε",
46 | "Country": "Χώρα",
47 | "SelectCountry": "Επιλέξτε Χώρα",
48 | "Username": "Όνομα χρήστη",
49 | "Password": "Σύνθημα",
50 | "Language": "Γλώσσα",
51 | "SelectLanguage": "Επιλέξτε Γλώσσα"
52 | }
53 |
--------------------------------------------------------------------------------
/src/renderer/hooks/useGlucoseAlerts.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 | import { getAlertSoundFile, triggerWarningAlert } from '@/lib/utils';
3 | import { AudioManager } from '@/lib/AudioManager';
4 | import { useAlertStore } from '@/stores/alertStore';
5 |
6 | export const useGlucoseAlerts = () => {
7 | const {
8 | bringToFrontEnabled,
9 | flashWindowEnabled,
10 | audioAlertEnabled,
11 | useCustomSound,
12 | overrideThreshold,
13 | customTargetHigh,
14 | customTargetLow,
15 | } = useAlertStore();
16 | const dispatchAlert = useCallback(() => {
17 | return async (
18 | glucoseLevel: number,
19 | targetLow: number,
20 | targetHigh: number,
21 | ) => {
22 | try {
23 | // // NOTE:: used for testing purposes
24 | // triggerWarningAlert({
25 | // visualAlertEnabled: visualAlertEnabled,
26 | // });
27 |
28 | // // NOTE:: used for testing purposes
29 | // if (audioAlertEnabled) {
30 | // const paths = await getAlertSoundFile();
31 | // let audioFilePath = paths.default;
32 | // if (useCustomSound && paths?.custom) {
33 | // audioFilePath = paths.custom;
34 | // }
35 |
36 | // if (audioFilePath) {
37 | // const audioManager = AudioManager.getInstance();
38 | // await audioManager.playAudio(audioFilePath);
39 | // }
40 | // }
41 |
42 | // glucose level checks and alerts
43 | const lowThreshold =
44 | overrideThreshold && customTargetLow ? customTargetLow : targetLow;
45 | const highThreshold =
46 | overrideThreshold && customTargetHigh ? customTargetHigh : targetHigh;
47 |
48 | if (
49 | glucoseLevel !== undefined &&
50 | (glucoseLevel < lowThreshold || glucoseLevel > highThreshold)
51 | ) {
52 | triggerWarningAlert({
53 | bringToFrontEnabled,
54 | flashWindowEnabled,
55 | });
56 |
57 | if (audioAlertEnabled) {
58 | const paths = await getAlertSoundFile();
59 |
60 | let audioFilePath = paths.default;
61 | if (useCustomSound && paths?.custom) {
62 | audioFilePath = paths.custom;
63 | }
64 |
65 | if (audioFilePath) {
66 | const audioManager = AudioManager.getInstance();
67 | await audioManager.playAudio(audioFilePath);
68 | }
69 | }
70 | }
71 | } catch (err) {
72 | console.error('Error in dispatchAlert:', err);
73 | }
74 | };
75 | }, [
76 | bringToFrontEnabled,
77 | flashWindowEnabled,
78 | audioAlertEnabled,
79 | useCustomSound,
80 | overrideThreshold,
81 | customTargetHigh,
82 | customTargetLow,
83 | ]);
84 |
85 | return { dispatchAlert: dispatchAlert() };
86 | };
87 |
--------------------------------------------------------------------------------
/snapcraft.yaml:
--------------------------------------------------------------------------------
1 | name: librelinkupdesktop
2 | base: core22
3 | version: '0.1.15'
4 | summary: LibreLinkUpDesktop
5 | description: |
6 | This is a desktop application that fetches your blood sugar from LibreLinkUp.
7 |
8 | Features of LibreLinkUpDesktop:
9 | - Show blood glucose level on your desktop in a little window
10 | - No tracking
11 | - Dark Mode
12 | - Libre software
13 | - That's it. 🩸
14 |
15 | contact: 'mailto:marvin@poopjournal.rocks'
16 | donation: https://poopjournal.rocks/blog/donate/
17 | issues: https://github.com/Crazy-Marvin/LibreLinkUpDesktop/issues
18 | source-code: https://github.com/Crazy-Marvin/LibreLinkUpDesktop
19 | license: 'Apache-2.0'
20 | title: LibreLinkUpDesktop
21 | website: 'https://github.com/Crazy-Marvin/LibreLinkUpDesktop'
22 |
23 | confinement: strict
24 | grade: stable
25 |
26 | architectures:
27 | - build-on: amd64
28 |
29 | icon: snap/gui/librelinkupdesktop.png
30 |
31 | apps:
32 | librelinkupdesktop:
33 | command: librelinkupdesktop/librelinkupdesktop --no-sandbox
34 | extensions: [gnome]
35 | desktop: snap/gui/librelinkupdesktop.desktop
36 | plugs:
37 | - browser-support
38 | - network
39 | - network-bind
40 | environment:
41 | TMPDIR: $XDG_RUNTIME_DIR
42 |
43 | parts:
44 | librelinkupdesktop:
45 | plugin: nil
46 | source: .
47 | override-build: |
48 | # Configure proxy for Electron download if a proxy is set
49 | if [ -n "${http_proxy:-}" ]; then
50 | export ELECTRON_GET_USE_PROXY=1
51 | export GLOBAL_AGENT_HTTP_PROXY="${http_proxy}"
52 | export GLOBAL_AGENT_HTTPS_PROXY="${http_proxy}"
53 | fi
54 |
55 | npm install electron @electron/packager --legacy-peer-deps
56 | npx ts-node ./.erb/scripts/clean.js dist
57 |
58 | npx cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts
59 | npx cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts
60 |
61 | npx electron-packager ./release/app librelinkupdesktop --overwrite --platform=linux --arch=x64 --out=release-build --prune=true --electron-version=31.3.1 --app-version=0.1.15
62 |
63 | cp -rv release-build/librelinkupdesktop-linux-x64 $SNAPCRAFT_PART_INSTALL/librelinkupdesktop
64 |
65 | mkdir -p $SNAPCRAFT_PART_INSTALL/librelinkupdesktop/resources/assets/sounds
66 | cp -rv assets/tray-logo.png $SNAPCRAFT_PART_INSTALL/librelinkupdesktop/resources/assets/ || true
67 | cp -rv assets/logo.svg $SNAPCRAFT_PART_INSTALL/librelinkupdesktop/resources/assets/ || true
68 | cp -rv assets/sounds/alert.mp3 $SNAPCRAFT_PART_INSTALL/librelinkupdesktop/resources/assets/sounds/ || true
69 |
70 | chmod -R 755 $SNAPCRAFT_PART_INSTALL/librelinkupdesktop
71 | build-snaps:
72 | - node/20/stable
73 | build-packages:
74 | - unzip
75 | stage-packages:
76 | - libnss3
77 | - libnspr4
78 |
--------------------------------------------------------------------------------
/flathub/rocks.poopjournal.librelinkupdesktop.metainfo.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | rocks.poopjournal.librelinkupdesktop
4 |
5 | LibreLinkUpDesktop
6 | This is a desktop application that fetches your blood sugar from LibreLinkUp
7 |
8 | MIT
9 | Apache-2.0
10 |
11 |
12 |
13 | This is a desktop application that fetches your blood sugar from LibreLinkUp.
14 |
15 |
16 | Features of LibreLinkUpDesktop
17 |
18 |
19 | - Show blood glucose level on your desktop in a little window
20 | - No tracking
21 | - Dark Mode
22 | - Libre software
23 | - That's it. 🩸
24 |
25 |
26 |
27 | rocks.poopjournal.librelinkupdesktop.desktop
28 |
29 |
30 |
31 | https://i.postimg.cc/k54sQ0tc/Login.jpg
32 | Login
33 |
34 |
35 | https://i.postimg.cc/3NPFYNLf/Main.jpg
36 | Main Screen
37 |
38 |
39 | https://i.postimg.cc/Bvb14W5B/Settings.png
40 | Settings
41 |
42 |
43 |
44 |
45 | diabetes
46 | librelink
47 | librelinkup
48 | blood sugar
49 | health
50 | desktop
51 | electron
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | https://github.com/Crazy-Marvin/LibreLinkUpDesktop
67 | https://github.com/Crazy-Marvin/LibreLinkUpDesktop/
68 | https://hosted.weblate.org/engage/librelinkupdesktop/
69 | https://poopjournal.rocks/blog/contact/
70 | https://poopjournal.rocks/donate/
71 | https://github.com/Crazy-Marvin/LibreLinkUpDesktop/issues
72 |
73 |
74 | Crazy Marvin & Contributors
75 |
76 |
77 |
--------------------------------------------------------------------------------
/src/main/alertHandler.ts:
--------------------------------------------------------------------------------
1 | import { ipcMain, app} from "electron";
2 | import { getMainWindow } from './main';
3 | import path from 'path';
4 | import fs from 'fs';
5 |
6 | const setupAlertSoundFile = () => {
7 | const appDataDir = app.getPath('userData');
8 | console.log('App Data directory:', appDataDir);
9 |
10 | const targetFilePath = path.join(appDataDir, 'alert.mp3');
11 |
12 | let sourceFilePath = path.join(process.resourcesPath, 'assets/sounds/alert.mp3');
13 | if (!app.isPackaged) {
14 | // development
15 | sourceFilePath = path.join(__dirname, '../../assets/sounds/alert.mp3');
16 | }
17 |
18 | if (!fs.existsSync(targetFilePath)) {
19 | try {
20 | fs.copyFileSync(sourceFilePath, targetFilePath);
21 | console.log('MP3 file copied to App Data directory:', targetFilePath);
22 | } catch (err) {
23 | console.error('Error copying MP3 file:', err);
24 | }
25 | } else {
26 | console.log('MP3 file already exists in App Data directory.');
27 | }
28 | }
29 |
30 | const getAudioFilePath = () => {
31 | const defaultPath = path.join(app.getPath('userData'), 'alert.mp3');
32 | const customPath = path.join(app.getPath('userData'), 'custom-alert.mp3');
33 |
34 | return {
35 | default: fs.existsSync(defaultPath) ? `file://${defaultPath}` : null,
36 | custom: fs.existsSync(customPath) ? `file://${customPath}` : null,
37 | };
38 | };
39 |
40 | const uploadCustomAlertSoundFile = (fileData: Array) => {
41 | const appDataDir = app.getPath('userData');
42 | const targetFilePath = path.join(appDataDir, 'custom-alert.mp3');
43 |
44 | try {
45 | const fileBuffer = Buffer.from(fileData);
46 | fs.writeFileSync(targetFilePath, fileBuffer);
47 | console.log('Custom MP3 file successfully moved to:', targetFilePath);
48 | return targetFilePath;
49 | } catch (error) {
50 | console.error('uploadCustomAlertSoundFile custom MP3 file:', error);
51 | throw new Error('Failed to move custom MP3 file.');
52 | }
53 | }
54 |
55 | export const registerAlertHandler = () => {
56 |
57 | setupAlertSoundFile()
58 |
59 | ipcMain.on("trigger-warning-alerts", (event, alertOptions) => {
60 | const mainWindow = getMainWindow();
61 | if (mainWindow && alertOptions.bringToFrontEnabled) {
62 | mainWindow.setAlwaysOnTop(true);
63 | mainWindow.show();
64 | mainWindow.setAlwaysOnTop(false);
65 | mainWindow.focus();
66 | }
67 | if(mainWindow && alertOptions.flashWindowEnabled) {
68 | mainWindow.flashFrame(true);
69 | }
70 | });
71 |
72 | ipcMain.handle('get-alert-sound-file', async () => {
73 | const audioFilePath = getAudioFilePath();
74 | return audioFilePath;
75 | });
76 |
77 | ipcMain.handle('upload-custom-alert-sound', async (event, fileData) => {
78 | const targetFilePath = uploadCustomAlertSoundFile(fileData);
79 | return targetFilePath;
80 | });
81 | };
82 |
83 | export const destroyAlertHandler = () => {
84 | ipcMain.removeAllListeners("set-custom-sound");
85 | ipcMain.removeAllListeners("trigger-warning-alerts");
86 | }
87 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | out/
3 | dist/
4 | node_modules/
5 | coverage/
6 | npm-debug.log
7 | yarn-error.log
8 | .awcache
9 | .idea/
10 | .vs/
11 | .vscode/*.log
12 | .eslintcache
13 | *.iml
14 | .envrc
15 | junit*.xml
16 | *.swp
17 | tslint-rules/
18 | *.css.d.ts
19 | *.sass.d.ts
20 | *.scss.d.ts
21 |
22 | # Logs
23 | logs
24 | *.log
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 | lerna-debug.log*
29 | .pnpm-debug.log*
30 |
31 | # Diagnostic reports (https://nodejs.org/api/report.html)
32 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
33 |
34 | # Runtime data
35 | pids
36 | *.pid
37 | *.seed
38 | *.pid.lock
39 |
40 | # Directory for instrumented libs generated by jscoverage/JSCover
41 | lib-cov
42 |
43 | # Coverage directory used by tools like istanbul
44 | coverage
45 | *.lcov
46 |
47 | # nyc test coverage
48 | .nyc_output
49 |
50 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
51 | .grunt
52 |
53 | # Bower dependency directory (https://bower.io/)
54 | bower_components
55 |
56 | # node-waf configuration
57 | .lock-wscript
58 |
59 | # Compiled binary addons (https://nodejs.org/api/addons.html)
60 | build/Release
61 | release/app/dist
62 | release/build
63 | .erb/dll
64 |
65 | # Dependency directories
66 | node_modules/
67 | jspm_packages/
68 |
69 | # Snowpack dependency directory (https://snowpack.dev/)
70 | web_modules/
71 |
72 | # TypeScript cache
73 | *.tsbuildinfo
74 |
75 | # Optional npm cache directory
76 | .npm
77 |
78 | # Optional eslint cache
79 | .eslintcache
80 |
81 | # Optional stylelint cache
82 | .stylelintcache
83 |
84 | # Microbundle cache
85 | .rpt2_cache/
86 | .rts2_cache_cjs/
87 | .rts2_cache_es/
88 | .rts2_cache_umd/
89 |
90 | # Optional REPL history
91 | .node_repl_history
92 |
93 | # Output of 'npm pack'
94 | *.tgz
95 |
96 | # Yarn Integrity file
97 | .yarn-integrity
98 |
99 | # dotenv environment variable files
100 | .env
101 | .env.development.local
102 | .env.test.local
103 | .env.production.local
104 | .env.local
105 |
106 | # parcel-bundler cache (https://parceljs.org/)
107 | .cache
108 | .parcel-cache
109 |
110 | # Next.js build output
111 | .next
112 | out
113 |
114 | # Nuxt.js build / generate output
115 | .nuxt
116 | dist
117 |
118 | # Gatsby files
119 | .cache/
120 | # Comment in the public line in if your project uses Gatsby and not Next.js
121 | # https://nextjs.org/blog/next-9-1#public-directory-support
122 | # public
123 |
124 | # vuepress build output
125 | .vuepress/dist
126 |
127 | # vuepress v2.x temp and cache directory
128 | .temp
129 | .cache
130 |
131 | # Docusaurus cache and generated files
132 | .docusaurus
133 |
134 | # Serverless directories
135 | .serverless/
136 |
137 | # FuseBox cache
138 | .fusebox/
139 |
140 | # DynamoDB Local files
141 | .dynamodb/
142 |
143 | # TernJS port file
144 | .tern-port
145 |
146 | # Stores VSCode versions used for testing VSCode extensions
147 | .vscode-test
148 |
149 | # yarn v2
150 | .yarn/cache
151 | .yarn/unplugged
152 | .yarn/build-state.yml
153 | .yarn/install-state.gz
154 | .pnp.*
155 |
156 | # macOS
157 | .DS_Store
158 |
--------------------------------------------------------------------------------
/src/renderer/components/ui/toggle-switch.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 | import { cn } from "@/lib/utils";
4 |
5 | const toggleVariants = cva(
6 | "relative rounded-full transition-colors text-sm",
7 | {
8 | variants: {
9 | variant: {
10 | default: "bg-red-300 dark:bg-secondary peer-checked:bg-primary",
11 | secondary: "bg-secondary peer-checked:bg-secondary-foreground",
12 | destructive: "bg-destructive peer-checked:bg-destructive-foreground",
13 | outline:
14 | "border border-input bg-transparent peer-checked:bg-accent peer-checked:border-accent-foreground",
15 | transparent: "bg-transparent peer-checked:bg-primary/10",
16 | ghost: "bg-transparent peer-checked:bg-accent-foreground",
17 | },
18 | size: {
19 | default: "w-11 h-6",
20 | sm: "w-9 h-5",
21 | lg: "w-14 h-7",
22 | },
23 | },
24 | defaultVariants: {
25 | variant: "default",
26 | size: "default",
27 | },
28 | }
29 | );
30 |
31 | const toggleKnobVariants = cva(
32 | "absolute bg-white border rounded-full transition-transform top-0.5 left-0.5 w-5 h-5 border-gray-300 dark:border-gray-400",
33 | {
34 | variants: {
35 | checked: {
36 | true: "translate-x-[100%]",
37 | false: "",
38 | },
39 | },
40 | defaultVariants: {
41 | checked: false,
42 | },
43 | }
44 | );
45 |
46 | interface ToggleSwitchProps {
47 | variant?: string;
48 | size?: string;
49 | className?: string;
50 | leftLabel?: string;
51 | rightLabel?: string;
52 | checked?: boolean; // Initial state
53 | onChange?: (checked: boolean) => void; // pass state to parent
54 | }
55 |
56 | const ToggleSwitch: React.FC = ({
57 | variant,
58 | size,
59 | className,
60 | leftLabel,
61 | rightLabel,
62 | onChange,
63 | checked: initialChecked,
64 | ...props
65 | }) => {
66 | const [isChecked, setIsChecked] = useState(initialChecked || false);
67 |
68 | const handleToggle = () => {
69 | setIsChecked((prev) => {
70 | const newState = !prev;
71 | if (onChange) onChange(newState);
72 | return newState;
73 | });
74 | };
75 |
76 | return (
77 |
78 | {leftLabel && (
79 |
{leftLabel}
80 | )}
81 |
97 | {rightLabel && (
98 |
{rightLabel}
99 | )}
100 |
101 | );
102 | };
103 |
104 |
105 | export { ToggleSwitch, toggleVariants };
106 |
--------------------------------------------------------------------------------
/src/renderer/i18n/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "Welcome": "This is a desktop application that fetches your blood sugar from LibreLinkUp",
3 | "Settings": "Settings",
4 | "General": "General",
5 | "Account": "Account",
6 |
7 | "First Name": "First Name",
8 | "Last Name": "Last Name",
9 |
10 | "System": "System",
11 | "Dark": "Dark",
12 | "Light": "Light",
13 |
14 | "German": "German",
15 | "English": "English",
16 | "Sinhala": "Sinhala",
17 | "Norwegian": "Norwegian",
18 | "Spanish": "Spanish",
19 | "Czech": "Czech",
20 | "Russian": "Russian",
21 | "Bosnian": "Bosnian",
22 | "Greek": "Greek",
23 | "French": "French",
24 | "Italian": "Italian",
25 | "Tamil": "Tamil",
26 | "Swedish": "Swedish",
27 | "Japanese": "Japanese",
28 | "Hindi": "Hindi",
29 | "Portuguese": "Portuguese",
30 | "Chinese": "Chinese",
31 | "Bengali": "Bengali",
32 |
33 | "Global": "Global",
34 | "Germany": "Germany",
35 | "European Union": "European Union",
36 | "European Union 2": "European Union 2",
37 | "United States": "United States",
38 | "Asia/Pacific": "Asia/Pacific",
39 | "Canada": "Canada",
40 | "Japan": "Japan",
41 | "United Arab Emirates": "United Arab Emirates",
42 | "France": "France",
43 | "Australia": "Australia",
44 |
45 | "getCredentialsTitle": "How to get credentials",
46 | "getCredentialsStep1": "As sharing person, open your <1>Libre smartphone app1>, go to <3>Connected Apps3>, click on <5>Manage5> next to LibreLinkUp, click on <7>Add connection7> and input the details for the account you wish to use with LibreLinkUpDesktop.",
47 | "getCredentialsStep1Link": "https://play.google.com/store/apps/details?id=com.freestylelibre3.app.de",
48 | "getCredentialsStep2": "Save those credentials inside your password manager. You may use them for yourself or you may share them with someone.",
49 | "getCredentialsStep3": "Enter those credentials on this login page.",
50 | "getCredentialsStep4": "That's it. 😄",
51 |
52 | "Username": "Username",
53 | "Password": "Password",
54 | "Login": "Log in",
55 | "Country": "Country",
56 | "Language": "Language",
57 | "SelectCountry": "Select Country",
58 | "SelectLanguage": "Select Language",
59 | "Theme": "Theme",
60 | "Unit": "Unit",
61 |
62 | "SelectTheme" : "Select Theme",
63 | "SelectUnit" : "Select Unit",
64 | "SelectMode" : "Select Mode",
65 | "WindowMode" : "Window Mode",
66 |
67 | "Overlay" : "Overlay",
68 | "Overlay (Transparent)" : "Overlay (Transparent)",
69 | "Windowed": "Windowed",
70 |
71 | "mmol/L": "mmol/L",
72 | "mg/dL": "mg/dL",
73 |
74 | "Alert": "Alert",
75 | "ALERT_DESCRIPTION": "LibreLinkUpDesktop can alert you when your blood sugar level is outside of a normal range. Choose how you'll be alerted and adjust the normal range below.",
76 | "Bring Window to Front": "Bring Window to Front",
77 | "Flash Window": "Flash Window",
78 | "Play Sound": "Play Sound",
79 | "Use Custom Sound": "Use Custom Sound",
80 | "Upload Sound": "Upload Sound",
81 | "Custom Alert Sound" : "Custom Alert Sound",
82 | "Custom Alert Level": "Custom Alert Level",
83 | "Min Value" : "Min Value",
84 | "Max Value" : "Max Value",
85 | "Enter Value" : "Enter Value",
86 | "Apply Changes": "Apply Changes",
87 | "Show glucose values in tray": "System Tray"
88 | }
89 |
--------------------------------------------------------------------------------
/.github/UPDATING-FLATHUB-RELEASE.md:
--------------------------------------------------------------------------------
1 | # LibreLinkUpDesktop Flathub Update Guide
2 |
3 | ## Important
4 |
5 | Before proceeding with the Flathub build manifest update, it is crucial to test the build and run it locally.
6 |
7 | ## Prerequisites
8 |
9 | Ensure the following tools are installed on your system:
10 | - Flatpak
11 | - Flatpak Builder
12 | - org.electronjs.Electron2.BaseApp//23.08
13 | - org.freedesktop.Sdk.Extension.node20//23.08
14 |
15 | Execute the following commands to install the necessary tools:
16 |
17 | ```bash
18 | sudo apt install flatpak
19 | flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
20 | flatpak install -y flathub org.flatpak.Builder
21 | flatpak install flathub org.electronjs.Electron2.BaseApp//23.08
22 | flatpak install flathub org.freedesktop.Sdk.Extension.node20//23.08
23 | sudo apt install flatpak-builder
24 | ```
25 |
26 | ## Process
27 |
28 | 1. **Clone the Repository:**
29 | Clone or fork the Flathub repository from https://github.com/flathub/rocks.poopjournal.librelinkupdesktop.
30 |
31 | 2. **Update the Manifest:**
32 | Modify the manifest file with the new tag and commit the changes.
33 |
34 | 3. **Generate Generated Sources:**
35 | To build locally, you may copy the generated sources from https://github.com/Crazy-Marvin/LibreLinkUpDesktop or regenerate a new file as described in the "Updating Node Modules" section.
36 |
37 | 4. **Build and Install:**
38 | Execute the command `flatpak-builder --user --install --force-clean build rocks.poopjournal.librelinkupdesktop.yml` to build and install the application.
39 |
40 | 5. **Run the Application:**
41 | Launch the application using the command `flatpak run rocks.poopjournal.librelinkupdesktop`.
42 |
43 | 6. **Finalize the Update:**
44 | If the application runs successfully, commit and push the changes (`generated-sources.json` and `rocks.poopjournal.librelinkupdesktop.yml` files) to the repository. Then, create a pull request to the master branch for the new release.
45 |
46 | ## Updating Node Modules
47 |
48 | If the node modules have been updated, follow these steps to update the `generated-sources.json` file.
49 |
50 | ### Prerequisites
51 | - Python 3.8
52 | - pipx
53 | - flatpak-node-generator
54 |
55 | #### Setup
56 |
57 | 1. Clone the Flatpak Builder Tools repository from https://github.com/flatpak/flatpak-builder-tools.
58 |
59 | 2. Navigate to the `node` directory and install the tools using `pipx install .`. For more details, refer to the README at https://github.com/flatpak/flatpak-builder-tools/blob/master/node/README.md.
60 |
61 | 3. Ensure your PATH is correctly set up with `pipx ensurepath`.
62 |
63 | ### Process
64 |
65 | 1. **Clone the Source Repository:**
66 | Clone the LibreLinkUpDesktop source repository from https://github.com/Crazy-Marvin/LibreLinkUpDesktop.
67 |
68 | 2. **Prepare the Directory:**
69 | Ensure the `node_modules` folder does not exist in your clone of the repository.
70 |
71 | 3. **Generate Sources:**
72 | Run `flatpak-node-generator npm package-lock.json` to generate the new sources.
73 |
74 | 4. **Test the Build Locally:**
75 | Follow the guide above to test the build locally.
76 |
77 | 5. **Update the Repository:**
78 | Commit the updated `generated-sources.json` file to the LibreLinkUpDesktop repository.
79 |
80 | Note: Ensure the `generated-sources.json` file is present in both the Flathub repository and the upstream repository.
81 |
--------------------------------------------------------------------------------
/src/renderer/i18n/it.json:
--------------------------------------------------------------------------------
1 | {
2 | "Bosnian": "Bosniaco",
3 | "System": "Sistema",
4 | "Dark": "Scuro",
5 | "SelectLanguage": "Seleziona lingua",
6 | "Theme": "Tema",
7 | "Unit": "Unità",
8 | "SelectTheme": "Seleziona tema",
9 | "SelectUnit": "Seleziona unità",
10 | "SelectMode": "Seleziona modalità",
11 | "WindowMode": "Modalità finestra",
12 | "Overlay": "Sovrapposizione",
13 | "Overlay (Transparent)": "Sovrapposizione (trasparente)",
14 | "Windowed": "Finestrato",
15 | "Welcome": "Questa è un'applicazione desktop che recupera la glicemia da LibreLinkUp",
16 | "General": "Generale",
17 | "Account": "Account",
18 | "First Name": "Nome",
19 | "Settings": "Impostazioni",
20 | "Last Name": "Cognome",
21 | "Light": "Chiaro",
22 | "German": "Tedesco",
23 | "English": "Inglese",
24 | "Sinhala": "Singalese",
25 | "Spanish": "Spagnolo",
26 | "Russian": "Russo",
27 | "Australia": "Australia",
28 | "Norwegian": "Norvegese",
29 | "Czech": "Ceco",
30 | "Greek": "Greco",
31 | "Germany": "Germania",
32 | "European Union": "Unione Europea",
33 | "European Union 2": "Unione Europea 2",
34 | "Asia/Pacific": "Asia/Pacifico",
35 | "France": "Francia",
36 | "United States": "Stati Uniti",
37 | "Canada": "Canada",
38 | "Japan": "Giappone",
39 | "United Arab Emirates": "Emirati Arabi Uniti",
40 | "getCredentialsTitle": "Come ottenere le credenziali",
41 | "getCredentialsStep1": "Come persona che condivide, apri l'<1>app per smartphone Libre1>, vai su <3>App connesse3>, clicca su <5>Gestisci5> accanto a LibreLinkUp, clicca su <7>Aggiungi connessione7> e inserisci i dettagli dell'account che desideri utilizzare con LibreLinkUpDesktop.",
42 | "Username": "Nome utente",
43 | "getCredentialsStep1Link": "https://play.google.com/store/apps/details?id=com.freestylelibre3.app.de",
44 | "getCredentialsStep2": "Salva queste credenziali nel tuo gestore password. Puoi usarle per te stesso o condividerle con qualcuno.",
45 | "getCredentialsStep3": "Inserisci le credenziali in questa pagina di accesso.",
46 | "getCredentialsStep4": "Questo è tutto. 😄",
47 | "Password": "Password",
48 | "Login": "Log in",
49 | "Country": "Paese",
50 | "Language": "Lingua",
51 | "SelectCountry": "Seleziona Paese",
52 | "mmol/L": "mmol/L",
53 | "mg/dL": "mg/dL",
54 | "Alert": "Avviso",
55 | "Use Custom Sound": "Usa suono personalizzato",
56 | "Min Value": "Valore min",
57 | "Max Value": "Valore max",
58 | "French": "Francese",
59 | "Italian": "Italiano",
60 | "Swedish": "Svedese",
61 | "Tamil": "Tamil",
62 | "ALERT_DESCRIPTION": "LibreLinkUp Desktop può avvisarti quando il tuo livello di zucchero nel sangue è fuori da un intervallo normale. Scegli come essere avvisato e regola l'intervallo normale qui sotto.",
63 | "Bring Window to Front": "Porta la finestra in primo piano",
64 | "Flash Window": "Finestra lampeggiante",
65 | "Play Sound": "Riproduci suono",
66 | "Upload Sound": "Carica suono",
67 | "Custom Alert Sound": "Suono di avviso personalizzato",
68 | "Custom Alert Level": "Livello di avviso personalizzato",
69 | "Enter Value": "Inserisci valore",
70 | "Apply Changes": "Applica modifiche",
71 | "Global": "Globale",
72 | "Hindi": "Hindi",
73 | "Portuguese": "Portoghese",
74 | "Chinese": "Cinese",
75 | "Show glucose values in tray": "Barra delle applicazioni",
76 | "Japanese": "Giapponese",
77 | "Bengali": "Bengalese"
78 | }
79 |
--------------------------------------------------------------------------------
/.github/FLATHUB_BUILD_SYNC_QUICKSTART.md:
--------------------------------------------------------------------------------
1 | # Flathub Build & Sync — Detailed Quick Start
2 |
3 | A step-by-step guide to generate `generated-sources.json`, sync it to your Flathub fork (**TARGET_REPO**), update the Flatpak manifest with the latest commit from your **SOURCE_REPO**, and open a Pull Request to upstream — all via **one GitHub Actions workflow**.
4 |
5 | ---
6 |
7 | ## Terminology
8 |
9 | - **SOURCE_REPO** — Your application repository that contains this workflow.
10 | - **TARGET_REPO** — Your fork of the Flathub repo (`rocks.poopjournal.librelinkupdesktop`) where the workflow will push updates.
11 | - **Upstream** — `flathub/rocks.poopjournal.librelinkupdesktop`.
12 |
13 | ---
14 |
15 | ## 1) Fork upstream once
16 |
17 | Fork `https://github.com/flathub/rocks.poopjournal.librelinkupdesktop` to **your account/org**.
18 | This fork is your **`TARGET_REPO`** (example notation: `/rocks.poopjournal.librelinkupdesktop`).
19 |
20 | > If your fork’s default branch is `main`, update the workflow’s `ref:` values accordingly.
21 |
22 | ---
23 |
24 | ## 2) Add a secret in your SOURCE_REPO
25 |
26 | Create a repository secret named **`TARGET_REPO_TOKEN`** that can **push to `TARGET_REPO`** (e.g., a bot/service token with minimal required permissions).
27 |
28 | - Go to **SOURCE_REPO → Settings → Secrets and variables → Actions → New repository secret**
29 | - **Name:** `TARGET_REPO_TOKEN`
30 | - **Value:** a token that can push to your fork (**TARGET_REPO**)
31 |
32 | Also set **Settings → Actions → General → Workflow permissions** to **Read and write**.
33 |
34 | > The built-in `GITHUB_TOKEN` will handle commits back to **SOURCE_REPO**.
35 | > `TARGET_REPO_TOKEN` is only used to push to your fork (**TARGET_REPO**).
36 |
37 | ---
38 |
39 | ## 3) Run the workflow
40 |
41 | Trigger the action by either:
42 | - **Run workflow** (`workflow_dispatch`)
43 | - **Create/Edit a Release**
44 | - **Push a tag** matching `flathubbuild-*`, for example:
45 | ```bash
46 | git tag flathubbuild-$(date +%Y%m%d-%H%M%S)
47 | git push origin --tags
48 | ```
49 |
50 | ---
51 |
52 | ## 4) What the workflow does (at a glance)
53 |
54 | 1. **Installs Flatpak & tooling** (SDK 23.08, Node 20 extension, Builder).
55 | 2. **Runs `npm install`** to ensure a valid lockfile and dependency graph.
56 | 3. **Generates `generated-sources.json`** via `flatpak-node-generator`.
57 | 4. **Commits to SOURCE_REPO** if `generated-sources.json` changed (also uploaded as an artifact).
58 | 5. **Checks out TARGET_REPO** (your fork) and copies `generated-sources.json` into it.
59 | 6. **Updates the Flatpak manifest** (`rocks.poopjournal.librelinkupdesktop.yml`) so the `librelinkupdesktop` module’s `git` source:
60 | - **url** → `https://github.com/${SOURCE_REPO}.git`
61 | - **commit** → latest commit SHA from SOURCE_REPO
62 | 7. **Commits & pushes** those changes to your fork (**TARGET_REPO**).
63 |
64 | ---
65 |
66 | ## 5) Open the PR
67 |
68 | After a successful run, open a Pull Request **from your fork (`TARGET_REPO`)** to **`flathub/rocks.poopjournal.librelinkupdesktop`** (usually **`master → master`**).
69 |
70 | **Via GitHub UI:**
71 | - Go to your fork → **Compare & pull request**
72 | - Base: `flathub/rocks.poopjournal.librelinkupdesktop@master`
73 | - Head: `/rocks.poopjournal.librelinkupdesktop@master`
74 |
75 | **Via CLI:**
76 | ```bash
77 | gh pr create \
78 | --repo flathub/rocks.poopjournal.librelinkupdesktop \
79 | --head :master \
80 | --base master \
81 | --title "Update generated-sources.json and manifest to latest SOURCE_REPO" \
82 | --body "Automated update via Build & Sync workflow."
83 | ```
84 |
85 | ---
86 |
--------------------------------------------------------------------------------
/src/renderer/i18n/de.json:
--------------------------------------------------------------------------------
1 | {
2 | "Welcome": "Dies ist eine Desktop-Anwendung, die Ihren Blutzucker über LibreLinkUp abruft",
3 | "Settings": "Einstellungen",
4 | "English": "Englisch",
5 | "Sinhala": "Singhalesisch",
6 | "Last Name": "Nachname",
7 | "Germany": "Deutschland",
8 | "General": "Allgemein",
9 | "Light": "Hell",
10 | "Norwegian": "Norwegisch",
11 | "System": "System",
12 | "First Name": "Vorname",
13 | "Account": "Konto",
14 | "Dark": "Dunkel",
15 | "Australia": "Australien",
16 | "European Union": "Europäische Union",
17 | "France": "Frankreich",
18 | "Canada": "Kanada",
19 | "Asia/Pacific": "Asien/Pazifik",
20 | "United States": "Vereinigte Staaten",
21 | "European Union 2": "Europäische Union 2",
22 | "German": "Deutsch",
23 | "Japan": "Japan",
24 | "United Arab Emirates": "Vereinigte Arabische Emirate",
25 | "SelectCountry": "Land wählen",
26 | "getCredentialsTitle": "Anmeldeinformationen erhalten",
27 | "getCredentialsStep1Link": "https://play.google.com/store/apps/details?id=com.freestylelibre3.app.de",
28 | "getCredentialsStep2": "Speichern Sie diese Anmeldedaten in Ihrem Passwort-Manager. Sie können sie für sich selbst verwenden oder sie an andere weitergeben.",
29 | "getCredentialsStep3": "Geben Sie diese Anmeldedaten auf dieser Anmeldeseite ein.",
30 | "getCredentialsStep4": "Das war's schon. 😄",
31 | "Username": "Benutzername",
32 | "Password": "Passwort",
33 | "Login": "Anmelden",
34 | "Country": "Land",
35 | "Language": "Sprache",
36 | "SelectLanguage": "Sprache wählen",
37 | "getCredentialsStep1": "Öffnen Sie als teilende Person Ihre <1>Libre Smartphone-App1>, gehen Sie zu <3>Verbundene Apps3>, klicken Sie auf <5>Verwalten5> neben LibreLinkUp, klicken Sie auf <7>Verbindung hinzufügen7> und geben Sie die Details für das Konto ein, das Sie mit LibreLinkUpDesktop verwenden möchten.",
38 | "Theme": "Aussehen",
39 | "Unit": "Einheit",
40 | "SelectTheme": "Aussehen wählen",
41 | "SelectUnit": "Einheit wählen",
42 | "SelectMode": "Fenstermodus wählen",
43 | "WindowMode": "Fenstermodus",
44 | "Overlay (Transparent)": "Overlay (transparent)",
45 | "Windowed": "Festes Fenster",
46 | "Overlay": "Overlay (kleines Fenster)",
47 | "Bosnian": "Bosnisch",
48 | "Greek": "Griechisch",
49 | "Spanish": "Spanisch",
50 | "Czech": "Tschechisch",
51 | "Russian": "Russisch",
52 | "Bring Window to Front": "Fenster nach vorne bringen",
53 | "Flash Window": "Fenster blinken lassen",
54 | "Play Sound": "Ton abspielen",
55 | "Custom Alert Level": "Eigener Warnbereich",
56 | "Min Value": "Niedriger Wert",
57 | "Apply Changes": "Änderungen übernehmen",
58 | "Use Custom Sound": "Eigenen Ton verwenden",
59 | "mg/dL": "mg/dL",
60 | "Enter Value": "Wert eintragen",
61 | "mmol/L": "mmol/L",
62 | "Upload Sound": "Ton hinterlegen",
63 | "Custom Alert Sound": "Eigener Alarmton",
64 | "ALERT_DESCRIPTION": "LibreLinkUpDesktop kann warnen, wenn der Blutzuckerspiegel außerhalb des normalen Bereichs liegt. Der Zielbereich wird von LibreLinkUp übernommen und kann unten bei Bedarf geändert werden.",
65 | "Alert": "Alarm",
66 | "Max Value": "Hoher Wert",
67 | "French": "Französisch",
68 | "Italian": "Italienisch",
69 | "Tamil": "Tamilisch",
70 | "Swedish": "Schwedisch",
71 | "Global": "Global",
72 | "Hindi": "Hindi",
73 | "Portuguese": "Portugiesisch",
74 | "Bengali": "Bengalisch",
75 | "Japanese": "Japanisch",
76 | "Chinese": "Chinesisch",
77 | "Show glucose values in tray": "Glukosewerte im Tray anzeigen"
78 | }
79 |
--------------------------------------------------------------------------------
/src/renderer/i18n/ru.json:
--------------------------------------------------------------------------------
1 | {
2 | "getCredentialsStep2": "Сохраните эти учетные данные в менеджере паролей. Вы можете использовать их для себя или поделиться ими с кем-то.",
3 | "getCredentialsStep3": "Введите эти учетные данные на этой странице входа в систему.",
4 | "getCredentialsStep4": "Вот и все. 😄",
5 | "Username": "Имя пользователя",
6 | "Password": "Пароль",
7 | "Login": "Войти",
8 | "Country": "Страна",
9 | "Language": "Язык",
10 | "SelectCountry": "Выберите страну",
11 | "SelectLanguage": "Выберите язык",
12 | "Welcome": "Это приложение для настольных компьютеров, которое получает данные о вашем уровне сахара в крови из LibreLinkUp",
13 | "Settings": "Настройки",
14 | "General": "Общий",
15 | "Account": "Аккаунт",
16 | "First Name": "Имя",
17 | "Last Name": "Фамилия",
18 | "System": "Система",
19 | "Dark": "Темный",
20 | "Light": "Свет",
21 | "German": "Немецкий",
22 | "English": "Английский",
23 | "Sinhala": "Синхала",
24 | "Norwegian": "Норвежский",
25 | "Germany": "Германия",
26 | "European Union": "Европейский союз",
27 | "European Union 2": "Европейский союз 2",
28 | "United States": "Соединенные Штаты",
29 | "Asia/Pacific": "Азия/Тихий океан",
30 | "Canada": "Канада",
31 | "Japan": "Япония",
32 | "United Arab Emirates": "Объединенные Арабские Эмираты",
33 | "France": "Франция",
34 | "Australia": "Австралия",
35 | "getCredentialsTitle": "Как получить удостоверение",
36 | "getCredentialsStep1": "В качестве участника совместного доступа откройте приложение <1>Libre для смартфона1>, перейдите в раздел <3>Подключенные приложения3>, нажмите на <5>Управление5> рядом с LibreLinkUp, нажмите на <7>Добавить подключение7> и введите данные учетной записи, которую вы хотите использовать с LibreLinkUpDesktop.",
37 | "getCredentialsStep1Link": "https://play.google.com/store/apps/details?id=com.freestylelibre2.app.ru",
38 | "Theme": "Тема",
39 | "Unit": "Единица",
40 | "Spanish": "испанский",
41 | "Czech": "чешский",
42 | "Russian": "Русский",
43 | "Bosnian": "боснийский",
44 | "SelectTheme": "Выбрать тему",
45 | "Greek": "греческий",
46 | "SelectMode": "Выбрать режим",
47 | "SelectUnit": "Выберите единицу измерения",
48 | "WindowMode": "Режим окна",
49 | "Overlay": "Наложение",
50 | "Overlay (Transparent)": "Наложение (прозрачное)",
51 | "Windowed": "С окном",
52 | "Alert": "Оповещение",
53 | "Bring Window to Front": "Перенесите окно на фасад",
54 | "Play Sound": "Играть Звук",
55 | "Use Custom Sound": "Использовать пользовательский звук",
56 | "Upload Sound": "Загрузка звука",
57 | "Custom Alert Level": "Пользовательский уровень оповещения",
58 | "Min Value": "Минимальное значение",
59 | "Max Value": "Максимальное значение",
60 | "Enter Value": "Введите значение",
61 | "Apply Changes": "Применить изменения",
62 | "mg/dL": "мг/дл",
63 | "mmol/L": "ммоль/л",
64 | "ALERT_DESCRIPTION": "LibreLinkUpDesktop может предупредить вас о том, что уровень сахара в крови выходит за пределы нормального диапазона. Выберите способ оповещения и настройте нормальный диапазон ниже.",
65 | "Flash Window": "Окно флэш-памяти",
66 | "Custom Alert Sound": "Пользовательский звуковой сигнал",
67 | "Tamil": "Тамил",
68 | "French": "Французский",
69 | "Italian": "Итальянский",
70 | "Swedish": "Шведский",
71 | "Global": "Глобальная",
72 | "Portuguese": "португальский",
73 | "Chinese": "китайский",
74 | "Bengali": "Бенгальский",
75 | "Hindi": "хинди",
76 | "Show glucose values in tray": "Показать значения глюкозы в трее",
77 | "Japanese": "японский"
78 | }
79 |
--------------------------------------------------------------------------------
/src/renderer/i18n/fr.json:
--------------------------------------------------------------------------------
1 | {
2 | "Theme": "Thème",
3 | "SelectUnit": "Sélectionnez une unité",
4 | "First Name": "Prénom",
5 | "Password": "Mot de passe",
6 | "Bosnian": "Bosniaque",
7 | "Settings": "Paramètres",
8 | "General": "Général",
9 | "Last Name": "Nom",
10 | "System": "Système",
11 | "Dark": "Sombre",
12 | "Sinhala": "Singhalais",
13 | "Norwegian": "Norvégien",
14 | "Spanish": "Espagnol",
15 | "Czech": "Tchèque",
16 | "Russian": "Russe",
17 | "Greek": "Grec",
18 | "Germany": "Allemagne",
19 | "European Union": "Union Européenne",
20 | "European Union 2": "Union Européenne 2",
21 | "United States": "États-Unis",
22 | "getCredentialsTitle": "Comment obtenir les informations d'identification",
23 | "getCredentialsStep1": "En tant que personne partageant, ouvrez votre <1>application smartphone LibreLink1>, allez à <3>Applications connectées3>, cliquez sur <5>Gérer5> à côté de LibreLinkUp, cliquez sur <7>Ajouter une connexion7> et saisissez les détails du compte que vous souhaitez utiliser avec LibreLinkUpDesktop.",
24 | "getCredentialsStep1Link": "https://play.google.com/store/apps/details?id=com.freestylelibre3.app.de",
25 | "getCredentialsStep2": "Enregistrez ces informations d'identification dans votre gestionnaire de mots de passe. Vous pouvez les utiliser pour vous-même ou les partager avec quelqu'un.",
26 | "getCredentialsStep3": "Saisissez les informations d’identification sur cette page de connexion.",
27 | "getCredentialsStep4": "C'est tout. 😄",
28 | "Login": "Connexion",
29 | "Country": "Pays",
30 | "SelectCountry": "Sélectionnez un pays",
31 | "SelectTheme": "Sélectionnez un thème",
32 | "WindowMode": "Mode Fenêtre",
33 | "Overlay": "Superposition",
34 | "Canada": "Canada",
35 | "Light": "Clair",
36 | "Account": "Compte",
37 | "English": "Anglais",
38 | "France": "France",
39 | "Australia": "Australie",
40 | "Japan": "Japon",
41 | "Unit": "Unité",
42 | "SelectMode": "Sélectionnez un mode",
43 | "Overlay (Transparent)": "Superposition (Transparent)",
44 | "German": "Allemand",
45 | "United Arab Emirates": "Émirats Arabes Unis",
46 | "Language": "Langue",
47 | "Welcome": "Application pour bureau récupérant votre glycémie à partir de LibreLinkUp",
48 | "Asia/Pacific": "Asie/Pacifique",
49 | "Username": "Nom d'utilisateur",
50 | "Windowed": "Fenêtré",
51 | "SelectLanguage": "Sélectionnez une langue",
52 | "Global": "Mondiale",
53 | "Hindi": "Hindi",
54 | "Portuguese": "Portugais",
55 | "Bengali": "Bengali",
56 | "Japanese": "Japonaise",
57 | "Chinese": "Chinoise",
58 | "Play Sound": "Jouer du son",
59 | "Use Custom Sound": "Utiliser un son personnalisé",
60 | "French": "Français",
61 | "Italian": "Italien",
62 | "Tamil": "Tamoul",
63 | "Swedish": "Suédoise",
64 | "mmol/L": "mmol/L",
65 | "mg/dL": "mg/dL",
66 | "Alert": "Alerte",
67 | "ALERT_DESCRIPTION": "LibreLinkUpDesktop peut vous alerter lorsque votre glycémie est hors des limites normales. Choisissez votre mode d'alerte et ajustez les limites ci-dessous.",
68 | "Bring Window to Front": "Amener la fenêtre au premier plan",
69 | "Flash Window": "Fenêtres Flash",
70 | "Show glucose values in tray": "Afficher les valeurs de glucose dans le plateau",
71 | "Max Value": "Valeur maximale",
72 | "Custom Alert Sound": "Son d'alerte personnalisé",
73 | "Custom Alert Level": "Niveau d'alerte personnalisé",
74 | "Min Value": "Valeur minimale",
75 | "Upload Sound": "Télécharger du son",
76 | "Enter Value": "Entrez la valeur",
77 | "Apply Changes": "Appliquer les modifications"
78 | }
79 |
--------------------------------------------------------------------------------
/.erb/configs/webpack.config.renderer.prod.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Build config for electron renderer process
3 | */
4 |
5 | import path from 'path';
6 | import webpack from 'webpack';
7 | import HtmlWebpackPlugin from 'html-webpack-plugin';
8 | import MiniCssExtractPlugin from 'mini-css-extract-plugin';
9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
10 | import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
11 | import { merge } from 'webpack-merge';
12 | import TerserPlugin from 'terser-webpack-plugin';
13 | import baseConfig from './webpack.config.base';
14 | import webpackPaths from './webpack.paths';
15 | import checkNodeEnv from '../scripts/check-node-env';
16 | import deleteSourceMaps from '../scripts/delete-source-maps';
17 |
18 | checkNodeEnv('production');
19 | deleteSourceMaps();
20 |
21 | const configuration: webpack.Configuration = {
22 | devtool: 'source-map',
23 |
24 | mode: 'production',
25 |
26 | target: ['web', 'electron-renderer'],
27 |
28 | entry: [path.join(webpackPaths.srcRendererPath, 'index.tsx')],
29 |
30 | output: {
31 | path: webpackPaths.distRendererPath,
32 | publicPath: './',
33 | filename: 'renderer.js',
34 | library: {
35 | type: 'umd',
36 | },
37 | },
38 |
39 | module: {
40 | rules: [
41 | // Tailwind CSS
42 | {
43 | test: /\.css$/,
44 | include: [webpackPaths.srcRendererPath],
45 | use: ['style-loader', 'css-loader', 'postcss-loader'],
46 | },
47 | // Fonts
48 | {
49 | test: /\.(woff|woff2|eot|ttf|otf)$/i,
50 | type: 'asset/resource',
51 | },
52 | // Images
53 | {
54 | test: /\.(png|jpg|jpeg|gif)$/i,
55 | type: 'asset/resource',
56 | },
57 | // SVG
58 | {
59 | test: /\.svg$/,
60 | use: [
61 | {
62 | loader: '@svgr/webpack',
63 | options: {
64 | prettier: false,
65 | svgo: false,
66 | svgoConfig: {
67 | plugins: [{ removeViewBox: false }],
68 | },
69 | titleProp: true,
70 | ref: true,
71 | },
72 | },
73 | 'file-loader',
74 | ],
75 | },
76 | ],
77 | },
78 |
79 | optimization: {
80 | minimize: true,
81 | minimizer: [new TerserPlugin(), new CssMinimizerPlugin()],
82 | },
83 |
84 | plugins: [
85 | /**
86 | * Create global constants which can be configured at compile time.
87 | *
88 | * Useful for allowing different behaviour between development builds and
89 | * release builds
90 | *
91 | * NODE_ENV should be production so that modules do not perform certain
92 | * development checks
93 | */
94 | new webpack.EnvironmentPlugin({
95 | NODE_ENV: 'production',
96 | DEBUG_PROD: false,
97 | }),
98 |
99 | new MiniCssExtractPlugin({
100 | filename: 'style.css',
101 | }),
102 |
103 | new BundleAnalyzerPlugin({
104 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
105 | analyzerPort: 8889,
106 | }),
107 |
108 | new HtmlWebpackPlugin({
109 | filename: 'index.html',
110 | template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
111 | minify: {
112 | collapseWhitespace: true,
113 | removeAttributeQuotes: true,
114 | removeComments: true,
115 | },
116 | isBrowser: false,
117 | isDevelopment: false,
118 | }),
119 |
120 | new webpack.DefinePlugin({
121 | 'process.type': '"renderer"',
122 | }),
123 | ],
124 | };
125 |
126 | export default merge(baseConfig, configuration);
127 |
--------------------------------------------------------------------------------
/src/renderer/i18n/ta.json:
--------------------------------------------------------------------------------
1 | {
2 | "Overlay (Transparent)": "மேலடுக்கில் (வெளிப்படையானது)",
3 | "Windowed": "சாளரம்",
4 | "Welcome": "இது டெச்க்டாப் பயன்பாடாகும், இது உங்கள் இரத்த சர்க்கரையை லிப்ரெலிங்கப்பிலிருந்து பெறுகிறது",
5 | "Settings": "அமைப்புகள்",
6 | "General": "பொது",
7 | "Account": "கணக்கு",
8 | "First Name": "முதல் பெயர்",
9 | "Last Name": "கடைசி பெயர்",
10 | "System": "மண்டலம்",
11 | "Dark": "இருண்ட",
12 | "Light": "ஒளி",
13 | "German": "செர்மன்",
14 | "English": "ஆங்கிலம்",
15 | "Sinhala": "சிங்களம்",
16 | "Norwegian": "நோர்வே",
17 | "Spanish": "ச்பானிச்",
18 | "Czech": "செக்",
19 | "Russian": "ரச்ய",
20 | "Bosnian": "போச்னிய",
21 | "Greek": "கிரேக்கம்",
22 | "Germany": "செர்மனி",
23 | "European Union": "ஐரோப்பிய ஒன்றியம்",
24 | "European Union 2": "ஐரோப்பிய ஒன்றியம் 2",
25 | "United States": "ஐக்கிய அமெரிக்கா",
26 | "Asia/Pacific": "ஆசியா/பசிபிக்",
27 | "Canada": "கனடா",
28 | "Japan": "சப்பான்",
29 | "United Arab Emirates": "ஐக்கிய அராபிய எமிரேட்சு",
30 | "France": "ஃப்ரான்ச்",
31 | "Australia": "ஆச்திரேலியா",
32 | "getCredentialsTitle": "நற்சான்றிதழ்களை எவ்வாறு பெறுவது",
33 | "getCredentialsStep1": "நபரைப் பகிரும் நபராக, உங்கள் <1> லிப்ரே ச்மார்ட்போன் பயன்பாடு 1> ஐத் திறக்கவும், <3> இணைக்கப்பட்ட பயன்பாடுகளுக்குச் செல்லவும் 3>, <5> நிர்வகி 5> ஐக் சொடுக்கு செய்க, லிப்ரெலிங்கப்பிற்கு அடுத்து, <7> இணைப்பைச் சேர் என்பதைக் சொடுக்கு செய்க 7> மற்றும் நீங்கள் பயன்படுத்த விரும்பும் கணக்கிற்கான விவரங்களை லிப்ரெலிங்கப் டெச்க்டாப் மூலம் உள்ளிடவும்.",
34 | "getCredentialsStep1Link": "https://play.google.com/store/apps/details?id=com.freestylelibre3.app.de",
35 | "getCredentialsStep2": "உங்கள் கடவுச்சொல் நிர்வாகிக்குள் அந்த நற்சான்றிதழ்களைச் சேமிக்கவும். அவற்றை நீங்களே பயன்படுத்தலாம் அல்லது அவற்றை ஒருவருடன் பகிர்ந்து கொள்ளலாம்.",
36 | "getCredentialsStep3": "இந்த உள்நுழைவு பக்கத்தில் அந்த சான்றுகளை உள்ளிடவும்.",
37 | "getCredentialsStep4": "அவ்வளவுதான். 😄",
38 | "Username": "பயனர்பெயர்",
39 | "Password": "கடவுச்சொல்",
40 | "Login": "புகுபதிகை",
41 | "Country": "நாடு",
42 | "Language": "மொழி",
43 | "SelectCountry": "நாட்டைத் தேர்ந்தெடுக்கவும்",
44 | "SelectLanguage": "மொழியைத் தேர்ந்தெடுக்கவும்",
45 | "Theme": "கருப்பொருள்",
46 | "Unit": "அலகு",
47 | "SelectTheme": "கருப்பொருள் தேர்ந்தெடுக்கவும்",
48 | "SelectUnit": "அலகு தேர்ந்தெடுக்கவும்",
49 | "SelectMode": "பயன்முறையைத் தேர்ந்தெடுக்கவும்",
50 | "WindowMode": "சாளரம் பயன்முறை",
51 | "Overlay": "மேலடுக்கு",
52 | "French": "பிரஞ்சு",
53 | "Italian": "இத்தாலிய",
54 | "Tamil": "தமிழ்",
55 | "Swedish": "ச்வீடிச்",
56 | "Japanese": "சப்பானியர்கள்",
57 | "Hindi": "இந்தி",
58 | "Portuguese": "போர்த்துகீசியம்",
59 | "Chinese": "சீன",
60 | "Bengali": "பெங்காலி",
61 | "Global": "உலகளாவிய",
62 | "mmol/L": "mmol / l",
63 | "mg/dL": "எம்.சி/டி.எல்",
64 | "Alert": "விழிப்புணர்வு",
65 | "ALERT_DESCRIPTION": "உங்கள் இரத்த சர்க்கரை அளவு சாதாரண வரம்பிற்கு வெளியே இருக்கும்போது லிப்ரெலிங்கப் டெச்க்டாப் உங்களை எச்சரிக்கலாம். நீங்கள் எவ்வாறு மாற்றப்படுவீர்கள் என்பதைத் தேர்ந்தெடுத்து, கீழே உள்ள சாதாரண வரம்பை சரிசெய்யவும்.",
66 | "Bring Window to Front": "சாளரத்தை முன் கொண்டு வாருங்கள்",
67 | "Flash Window": "ஒளிரும் சாளரம்",
68 | "Play Sound": "ஒலி விளையாடுங்கள்",
69 | "Use Custom Sound": "தனிப்பயன் ஒலியைப் பயன்படுத்தவும்",
70 | "Upload Sound": "ஒலியைப் பதிவேற்றவும்",
71 | "Custom Alert Sound": "தனிப்பயன் முன்னறிவிப்பு ஒலி",
72 | "Custom Alert Level": "தனிப்பயன் முன்னறிவிப்பு நிலை",
73 | "Min Value": "குறைந்தபட்ச மதிப்பு",
74 | "Max Value": "அதிகபட்ச மதிப்பு",
75 | "Enter Value": "மதிப்பை உள்ளிடவும்",
76 | "Apply Changes": "மாற்றங்களைப் பயன்படுத்துங்கள்",
77 | "Show glucose values in tray": "தட்டில் குளுக்கோச் மதிப்புகளைக் காட்டு"
78 | }
79 |
--------------------------------------------------------------------------------
/.github/workflows/generate-update-flatpak-sources.yml:
--------------------------------------------------------------------------------
1 | name: Generate and Update Flatpak Sources On Main Repo
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'flathubbuild-*'
7 | - 'stage-flathubbuild*'
8 | pull_request:
9 | types: [assigned]
10 |
11 | jobs:
12 | generate-sources:
13 | runs-on: ubuntu-latest
14 | permissions:
15 | contents: write
16 |
17 | steps:
18 | - name: Checkout repository
19 | uses: actions/checkout@v5
20 | with:
21 | token: ${{ secrets.GITHUB_TOKEN }}
22 | fetch-depth: 0
23 |
24 | - name: Install system dependencies
25 | run: |
26 | sudo apt update
27 | sudo apt install -y flatpak flatpak-builder nodejs npm python3 python3-pip python3-venv
28 |
29 | - name: Setup Flatpak
30 | run: |
31 | sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
32 | sudo flatpak install -y flathub org.freedesktop.Platform//23.08
33 | sudo flatpak install -y flathub org.freedesktop.Sdk//23.08
34 | sudo flatpak install -y flathub org.flatpak.Builder
35 | sudo flatpak install -y flathub org.electronjs.Electron2.BaseApp//23.08
36 | sudo flatpak install -y flathub org.freedesktop.Sdk.Extension.node20//23.08
37 |
38 | - name: Install dependencies and verify package-lock.json
39 | run: |
40 | echo "=== Current directory contents ==="
41 | ls -la
42 |
43 | # Install dependencies to generate/update package-lock.json if needed
44 | npm install --legacy-peer-deps --no-audit --no-fund
45 |
46 | echo "=== Package-lock.json exists ==="
47 | [ -f package-lock.json ] && echo "package-lock.json found" || echo "package-lock.json not found"
48 |
49 | echo "=== Package-lock.json details ==="
50 | ls -la package-lock.json
51 |
52 | - name: Install flatpak-node-generator
53 | run: |
54 | python3 -m pip install --user pipx
55 | python3 -m pipx ensurepath
56 | export PATH="$HOME/.local/bin:$PATH"
57 |
58 | # Clone and install flatpak-builder-tools
59 | git clone https://github.com/flatpak/flatpak-builder-tools.git
60 | cd flatpak-builder-tools/node
61 | pipx install .
62 |
63 | echo "=== flatpak-node-generator installed ==="
64 | which flatpak-node-generator
65 |
66 | - name: Generate sources.json
67 | run: |
68 | echo "=== Current directory contents before generation ==="
69 | ls -la
70 |
71 | echo "=== Generating sources from package-lock.json ==="
72 | export PATH="$HOME/.local/bin:$PATH"
73 | flatpak-node-generator npm package-lock.json
74 |
75 | echo "=== Generated files ==="
76 | ls -la generated-sources.json || echo "No generated-sources.json created"
77 |
78 | # Verify the generated file exists
79 | [ -f generated-sources.json ] && echo "generated-sources.json successfully created" || exit 1
80 |
81 | - name: Verify generated-sources.json
82 | run: |
83 | echo "=== Verifying generated-sources.json ==="
84 | [ -f generated-sources.json ] && echo "generated-sources.json exists" || exit 1
85 |
86 | echo "=== File size ==="
87 | ls -la generated-sources.json
88 |
89 | echo "=== First few lines ==="
90 | head -20 generated-sources.json
91 |
92 | - name: Commit and push generated-sources.json
93 | run: |
94 | git config --local user.email "action@github.com"
95 | git config --local user.name "GitHub Action"
96 | git add generated-sources.json
97 |
98 | # Check if there are changes to commit
99 | if ! git diff --staged --quiet; then
100 | git commit -m "Update generated-sources.json for Flatpak build [skip ci]"
101 | git push
102 | echo "Successfully pushed updated generated-sources.json to repository"
103 | else
104 | echo "No changes to generated-sources.json - skipping commit"
105 | fi
106 |
107 | - name: Upload generated-sources.json as artifact
108 | uses: actions/upload-artifact@v5
109 | with:
110 | name: generated-sources-json
111 | path: generated-sources.json
112 | retention-days: 7
113 |
--------------------------------------------------------------------------------
/src/renderer/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 | import { useAuthStore } from '../stores/auth'
4 | import CryptoJS from 'crypto-js';
5 | import { getCGMData } from '@/lib/linkup';
6 |
7 | export function cn(...inputs: ClassValue[]) {
8 | return twMerge(clsx(inputs))
9 | }
10 |
11 | export async function openFile(type: string, folder: string, group: string, filename: string) {
12 | await window.electron.ipcRenderer.invoke('ipc-open-file', type, folder, group, filename)
13 | }
14 |
15 | export async function openNewWindow(path: string, width: number, height: number) {
16 | await window.electron.ipcRenderer.sendMessage('open-new-window', path, width, height)
17 | }
18 |
19 | export async function setWindowMode(mode: string) {
20 | await window.electron.ipcRenderer.sendMessage('set-window-mode', mode)
21 | }
22 |
23 | export async function getWindowMode() {
24 | return await window.electron.ipcRenderer.invoke('get-window-mode')
25 | }
26 |
27 | export async function setLocalStorageWindowMode(mode: string) {
28 | await localStorage.setItem('windowMode', mode);
29 | }
30 |
31 | export async function getLocalStorageWindowMode(): Promise {
32 | return await localStorage.getItem('windowMode') as string;
33 | }
34 |
35 | export async function setRedirectTo (path: string) {
36 | await localStorage.setItem('redirectTo', path)
37 | }
38 |
39 | export async function getRedirectTo () {
40 | return localStorage.getItem('redirectTo')
41 | }
42 |
43 | export async function clearRedirectTo () {
44 | await localStorage.removeItem('redirectTo')
45 | }
46 |
47 | export function sendLogout() {
48 | setTrayVisibility(false);
49 | window.electron.ipcRenderer.sendMessage('logout')
50 | }
51 |
52 | export function sendRefreshAllWindows() {
53 | window.electron.ipcRenderer.sendMessage('refresh-all')
54 | }
55 |
56 | export function sendRefreshPrimaryWindow() {
57 | window.electron.ipcRenderer.sendMessage('refresh-primary')
58 | }
59 |
60 | export function triggerWarningAlert(alertOptions: any){
61 | window.electron.ipcRenderer.sendMessage('trigger-warning-alerts', alertOptions)
62 | }
63 |
64 | export async function getAlertSoundFile() {
65 | return await window.electron.ipcRenderer.invoke('get-alert-sound-file')
66 | }
67 |
68 | export async function uploadCustomAlertSoundFile(fileData: Array) {
69 | return await window.electron.ipcRenderer.invoke(
70 | 'upload-custom-alert-sound',
71 | fileData,
72 | );
73 | }
74 |
75 | export function getUserValue(value: number): number {
76 | const { resultUnit } = useAuthStore.getState()
77 |
78 | if (resultUnit === 'mg/dL') {
79 | return Math.round(value)
80 | }
81 |
82 | if (resultUnit === 'mmol/L') {
83 | const convertedValue = value / 18.0182
84 | return parseFloat(convertedValue.toFixed(1))
85 | }
86 |
87 | throw new Error(`Unsupported result unit: ${resultUnit}`)
88 | }
89 |
90 | export function getUserUnit(): string {
91 | const { resultUnit } = useAuthStore.getState()
92 |
93 | return resultUnit
94 | }
95 |
96 | export function hash256(input: string): string {
97 | return CryptoJS.SHA256(input).toString(CryptoJS.enc.Hex);
98 | }
99 |
100 | export async function getGlucoseValueForTray(token: string, country: string, accountId: string): Promise {
101 | try {
102 | const data = await getCGMData({
103 | token,
104 | country,
105 | accountId,
106 | });
107 |
108 | if (data?.glucoseMeasurement?.ValueInMgPerDl) {
109 | const value = getUserValue(data.glucoseMeasurement.ValueInMgPerDl);
110 | return Math.round(value);
111 | }
112 |
113 | return 0;
114 | } catch (error) {
115 | console.error('Error getting glucose data for tray:', error);
116 | return 0;
117 | }
118 | }
119 |
120 | export function updateTrayNumber(number: number, targetLow?: number, targetHigh?: number) {
121 | const trayVisible = localStorage.getItem('trayVisible') !== '0';
122 | if (!trayVisible) return;
123 |
124 | const { resultUnit } = useAuthStore.getState();
125 | if (window.electron?.ipcRenderer) {
126 | window.electron.ipcRenderer.sendMessage('update-tray-number', number, resultUnit, targetLow, targetHigh);
127 | }
128 | }
129 |
130 |
131 | export const getTrayVisibility = (): boolean => {
132 | const trayVisible = localStorage.getItem('trayVisible');
133 | return trayVisible !== '0';
134 | };
135 |
136 | export const setTrayVisibility = async (visible: boolean) => {
137 | localStorage.setItem('trayVisible', visible ? '1' : '0');
138 |
139 | if (visible) {
140 | await window.electron.ipcRenderer.sendMessage('create-tray');
141 | } else {
142 | await window.electron.ipcRenderer.sendMessage('destroy-tray');
143 | }
144 | };
145 |
--------------------------------------------------------------------------------
/src/renderer/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"
3 | import * as SelectPrimitive from "@radix-ui/react-select"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Select = SelectPrimitive.Root
8 |
9 | const SelectGroup = SelectPrimitive.Group
10 |
11 | const SelectValue = SelectPrimitive.Value
12 |
13 | const SelectTrigger = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef
16 | >(({ className, children, ...props }, ref) => (
17 |
25 | {children}
26 |
27 |
28 |
29 |
30 | ))
31 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
32 |
33 | const SelectContent = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef
36 | >(({ className, children, position = "popper", ...props }, ref) => (
37 |
38 |
49 |
56 | {children}
57 |
58 |
59 |
60 | ))
61 | SelectContent.displayName = SelectPrimitive.Content.displayName
62 |
63 | const SelectLabel = React.forwardRef<
64 | React.ElementRef,
65 | React.ComponentPropsWithoutRef
66 | >(({ className, ...props }, ref) => (
67 |
72 | ))
73 | SelectLabel.displayName = SelectPrimitive.Label.displayName
74 |
75 | const SelectItem = React.forwardRef<
76 | React.ElementRef,
77 | React.ComponentPropsWithoutRef
78 | >(({ className, children, ...props }, ref) => (
79 |
87 |
88 |
89 |
90 |
91 |
92 | {children}
93 |
94 | ))
95 | SelectItem.displayName = SelectPrimitive.Item.displayName
96 |
97 | const SelectSeparator = React.forwardRef<
98 | React.ElementRef,
99 | React.ComponentPropsWithoutRef
100 | >(({ className, ...props }, ref) => (
101 |
106 | ))
107 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
108 |
109 | export {
110 | Select,
111 | SelectGroup,
112 | SelectValue,
113 | SelectTrigger,
114 | SelectContent,
115 | SelectLabel,
116 | SelectItem,
117 | SelectSeparator,
118 | }
119 |
--------------------------------------------------------------------------------
/.github/workflows/build-and-release.yml:
--------------------------------------------------------------------------------
1 | name: Build And Release
2 | on:
3 | push:
4 | tags:
5 | - 'v*'
6 | - 'stage-v*'
7 | pull_request:
8 | types: [assigned]
9 | release:
10 | types: [edited]
11 |
12 | permissions:
13 | contents: write
14 |
15 | jobs:
16 | build_on_windows:
17 | runs-on: windows-latest
18 | steps:
19 | - name: Checkout repository
20 | uses: actions/checkout@v5
21 |
22 | - name: Cache Node.js dependencies
23 | uses: actions/cache@v4
24 | with:
25 | path: ~/.npm
26 | key: windows-node-${{ hashFiles('package-lock.json') }}
27 | restore-keys: |
28 | windows-node-
29 |
30 | - name: Setup Node.js
31 | uses: actions/setup-node@v6
32 | with:
33 | node-version: '20'
34 |
35 | - name: Install Dependencies
36 | run: npm install --prefer-offline --legacy-peer-deps
37 |
38 | - name: Build
39 | run: npm run package
40 |
41 | - name: List release folder
42 | run: dir release/build/
43 |
44 | - name: Upload Windows Artifacts for PR
45 | if: github.event_name == 'pull_request'
46 | uses: actions/upload-artifact@v5
47 | with:
48 | name: windows-artifacts
49 | path: release/build/
50 |
51 | - name: Upload to Release (Windows)
52 | if: startsWith(github.ref, 'refs/tags/')
53 | uses: softprops/action-gh-release@v2
54 | with:
55 | files: |
56 | release/build/*.msi
57 | release/build/*.appx
58 | release/build/*-setup.exe
59 | tag_name: ${{ github.ref_name }}
60 | env:
61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
62 |
63 | build_on_ubuntu:
64 | runs-on: ubuntu-latest
65 | steps:
66 | - name: Checkout repository
67 | uses: actions/checkout@v5
68 |
69 | - name: Cache Node.js dependencies
70 | uses: actions/cache@v4
71 | with:
72 | path: ~/.npm
73 | key: ubuntu-node-${{ hashFiles('package-lock.json') }}
74 | restore-keys: |
75 | ubuntu-node-
76 |
77 | - name: Setup Node.js
78 | uses: actions/setup-node@v6
79 | with:
80 | node-version: '20'
81 |
82 | - name: Install Dependencies
83 | run: npm install --prefer-offline --legacy-peer-deps
84 |
85 | - name: Build the Package
86 | run: npm run package
87 |
88 | - name: List release folder
89 | run: ls -la release/build/
90 |
91 | - name: Upload Ubuntu Artifacts for PR
92 | if: github.event_name == 'pull_request'
93 | uses: actions/upload-artifact@v5
94 | with:
95 | name: ubuntu-artifacts
96 | path: |
97 | release/build/*.deb
98 | release/build/*.snap
99 | release/build/*.AppImage
100 |
101 | - name: Upload to Release (Ubuntu)
102 | if: startsWith(github.ref, 'refs/tags/')
103 | uses: softprops/action-gh-release@v2
104 | with:
105 | files: |
106 | release/build/LibreLinkUpDesktop-*-amd64.deb
107 | release/build/LibreLinkUpDesktop-*-amd64.snap
108 | release/build/LibreLinkUpDesktop-*-x86_64.AppImage
109 | tag_name: ${{ github.ref_name }}
110 | env:
111 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
112 |
113 | build_on_mac:
114 | runs-on: macOS-latest
115 | steps:
116 | - name: Checkout repository
117 | uses: actions/checkout@v5
118 |
119 | - name: Cache Node.js dependencies
120 | uses: actions/cache@v4
121 | with:
122 | path: ~/.npm
123 | key: mac-node-${{ hashFiles('package-lock.json') }}
124 | restore-keys: |
125 | mac-node-
126 |
127 | - name: Setup Node.js
128 | uses: actions/setup-node@v6
129 | with:
130 | node-version: '20'
131 |
132 | - name: Install Dependencies
133 | run: npm install --prefer-offline --legacy-peer-deps
134 |
135 | - name: Build the Package
136 | run: npm run package
137 |
138 | - name: List release folder
139 | run: ls -la release/build/
140 |
141 | - name: Upload macOS Artifacts for PR
142 | if: github.event_name == 'pull_request'
143 | uses: actions/upload-artifact@v5
144 | with:
145 | name: macos-artifacts
146 | path: |
147 | release/build/*.dmg
148 |
149 | - name: Upload to Release (macOS)
150 | if: startsWith(github.ref, 'refs/tags/')
151 | uses: softprops/action-gh-release@v2
152 | with:
153 | files: |
154 | release/build/LibreLinkUpDesktop-*-arm64.dmg
155 | release/build/LibreLinkUpDesktop-*-x64.dmg
156 | tag_name: ${{ github.ref_name }}
157 | env:
158 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
159 |
--------------------------------------------------------------------------------
/src/renderer/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { Slot } from "@radix-ui/react-slot"
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form"
12 |
13 | import { cn } from "@/lib/utils"
14 | import { Label } from "@/components/ui/label"
15 |
16 | const Form = FormProvider
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath
21 | > = {
22 | name: TName
23 | }
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue
27 | )
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext)
44 | const itemContext = React.useContext(FormItemContext)
45 | const { getFieldState, formState } = useFormContext()
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState)
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ")
51 | }
52 |
53 | const { id } = itemContext
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | }
63 | }
64 |
65 | type FormItemContextValue = {
66 | id: string
67 | }
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue
71 | )
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
82 |
83 | )
84 | })
85 | FormItem.displayName = "FormItem"
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField()
92 |
93 | return (
94 |
100 | )
101 | })
102 | FormLabel.displayName = "FormLabel"
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = "FormControl"
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = "FormDescription"
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = "FormMessage"
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField,
176 | }
177 |
--------------------------------------------------------------------------------
/src/renderer/lib/linkup.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { hash256 } from './utils'
3 |
4 | const BASE_URL = 'https://api-COUNTRY_CODE.libreview.io/llu'
5 |
6 | const getBaseUrl = (countryCode: string): string => {
7 | if(countryCode === 'global') {
8 | return BASE_URL.replace('-COUNTRY_CODE', '')
9 | }
10 | return BASE_URL.replace('COUNTRY_CODE', countryCode)
11 | }
12 |
13 | type LoginAttemptRequest = {
14 | country: string
15 | username: string
16 | password: string
17 | }
18 |
19 | type GetGeneralRequest = {
20 | token: string
21 | country: string
22 | accountId: string
23 | }
24 |
25 | export async function getAuthToken(request: LoginAttemptRequest): Promise<{
26 | token: string, accountId: string, accountCountry: string,
27 | } | { error: number } | null> {
28 | try {
29 | let baseUrl = getBaseUrl(request.country);
30 |
31 | let response = await axios({
32 | method: 'post',
33 | baseURL: baseUrl,
34 | url: '/auth/login',
35 | data: {
36 | email: request.username,
37 | password: request.password,
38 | },
39 | headers: {
40 | product: 'llu.android',
41 | version: '4.16.0',
42 | Pragma: 'no-cache',
43 | 'Cache-Control': 'no-cache',
44 | // 'Accept-Encoding': 'gzip',
45 | // Connection: 'keep-alive',
46 | },
47 | });
48 |
49 | if (response.data?.status === 0 ) {
50 | // Handle different response structures
51 | let countryCode;
52 | if (response.data.data.user?.country) {
53 | // Original structure: {data: {user: {country: "fr"}}}
54 | countryCode = response.data.data.user.country;
55 | // if countryCode is ch, set it to eu
56 | if (countryCode.toLowerCase() === 'ch') {
57 | countryCode = 'eu';
58 | }
59 | } else if (response.data.data.region) {
60 | // New structure: {data: {region: "fr"}}
61 | countryCode = response.data.data.region;
62 | } else {
63 | // Fallback to original request country
64 | countryCode = request.country;
65 | }
66 |
67 | baseUrl = getBaseUrl(countryCode);
68 |
69 | response = await axios({
70 | method: 'post',
71 | baseURL: baseUrl,
72 | url: '/auth/login',
73 | data: {
74 | email: request.username,
75 | password: request.password,
76 | },
77 | headers: {
78 | product: 'llu.android',
79 | version: '4.16.0',
80 | Pragma: 'no-cache',
81 | 'Cache-Control': 'no-cache',
82 | // 'Accept-Encoding': 'gzip',
83 | // Connection: 'keep-alive',
84 | },
85 | });
86 | }
87 | else{
88 | return {
89 | error: response.data?.status || 999999
90 | };
91 | }
92 |
93 | let finalCountryCode = response.data?.data?.user?.country?.toLowerCase();
94 | finalCountryCode = finalCountryCode === 'ch' ? 'eu' : finalCountryCode;
95 |
96 | return {
97 | token: response.data?.data?.authTicket?.token,
98 | accountId: response.data?.data?.user?.id,
99 | accountCountry: finalCountryCode,
100 | };
101 | } catch (error) {
102 | console.log("Unable to get the token: ", error);
103 | throw error;
104 | }
105 |
106 | return null;
107 | }
108 |
109 | export async function getCGMData(request: GetGeneralRequest): Promise {
110 | try {
111 | const baseURL = getBaseUrl(request.country)
112 | const headers = {
113 | product: 'llu.android',
114 | version: '4.16.0',
115 | Pragma: 'no-cache',
116 | 'Cache-Control': 'no-cache',
117 | Authorization: `Bearer ${request.token}`,
118 | 'Account-Id': hash256(request.accountId),
119 | }
120 |
121 | const connResponse = await axios({
122 | method: 'get',
123 | baseURL,
124 | headers,
125 | url: '/connections',
126 | })
127 |
128 | const patientId = connResponse.data?.data[0]?.patientId
129 |
130 | if (!patientId) {
131 | if (connResponse.data?.data?.length === 0) {
132 | return { error: 'NO_CONNECTIONS', message: 'No LibreLinkUp connections found. Please set up a connection in your Libre app.' }
133 | }
134 | return null
135 | }
136 |
137 | const graphResponse = await axios({
138 | method: 'get',
139 | baseURL,
140 | headers,
141 | url: `/connections/${patientId}/graph`,
142 | })
143 |
144 | return graphResponse?.data?.data?.connection
145 | } catch (error: any) {
146 | console.log('Unable to getCGMData: ', error)
147 | }
148 |
149 | return null
150 | }
151 |
152 | export async function getConnection(request: GetGeneralRequest): Promise {
153 | try {
154 | const baseURL = getBaseUrl(request.country)
155 | const headers = {
156 | product: 'llu.android',
157 | version: '4.16.0',
158 | Pragma: 'no-cache',
159 | 'Cache-Control': 'no-cache',
160 | Authorization: `Bearer ${request.token}`,
161 | 'Account-Id': hash256(request.accountId),
162 | }
163 |
164 | const response = await axios({
165 | method: 'get',
166 | baseURL,
167 | headers,
168 | url: '/connections',
169 | })
170 |
171 | return response?.data?.data[0]
172 | } catch (error: any) {
173 | console.log('Unable to getConnection: ', error)
174 | }
175 |
176 | return null
177 | }
178 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | 
3 | [](https://www.figma.com/file/GpKMB4Qnp2FWi4A9gM19Gf/Untitled?type=design&node-id=0%3A1&mode=design&t=0qRXeUdC0lafjaKt-1)
4 | [](https://github.com/Crazy-Marvin/LibreLinkUpDesktop/commits)
5 | [](https://github.com/Crazy-Marvin/LibreLinkUpDesktop/releases)
6 | [](https://github.com/Crazy-Marvin/LibreLinkUpDesktop/tags)
7 | [](https://github.com/Crazy-Marvin/LibreLinkUpDesktop/issues)
8 | [](https://github.com/Crazy-Marvin/LibreLinkUpDesktop/pulls)
9 | [](https://app.codacy.com/gh/Crazy-Marvin/LibreLinkUpDesktop/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
10 | [](https://hosted.weblate.org/engage/librelinkupdesktop/)
11 | [](https://snyk.io/test/github/Crazy-Marvin/LibreLinkUpDesktop?targetFile=package.json)
12 | [](https://www.electronjs.org/)
13 | [](https://www.microsoft.com/store/apps/9N5RKKLQM5C9)
14 | [](https://snapcraft.io/librelinkupdesktop)
15 | [](https://flathub.org/apps/rocks.poopjournal.librelinkupdesktop)
16 |
17 | # LibreLinkUpDesktop
18 | This is a desktop application that fetches your blood sugar from [LibreLinkUp](https://librelinkup.com/).
19 |
20 | It works like the official [LibreLinkUp smartphone app](https://play.google.com/store/apps/details?id=org.nativescript.LibreLinkUp) but on desktop clients like [Ubuntu](https://www.ubuntu.com/desktop/) or [Windows](https://www.windows.com/).
21 |
22 | Under the hood, [Electron](https://www.electronjs.org/) is used with [TypeScript](https://www.typescriptlang.org/) and [Vue.js](https://vuejs.org/).
23 | As there is no offical API from [Abbott](https://www.freestyle.abbott/) yet, we use [this awesome client](https://github.com/DiaKEM/libre-link-up-api-client) from DiaKEM.
24 |
25 | #### Features of LibreLinkUpDesktop:
26 | - Show blood glucose level on your desktop in a little window
27 | - No tracking
28 | - Dark Mode
29 | - Libre software
30 | - That's it. 🩸
31 |
32 | ### Instructions:
33 | - As sharing person, open your [Libre smartphone app](https://play.google.com/store/apps/details?id=com.freestylelibre3.app.de), go to _Connected Apps_, click on _Manage_ next to LibreLinkUp, click on _Add connection_ and input the details for the account you wish to use with LibreLinkUpDesktop.
34 | - Save those credentials inside your password manager. You may use them for yourself or you may share them with someone.
35 | - Start LibreLinkUpDesktop and enter those credentials.
36 | - That's it. ☺️
37 |
38 | Let me know about any issues by opening a new [issue on GitHub](https://github.com/Crazy-Marvin/LibreLinkUpDesktop/issues) or commenting on an existing issue if there is one already.
39 |
40 | You may download the application from [GitHub Releases](https://github.com/Crazy-Marvin/LibreLinkUpDesktop/releases), [Snapcraft](https://snapcraft.io/librelinkupdesktop/), [Flathub](https://flathub.org/apps/rocks.poopjournal.librelinkupdesktop) or [Microsoft Store](https://www.microsoft.com/store/apps/9N5RKKLQM5C9).
41 |
42 | # Screenshots
43 |
44 | Login | Main Screen | Settings
45 | ------------ | ------------- | -------------
46 |  |  | 
47 |
48 | # Contributing
49 |
50 | The ```development``` or a feature branch is used while developing the code, and pushed into the master branch ```trunk``` afterwards for releases.
51 | PRs to the ```trunk``` need at least one approving review before getting merged.
52 |
53 | Help translate the app at [Hosted Weblate](https://hosted.weblate.org/engage/librelinkupdesktop/).
54 |
55 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
56 |
57 | Please make sure to update tests as appropriate.
58 |
59 | Check out the [contribution guidelines](https://github.com/Crazy-Marvin/LibreLinkUpDesktop/blob/trunk/.github/CONTRIBUTING.md) for details please.
60 |
61 | # License
62 |
63 | [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0)
64 |
--------------------------------------------------------------------------------
/.erb/configs/webpack.config.renderer.dev.ts:
--------------------------------------------------------------------------------
1 | import 'webpack-dev-server';
2 | import path from 'path';
3 | import fs from 'fs';
4 | import webpack from 'webpack';
5 | import HtmlWebpackPlugin from 'html-webpack-plugin';
6 | import chalk from 'chalk';
7 | import { merge } from 'webpack-merge';
8 | import { execSync, spawn } from 'child_process';
9 | import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
10 | import baseConfig from './webpack.config.base';
11 | import webpackPaths from './webpack.paths';
12 | import checkNodeEnv from '../scripts/check-node-env';
13 |
14 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's
15 | // at the dev webpack config is not accidentally run in a production environment
16 | if (process.env.NODE_ENV === 'production') {
17 | checkNodeEnv('development');
18 | }
19 |
20 | const port = process.env.PORT || 1212;
21 | const manifest = path.resolve(webpackPaths.dllPath, 'renderer.json');
22 | const skipDLLs =
23 | module.parent?.filename.includes('webpack.config.renderer.dev.dll') ||
24 | module.parent?.filename.includes('webpack.config.eslint');
25 |
26 | /**
27 | * Warn if the DLL is not built
28 | */
29 | if (
30 | !skipDLLs &&
31 | !(fs.existsSync(webpackPaths.dllPath) && fs.existsSync(manifest))
32 | ) {
33 | console.log(
34 | chalk.black.bgYellow.bold(
35 | 'The DLL files are missing. Sit back while we build them for you with "npm run build-dll"'
36 | )
37 | );
38 | execSync('npm run postinstall');
39 | }
40 |
41 | const configuration: webpack.Configuration = {
42 | devtool: 'inline-source-map',
43 |
44 | mode: 'development',
45 |
46 | target: ['web', 'electron-renderer'],
47 |
48 | entry: [
49 | `webpack-dev-server/client?http://localhost:${port}/dist`,
50 | 'webpack/hot/only-dev-server',
51 | path.join(webpackPaths.srcRendererPath, 'index.tsx'),
52 | ],
53 |
54 | output: {
55 | path: webpackPaths.distRendererPath,
56 | publicPath: '/',
57 | filename: 'renderer.dev.js',
58 | library: {
59 | type: 'umd',
60 | },
61 | },
62 |
63 | module: {
64 | rules: [
65 | // Tailwind CSS
66 | {
67 | test: /\.css$/,
68 | include: [webpackPaths.srcRendererPath],
69 | use: ['style-loader', 'css-loader', 'postcss-loader'],
70 | },
71 | // Fonts
72 | {
73 | test: /\.(woff|woff2|eot|ttf|otf)$/i,
74 | type: 'asset/resource',
75 | },
76 | // Images
77 | {
78 | test: /\.(png|jpg|jpeg|gif)$/i,
79 | type: 'asset/resource',
80 | },
81 | // SVG
82 | {
83 | test: /\.svg$/,
84 | use: [
85 | {
86 | loader: '@svgr/webpack',
87 | options: {
88 | prettier: false,
89 | svgo: false,
90 | svgoConfig: {
91 | plugins: [{ removeViewBox: false }],
92 | },
93 | titleProp: true,
94 | ref: true,
95 | },
96 | },
97 | 'file-loader',
98 | ],
99 | },
100 | ],
101 | },
102 | plugins: [
103 | ...(skipDLLs
104 | ? []
105 | : [
106 | new webpack.DllReferencePlugin({
107 | context: webpackPaths.dllPath,
108 | manifest: require(manifest),
109 | sourceType: 'var',
110 | }),
111 | ]),
112 |
113 | new webpack.NoEmitOnErrorsPlugin(),
114 |
115 | /**
116 | * Create global constants which can be configured at compile time.
117 | *
118 | * Useful for allowing different behaviour between development builds and
119 | * release builds
120 | *
121 | * NODE_ENV should be production so that modules do not perform certain
122 | * development checks
123 | *
124 | * By default, use 'development' as NODE_ENV. This can be overriden with
125 | * 'staging', for example, by changing the ENV variables in the npm scripts
126 | */
127 | new webpack.EnvironmentPlugin({
128 | NODE_ENV: 'development',
129 | }),
130 |
131 | new webpack.LoaderOptionsPlugin({
132 | debug: true,
133 | }),
134 |
135 | new ReactRefreshWebpackPlugin(),
136 |
137 | new HtmlWebpackPlugin({
138 | filename: path.join('index.html'),
139 | template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
140 | minify: {
141 | collapseWhitespace: true,
142 | removeAttributeQuotes: true,
143 | removeComments: true,
144 | },
145 | isBrowser: false,
146 | env: process.env.NODE_ENV,
147 | isDevelopment: process.env.NODE_ENV !== 'production',
148 | nodeModules: webpackPaths.appNodeModulesPath,
149 | }),
150 | ],
151 |
152 | node: {
153 | __dirname: false,
154 | __filename: false,
155 | },
156 |
157 | devServer: {
158 | port,
159 | compress: true,
160 | hot: true,
161 | headers: { 'Access-Control-Allow-Origin': '*' },
162 | static: {
163 | publicPath: '/',
164 | },
165 | historyApiFallback: {
166 | verbose: true,
167 | },
168 | setupMiddlewares(middlewares) {
169 | console.log('Starting preload.js builder...');
170 | const preloadProcess = spawn('npm', ['run', 'start:preload'], {
171 | shell: true,
172 | stdio: 'inherit',
173 | })
174 | .on('close', (code: number) => process.exit(code!))
175 | .on('error', (spawnError) => console.error(spawnError));
176 |
177 | console.log('Starting Main Process...');
178 | let args = ['run', 'start:main'];
179 | if (process.env.MAIN_ARGS) {
180 | args = args.concat(
181 | ['--', ...process.env.MAIN_ARGS.matchAll(/"[^"]+"|[^\s"]+/g)].flat()
182 | );
183 | }
184 | spawn('npm', args, {
185 | shell: true,
186 | stdio: 'inherit',
187 | })
188 | .on('close', (code: number) => {
189 | preloadProcess.kill();
190 | process.exit(code!);
191 | })
192 | .on('error', (spawnError) => console.error(spawnError));
193 | return middlewares;
194 | },
195 | },
196 | };
197 |
198 | export default merge(baseConfig, configuration);
199 |
--------------------------------------------------------------------------------
/.github/workflows/build-pipeline.yml:
--------------------------------------------------------------------------------
1 | name: Build by Command
2 | on:
3 | pull_request:
4 | types: [assigned, labeled]
5 | issue_comment:
6 | types: [created]
7 |
8 | permissions:
9 | contents: read
10 | pull-requests: write
11 |
12 | jobs:
13 | build_on_windows:
14 | runs-on: windows-latest
15 | if: |
16 | (github.event_name == 'pull_request' && github.event.action == 'assigned') ||
17 | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/build windows'))
18 | steps:
19 | - name: Checkout repository
20 | uses: actions/checkout@v5
21 |
22 | - name: Cache Node.js dependencies
23 | uses: actions/cache@v4
24 | with:
25 | path: ~/.npm
26 | key: windows-node-${{ hashFiles('package-lock.json') }}
27 | restore-keys: |
28 | windows-node-
29 |
30 | - name: Setup Node.js
31 | uses: actions/setup-node@v6
32 | with:
33 | node-version: '20'
34 |
35 | - name: Install Dependencies
36 | run: npm install --prefer-offline --legacy-peer-deps
37 |
38 | - name: Build
39 | run: npm run package
40 |
41 | - name: List release folder
42 | run: dir release/build/
43 |
44 | - name: Upload Windows Artifacts
45 | uses: actions/upload-artifact@v5
46 | with:
47 | name: windows-artifacts-${{ github.run_id }}
48 | path: release/build/
49 |
50 | - name: Add comment with Windows artifacts
51 | if: github.event_name == 'issue_comment'
52 | uses: actions/github-script@v7
53 | with:
54 | script: |
55 | try {
56 | const comment = `## Download build: https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
57 |
58 | await github.rest.issues.createComment({
59 | owner: context.repo.owner,
60 | repo: context.repo.repo,
61 | issue_number: context.issue.number,
62 | body: comment
63 | });
64 | } catch (error) {
65 | console.log('Error creating comment:', error);
66 | }
67 |
68 | build_on_ubuntu:
69 | runs-on: ubuntu-latest
70 | if: |
71 | (github.event_name == 'pull_request' && github.event.action == 'assigned') ||
72 | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/build linux'))
73 | steps:
74 | - name: Checkout repository
75 | uses: actions/checkout@v5
76 |
77 | - name: Cache Node.js dependencies
78 | uses: actions/cache@v4
79 | with:
80 | path: ~/.npm
81 | key: ubuntu-node-${{ hashFiles('package-lock.json') }}
82 | restore-keys: |
83 | ubuntu-node-
84 |
85 | - name: Setup Node.js
86 | uses: actions/setup-node@v6
87 | with:
88 | node-version: '20'
89 |
90 | - name: Install Dependencies
91 | run: npm install --prefer-offline --legacy-peer-deps
92 |
93 | - name: Build the Package
94 | run: npm run package
95 |
96 | - name: List release folder
97 | run: ls -la release/build/
98 |
99 | - name: Upload Ubuntu Artifacts
100 | uses: actions/upload-artifact@v5
101 | with:
102 | name: ubuntu-artifacts-${{ github.run_id }}
103 | path: |
104 | release/build/*.deb
105 | release/build/*.snap
106 | release/build/*.AppImage
107 |
108 | - name: Add comment with Linux artifacts
109 | if: github.event_name == 'issue_comment'
110 | uses: actions/github-script@v7
111 | with:
112 | script: |
113 | try {
114 | const comment = `## Download build: https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
115 |
116 | await github.rest.issues.createComment({
117 | owner: context.repo.owner,
118 | repo: context.repo.repo,
119 | issue_number: context.issue.number,
120 | body: comment
121 | });
122 | } catch (error) {
123 | console.log('Error creating comment:', error);
124 | }
125 |
126 | build_on_mac:
127 | runs-on: macOS-latest
128 | if: |
129 | (github.event_name == 'pull_request' && github.event.action == 'assigned') ||
130 | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/build mac'))
131 | steps:
132 | - name: Checkout repository
133 | uses: actions/checkout@v5
134 |
135 | - name: Cache Node.js dependencies
136 | uses: actions/cache@v4
137 | with:
138 | path: ~/.npm
139 | key: mac-node-${{ hashFiles('package-lock.json') }}
140 | restore-keys: |
141 | mac-node-
142 |
143 | - name: Setup Node.js
144 | uses: actions/setup-node@v6
145 | with:
146 | node-version: '20'
147 |
148 | - name: Install Dependencies
149 | run: npm install --prefer-offline --legacy-peer-deps
150 |
151 | - name: Build the Package
152 | run: npm run package
153 |
154 | - name: List release folder
155 | run: ls -la release/build/
156 |
157 | - name: Upload macOS Artifacts
158 | uses: actions/upload-artifact@v5
159 | with:
160 | name: macos-artifacts-${{ github.run_id }}
161 | path: |
162 | release/build/*.dmg
163 |
164 | - name: Add comment with macOS artifacts
165 | if: github.event_name == 'issue_comment'
166 | uses: actions/github-script@v7
167 | with:
168 | script: |
169 | try {
170 | const comment = `## Download build: https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
171 |
172 | await github.rest.issues.createComment({
173 | owner: context.repo.owner,
174 | repo: context.repo.repo,
175 | issue_number: context.issue.number,
176 | body: comment
177 | });
178 | } catch (error) {
179 | console.log('Error creating comment:', error);
180 | }
181 |
--------------------------------------------------------------------------------
/src/renderer/pages/settings/alert.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { ToggleSwitch } from '@/components/ui/toggle-switch';
3 | import SettingsLayout from '@/layouts/settings-layout';
4 | import { useAlertStore } from '@/stores/alertStore';
5 | import { useTranslation } from 'react-i18next';
6 | import {
7 | uploadCustomAlertSoundFile,
8 | sendRefreshPrimaryWindow,
9 | sendRefreshAllWindows,
10 | } from '@/lib/utils';
11 | import { Input } from '@/components/ui/input';
12 | import { Button } from '@/components/ui/button';
13 |
14 | export default function SettingsAlertPage() {
15 | const { t } = useTranslation();
16 |
17 | const {
18 | bringToFrontEnabled,
19 | flashWindowEnabled,
20 | audioAlertEnabled,
21 | useCustomSound,
22 | overrideThreshold,
23 | customTargetHigh,
24 | customTargetLow,
25 |
26 | setBringToFrontEnabled,
27 | setFlashWindowEnabled,
28 | setAudioAlertEnabled,
29 | setUserCustomSoundEnabled,
30 | setOverrideThreshold,
31 | setCustomTargetLow,
32 | setCustomTargetHigh,
33 | } = useAlertStore();
34 |
35 | const handleBringToFrontChange = (checked: boolean) => {
36 | setBringToFrontEnabled(checked);
37 | };
38 |
39 | const handleFlashWindowChange = (checked: boolean) => {
40 | setFlashWindowEnabled(checked);
41 | };
42 |
43 | const handleAudioAlertChange = (checked: boolean) => {
44 | setAudioAlertEnabled(checked);
45 | };
46 |
47 | const handleUserCustomSoundChange = (checked: boolean) => {
48 | setUserCustomSoundEnabled(checked);
49 | };
50 |
51 | const handleOverrideChange = (checked: boolean) => {
52 | setOverrideThreshold(checked);
53 | };
54 |
55 | const handleTargetLowChanged = (
56 | event: React.ChangeEvent,
57 | ) => {
58 | setCustomTargetLow(Number(event.target.value));
59 | };
60 |
61 | const handleTargetHighChanged = (
62 | event: React.ChangeEvent,
63 | ) => {
64 | setCustomTargetHigh(Number(event.target.value));
65 | };
66 |
67 | const handleApplyChanges = () => {
68 | sendRefreshPrimaryWindow();
69 | // sendRefreshAllWindows();
70 | };
71 |
72 | // TODO:: use a button and trigger this function
73 | const uploadCustomAlertSound = async () => {
74 | const fileInput = document.createElement('input');
75 | fileInput.type = 'file';
76 | fileInput.accept = '.mp3';
77 | fileInput.onchange = async (event) => {
78 | const target = event.target as HTMLInputElement;
79 | const file = target?.files ? target.files[0] : null;
80 | if (file) {
81 | const reader = new FileReader();
82 | reader.onload = async () => {
83 | if (reader.result) {
84 | const arrayBuffer = reader.result as ArrayBuffer;
85 | const uint8Array = new Uint8Array(arrayBuffer);
86 | await uploadCustomAlertSoundFile(Array.from(uint8Array));
87 | }
88 | };
89 |
90 | reader.onerror = () => {
91 | console.error('uploadCustomAlertSound:', reader.error);
92 | };
93 |
94 | reader.readAsArrayBuffer(file);
95 | }
96 | };
97 | fileInput.click();
98 | };
99 |
100 | return (
101 |
102 |
103 |
{t('ALERT_DESCRIPTION')}
104 |
105 |
106 |
{t('Bring Window to Front')}
107 |
111 |
112 |
113 |
114 |
{t('Flash Window')}
115 |
119 |
120 |
121 |
122 |
{t('Play Sound')}
123 |
127 |
128 |
129 | {audioAlertEnabled && (
130 |
131 |
{t('Use Custom Sound')}
132 |
136 |
137 | )}
138 |
139 | {audioAlertEnabled && useCustomSound && (
140 |
141 |
145 |
146 | )}
147 |
148 |
149 |
{t('Custom Alert Level')}
150 |
154 |
155 |
156 | {overrideThreshold && (
157 |
158 |
159 |
160 | {t('Min Value')} ({t('mg/dL')})
161 |
162 |
168 |
169 |
170 |
171 | {t('Max Value')} ({t('mg/dL')})
172 |
173 |
179 |
180 |
181 | )}
182 |
183 |
184 |
185 |
186 |
187 |
188 | );
189 | }
190 |
--------------------------------------------------------------------------------
/src/renderer/pages/settings/general.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeType, useTheme } from "@/components/theme-provider"
2 | import { Button } from "@/components/ui/button"
3 | import { ToggleSwitch } from "@/components/ui/toggle-switch"
4 | import SettingsLayout from "@/layouts/settings-layout"
5 | import { cn } from "@/lib/utils"
6 | import { useNavigate } from "react-router-dom"
7 | import {
8 | Select,
9 | SelectContent,
10 | SelectItem,
11 | SelectTrigger,
12 | SelectValue,
13 | } from "@/components/ui/select"
14 | import { useAuthStore } from "@/stores/auth"
15 | import { countries, languages, themes, resultUnits, windowModes } from "@/config/app"
16 | import { useTranslation } from "react-i18next"
17 | import { setRedirectTo, sendRefreshPrimaryWindow, setWindowMode, getLocalStorageWindowMode, getTrayVisibility, setTrayVisibility} from "@/lib/utils"
18 | import { useEffect, useState, useCallback } from 'react';
19 |
20 | export default function SettingsGeneralPage() {
21 | const navigate = useNavigate()
22 | const { i18n, t } = useTranslation()
23 | const theme = localStorage.getItem('vite-ui-theme')
24 | const language = useAuthStore((state) => state.language)
25 | const setLanguage = useAuthStore((state) => state.setLanguage)
26 | const country = useAuthStore((state) => state.country)
27 | const setCountry = useAuthStore((state) => state.setCountry)
28 |
29 | const resultUnit = useAuthStore((state) => state.resultUnit)
30 | const setResultUnit = useAuthStore((state) => state.setResultUnit)
31 |
32 | const { setTheme } = useTheme()
33 | const setAndRefreshTheme = (t: ThemeType) => {
34 | setTheme(t)
35 | setRedirectTo('/settings/general')
36 | window.location.reload();
37 | }
38 | const setAndRefreshLanguage = (l: string) => {
39 | i18n.changeLanguage(l)
40 | setLanguage(l)
41 | }
42 |
43 | const handleSetResultUnit = (value: string) => {
44 | setResultUnit(value);
45 | sendRefreshPrimaryWindow();
46 | }
47 |
48 | const handleSetWindowMode = (value: string) => {
49 | setWindowMode(value);
50 | }
51 |
52 | const [currentWindowMode, setCurrentWindowMode] = useState(null);
53 | useEffect(() => {
54 | const fetchWindowMode = async () => {
55 | const mode = await getLocalStorageWindowMode();
56 | setCurrentWindowMode(mode);
57 | };
58 | fetchWindowMode();
59 | }, []);
60 |
61 | const [trayVisible, setTrayVisible] = useState(true);
62 |
63 | useEffect(() => {
64 | const initializeTrayVisibility = () => {
65 | const visibility = getTrayVisibility();
66 | setTrayVisible(visibility);
67 | };
68 |
69 | initializeTrayVisibility();
70 | }, []);
71 |
72 | const handleToggleTray = useCallback(async (checked: boolean) => {
73 | setTrayVisible(checked);
74 | await setTrayVisibility(checked);
75 | }, []);
76 |
77 | const toggleSwitchKey = `tray-toggle-${trayVisible}`;
78 |
79 | return (
80 |
81 |
82 |
83 |
{t('Theme')}
84 |
96 |
97 |
98 |
99 |
{t('Country')}
100 |
112 |
113 |
114 |
115 |
{t('Language')}
116 |
128 |
129 |
130 |
131 |
{t('WindowMode')}
132 |
144 |
145 |
146 |
147 |
{t('Unit')}
148 |
160 |
161 |
162 |
163 |
{t('Show glucose values in tray')}
164 |
169 |
170 |
171 |
172 | )
173 | }
174 |
--------------------------------------------------------------------------------