diff --git a/apps/desktop/electron.vite.config.ts b/apps/desktop/electron.vite.config.ts index 360a55bd9..45fe9d399 100644 --- a/apps/desktop/electron.vite.config.ts +++ b/apps/desktop/electron.vite.config.ts @@ -19,6 +19,7 @@ export default defineConfig({ renderer: { resolve: { alias: { + '@database': resolve('src/database'), '@': resolve('src/renderer/src'), }, }, diff --git a/apps/desktop/src/database/db.main.ts b/apps/desktop/src/database/db.main.ts index 7c6ab5968..0426f812a 100644 --- a/apps/desktop/src/database/db.main.ts +++ b/apps/desktop/src/database/db.main.ts @@ -4,17 +4,22 @@ import Database from 'better-sqlite3' import { drizzle } from 'drizzle-orm/better-sqlite3' import { migrate } from 'drizzle-orm/better-sqlite3/migrator' import { app } from 'electron' -import { posts } from './schemas/posts' +import { settings } from './schemas/settings' const dbPath = import.meta.env.DEV ? 'sqlite.db' : path.join(app.getPath('userData'), 'data.db') fs.mkdirSync(path.dirname(dbPath), { recursive: true }) +// TODO: Remove this code before production. This is to ensure the database is recreated on each startup to avoid migration issues during development. +if (fs.existsSync(dbPath)) { + fs.unlinkSync(dbPath) +} + const sqlite = new Database( dbPath, ) -export const db = drizzle(sqlite, { schema: { posts } }) +export const db = drizzle(sqlite, { schema: { settings } }) function toDrizzleResult(row: Record) function toDrizzleResult(rows: Record | Array>) { diff --git a/apps/desktop/src/database/db.renderer.ts b/apps/desktop/src/database/db.renderer.ts index 08ff5201c..0d2370c55 100644 --- a/apps/desktop/src/database/db.renderer.ts +++ b/apps/desktop/src/database/db.renderer.ts @@ -1,7 +1,7 @@ import { drizzle } from 'drizzle-orm/sqlite-proxy' -import { posts } from './schemas/posts' +import { settings } from './schemas/settings' -export const database = drizzle(async (...args) => { +export const db = drizzle(async (...args) => { try { const result = await window.api.execute(...args) return { rows: result } @@ -10,5 +10,5 @@ export const database = drizzle(async (...args) => { return { rows: [] } } }, { - schema: { posts }, + schema: { settings }, }) diff --git a/apps/desktop/src/database/schemas/posts.ts b/apps/desktop/src/database/schemas/posts.ts deleted file mode 100644 index 728e48a4c..000000000 --- a/apps/desktop/src/database/schemas/posts.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core' - -export const posts = sqliteTable('posts', { - id: int('id').primaryKey({ autoIncrement: true }), - title: text('title').notNull().default(''), -}) - -export type SelectPosts = typeof posts.$inferSelect -export type InsertPosts = typeof posts.$inferInsert diff --git a/apps/desktop/src/database/schemas/settings.ts b/apps/desktop/src/database/schemas/settings.ts new file mode 100644 index 000000000..16049b90c --- /dev/null +++ b/apps/desktop/src/database/schemas/settings.ts @@ -0,0 +1,25 @@ +import type { Settings } from 'mqttx' +import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' + +export const settings = sqliteTable('settings', { + id: integer().primaryKey({ autoIncrement: true }), + width: integer({ mode: 'number' }).notNull().default(1024), + height: integer({ mode: 'number' }).notNull().default(749), + currentLang: text().$type().notNull().default('en'), + autoCheck: integer({ mode: 'boolean' }).notNull().default(true), + autoResub: integer({ mode: 'boolean' }).notNull().default(true), + multiTopics: integer({ mode: 'boolean' }).notNull().default(true), + maxReconnectTimes: integer().notNull().default(10), + syncOsTheme: integer({ mode: 'boolean' }).notNull().default(false), + currentTheme: text().$type().notNull().default('light'), + jsonHighlight: integer({ mode: 'boolean' }).notNull().default(true), + logLevel: text().$type().notNull().default('info'), + ignoreQoS0Message: integer({ mode: 'boolean' }).notNull().default(false), + enableCopilot: integer({ mode: 'boolean' }).notNull().default(true), + openAIAPIHost: text().notNull().default('https://api.openai.com/v1'), + openAIAPIKey: text().notNull().default(''), + model: text().notNull().default('gpt-4o'), +}) + +export type SelectSettings = typeof settings.$inferSelect +export type InsertSettings = typeof settings.$inferInsert diff --git a/apps/desktop/src/database/services/SettingsService.ts b/apps/desktop/src/database/services/SettingsService.ts new file mode 100644 index 000000000..45d7c3d4e --- /dev/null +++ b/apps/desktop/src/database/services/SettingsService.ts @@ -0,0 +1,50 @@ +import type { Settings } from 'mqttx' +import type { SelectSettings } from '../schemas/settings' +import { useSettingsStore } from '@mqttx/ui/stores' +import { eq } from 'drizzle-orm' +import { db } from '../db.renderer' +import { settings } from '../schemas/settings' + +const isWathching = ref(false) + +export default function useSettingsService() { + const { settings: storeSettings, updateSettings } = useSettingsStore() + + async function getSettingsInDB(): Promise { + let data: SelectSettings | undefined + data = await db.query.settings.findFirst() + if (!data) { + data = await updateSettingsInDB() + } + updateSettings(data) + return data + } + async function updateSettingsInDB(data?: Partial): Promise { + const existingSettings = await db.query.settings.findFirst() + if (existingSettings) { + await db.update(settings) + .set(data ?? {}) + .where(eq(settings.id, existingSettings.id)) + } else { + await db.insert(settings) + .values(data ?? {}) + } + return await getSettingsInDB() + } + + if (storeSettings) { + if (!isWathching.value) { + watch(storeSettings, (newSettings) => { + updateSettingsInDB(newSettings) + }) + } + isWathching.value = true + } + + return { + settings: storeSettings!, + updateSettings, + getSettingsInDB, + updateSettingsInDB, + } +} diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 2a0d043fb..a07ab2db7 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -1,18 +1,40 @@ import { join } from 'node:path' import { electronApp, is, optimizer } from '@electron-toolkit/utils' +import { eq } from 'drizzle-orm' import { app, BrowserWindow, ipcMain, shell } from 'electron' import icon from '../../resources/icon.png?asset' -import { execute, runMigrate } from '../database/db.main' +import { db, execute, runMigrate } from '../database/db.main' +import { type SelectSettings, settings } from '../database/schemas/settings' // const IsMacOS = process.platform === 'darwin' -function createWindow(): void { +async function createWindow() { + let data: SelectSettings | undefined + data = await db.query.settings.findFirst() + if (!data) { + await db.insert(settings).values({}) + } + data = await db.query.settings.findFirst() as SelectSettings + + const width = data.width || 1024 + const height = data.height || 749 + const currentTheme = data.currentTheme || 'light' + const bgColor = { + dark: '#232323', + night: '#212328', + light: '#ffffff', + } + const backgroundColor = bgColor[currentTheme] + // Create the browser window. const mainWindow = new BrowserWindow({ - width: 1024, - height: 749, + width, + height, + minHeight: 650, + minWidth: 997, show: false, autoHideMenuBar: true, + backgroundColor, ...(process.platform === 'linux' ? { icon } : {}), webPreferences: { preload: join(__dirname, '../preload/index.mjs'), @@ -24,6 +46,17 @@ function createWindow(): void { mainWindow.on('ready-to-show', () => { mainWindow.show() + if (is.dev) { + mainWindow.webContents.openDevTools() + } + }) + + mainWindow.on('resize', async () => { + const { width, height } = mainWindow.getBounds() + const data = await db.query.settings.findFirst() + if (data) { + await db.update(settings).set({ width, height }).where(eq(settings.id, data.id)) + } }) mainWindow.webContents.setWindowOpenHandler((details) => { @@ -58,7 +91,7 @@ app.whenReady().then(async () => { await runMigrate() - createWindow() + await createWindow() app.on('activate', () => { // On macOS it's common to re-create a window in the app when the diff --git a/apps/desktop/src/renderer/components.d.ts b/apps/desktop/src/renderer/components.d.ts index dd7085743..c31097493 100644 --- a/apps/desktop/src/renderer/components.d.ts +++ b/apps/desktop/src/renderer/components.d.ts @@ -14,9 +14,26 @@ declare module 'vue' { ConnectionsDetailsView: typeof import('./../../../../packages/ui/src/components/connections/DetailsView.vue')['default'] ConnectionsListView: typeof import('./../../../../packages/ui/src/components/connections/ListView.vue')['default'] ElAside: typeof import('element-plus/es')['ElAside'] + ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete'] + ElButton: typeof import('element-plus/es')['ElButton'] + ElCascaderPanel: typeof import('element-plus/es')['ElCascaderPanel'] + ElCol: typeof import('element-plus/es')['ElCol'] ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] ElContainer: typeof import('element-plus/es')['ElContainer'] + ElDivider: typeof import('element-plus/es')['ElDivider'] + ElIconDelete: typeof import('@element-plus/icons-vue')['Delete'] + ElIconDownload: typeof import('@element-plus/icons-vue')['Download'] + ElIconPrinter: typeof import('@element-plus/icons-vue')['Printer'] + ElIconUpload: typeof import('@element-plus/icons-vue')['Upload'] + ElIconWarning: typeof import('@element-plus/icons-vue')['Warning'] + ElInput: typeof import('element-plus/es')['ElInput'] + ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] ElMain: typeof import('element-plus/es')['ElMain'] + ElOption: typeof import('element-plus/es')['ElOption'] + ElRow: typeof import('element-plus/es')['ElRow'] + ElSelect: typeof import('element-plus/es')['ElSelect'] + ElSwitch: typeof import('element-plus/es')['ElSwitch'] + ElTooltip: typeof import('element-plus/es')['ElTooltip'] HelpView: typeof import('./../../../../packages/ui/src/components/HelpView.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] diff --git a/apps/desktop/src/renderer/src/main.ts b/apps/desktop/src/renderer/src/main.ts index f9b8a8e52..87c745870 100644 --- a/apps/desktop/src/renderer/src/main.ts +++ b/apps/desktop/src/renderer/src/main.ts @@ -1,21 +1,27 @@ -import { i18n } from '@mqttx/ui/i18n' -// import { useSettingsStore } from '@mqttx/ui/stores' +import type { PlatformType } from 'mqttx' +import useSettingsService from '@database/services/SettingsService' +import { i18n } from '@mqttx/ui/i18n' import App from './App.vue' -import { router } from './router' +import { router } from './router' import '@mqttx/ui/styles.scss' import './assets/scss/main.scss' // Create Vue const app = createApp(App) - const pinia = createPinia() +app.provide('platformType', 'desktop') + app.use(router).use(pinia) -// I18n -// const { settings } = useSettingsStore() -// i18n.global.locale = settings.currentLang +const { getSettingsInDB } = useSettingsService() + +getSettingsInDB().then(() => { + // I18n + const { settings } = useSettingsService() + i18n.global.locale = settings.currentLang -app.use(i18n).mount('#app') + app.use(i18n).mount('#app') +}) diff --git a/apps/desktop/src/renderer/src/pages/settings.vue b/apps/desktop/src/renderer/src/pages/settings.vue index 0543187be..030693a0a 100644 --- a/apps/desktop/src/renderer/src/pages/settings.vue +++ b/apps/desktop/src/renderer/src/pages/settings.vue @@ -1,35 +1,9 @@ diff --git a/apps/desktop/tsconfig.web.json b/apps/desktop/tsconfig.web.json index 3cc60f9cd..1c66fe6c0 100644 --- a/apps/desktop/tsconfig.web.json +++ b/apps/desktop/tsconfig.web.json @@ -5,7 +5,8 @@ "baseUrl": ".", "moduleResolution": "bundler", "paths": { - "@renderer/*": ["src/renderer/src/*"] + "@renderer/*": ["src/renderer/src/*"], + "@database/*": ["src/database/*"] }, "types": ["element-plus/global", "node", "mqttx", "unplugin-icons/types/vue"], "verbatimModuleSyntax": true diff --git a/apps/web/components.d.ts b/apps/web/components.d.ts index 4e094ce5e..ead027120 100644 --- a/apps/web/components.d.ts +++ b/apps/web/components.d.ts @@ -22,6 +22,7 @@ declare module 'vue' { ElContainer: typeof import('element-plus/es')['ElContainer'] ElDivider: typeof import('element-plus/es')['ElDivider'] ElIconDelete: typeof import('@element-plus/icons-vue')['Delete'] + ElIconDownload: typeof import('@element-plus/icons-vue')['Download'] ElIconPrinter: typeof import('@element-plus/icons-vue')['Printer'] ElIconUpload: typeof import('@element-plus/icons-vue')['Upload'] ElIconWarning: typeof import('@element-plus/icons-vue')['Warning'] diff --git a/apps/web/src/database/services/SettingsService.ts b/apps/web/src/database/services/SettingsService.ts index 378f615b7..c98d7c09c 100644 --- a/apps/web/src/database/services/SettingsService.ts +++ b/apps/web/src/database/services/SettingsService.ts @@ -2,7 +2,7 @@ import type { RxSettingsDocument, RxSettingsDocumentType } from '@/database/sche import type { Subscription } from 'rxjs' import { useDatabase } from '@/database' -import { useSettingsStore } from '@mqttx/ui' +import { useSettingsStore } from '@mqttx/ui/stores' export default function useSettingsService() { const db = useDatabase() diff --git a/apps/web/src/main.ts b/apps/web/src/main.ts index 94f7ce095..bc558a409 100644 --- a/apps/web/src/main.ts +++ b/apps/web/src/main.ts @@ -1,9 +1,10 @@ +import type { PlatformType } from 'mqttx' import { i18n } from '@mqttx/ui/i18n' -import App from './App.vue' +import App from './App.vue' import { createDatabase } from './database' -import useSettingsService from './database/services/SettingsService' +import useSettingsService from './database/services/SettingsService' import { router } from './router' import '@mqttx/ui/styles.scss' import './assets/scss/main.scss' @@ -12,9 +13,10 @@ const database = createDatabase() // Create Vue const app = createApp(App) - const pinia = createPinia() +app.provide('platformType', 'web') + app.use(router).use(pinia) database.then(async (db) => { diff --git a/packages/types/base.ts b/packages/types/base.ts index 67a9c6b45..b92dfa26e 100644 --- a/packages/types/base.ts +++ b/packages/types/base.ts @@ -1,3 +1,5 @@ +export type PlatformType = 'desktop' | 'web' + export type MQTTVersion = 3 | 4 | 5 export type Protocol = 'mqtt' | 'mqtts' diff --git a/packages/ui/components.d.ts b/packages/ui/components.d.ts index 66dc03f38..0f88d63cd 100644 --- a/packages/ui/components.d.ts +++ b/packages/ui/components.d.ts @@ -20,6 +20,7 @@ declare module 'vue' { ElCol: typeof import('element-plus/es')['ElCol'] ElDivider: typeof import('element-plus/es')['ElDivider'] ElIconDelete: typeof import('@element-plus/icons-vue')['Delete'] + ElIconDownload: typeof import('@element-plus/icons-vue')['Download'] ElIconPrinter: typeof import('@element-plus/icons-vue')['Printer'] ElIconUpload: typeof import('@element-plus/icons-vue')['Upload'] ElIconWarning: typeof import('@element-plus/icons-vue')['Warning'] diff --git a/packages/ui/src/components/SettingsView.vue b/packages/ui/src/components/SettingsView.vue index dbf713799..923dd1a33 100644 --- a/packages/ui/src/components/SettingsView.vue +++ b/packages/ui/src/components/SettingsView.vue @@ -1,9 +1,11 @@