├── 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 |
4 |

Loading...

5 |
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 app විවෘත කරන්න, <3>Connected Apps වෙත යන්න, <5>Manage මත ක්ලික් කරන්න, <7>Add connection එක මත ක්ලික් කරන්න සහ ඔබ 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í aplikaci, přejde na <3>Propojené aplikace, klikne na <5>Spravovat u volby LibreLinkUp, vybere <7>Přidat připojení 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 app, gå till <3>Connected Apps, klicka på <5>Manage på raden för LibreLinkUp, klicka på <7>Add connection 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 LibreLink en tu dispositivo móvil, en el menú de navegación ve a <3>Aplicaciones Conectadas, haz clic sobre <5>Gestionar junto a LibreLinkUp, haz clic sobre <7>Añadir conexión 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 smartphone, μεταβείτε στις <3>Συνδεδεμένες εφαρμογές, κάντε κλικ στην επιλογή <5>Διαχείριση δίπλα στο LibreLinkUp, κάντε κλικ στην επιλογή <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 app, go to <3>Connected Apps, click on <5>Manage next to LibreLinkUp, click on <7>Add connection 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 Libre, vai su <3>App connesse, clicca su <5>Gestisci accanto a LibreLinkUp, clicca su <7>Aggiungi connessione 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-App, gehen Sie zu <3>Verbundene Apps, klicken Sie auf <5>Verwalten neben LibreLinkUp, klicken Sie auf <7>Verbindung hinzufügen 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 для смартфона, перейдите в раздел <3>Подключенные приложения, нажмите на <5>Управление рядом с LibreLinkUp, нажмите на <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 LibreLink, allez à <3>Applications connectées, cliquez sur <5>Gérer à côté de LibreLinkUp, cliquez sur <7>Ajouter une connexion 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> லிப்ரே ச்மார்ட்போன் பயன்பாடு ஐத் திறக்கவும், <3> இணைக்கப்பட்ட பயன்பாடுகளுக்குச் செல்லவும் , <5> நிர்வகி ஐக் சொடுக்கு செய்க, லிப்ரெலிங்கப்பிற்கு அடுத்து, <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 |