Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(desktop): add settings schema and integrate settings service #1824

Merged
merged 5 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/desktop/electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default defineConfig({
renderer: {
resolve: {
alias: {
'@database': resolve('src/database'),
'@': resolve('src/renderer/src'),
},
},
Expand Down
9 changes: 7 additions & 2 deletions apps/desktop/src/database/db.main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>)
function toDrizzleResult(rows: Record<string, any> | Array<Record<string, any>>) {
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/database/db.renderer.ts
Original file line number Diff line number Diff line change
@@ -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 }
Expand All @@ -10,5 +10,5 @@ export const database = drizzle(async (...args) => {
return { rows: [] }
}
}, {
schema: { posts },
schema: { settings },
})
9 changes: 0 additions & 9 deletions apps/desktop/src/database/schemas/posts.ts

This file was deleted.

25 changes: 25 additions & 0 deletions apps/desktop/src/database/schemas/settings.ts
Original file line number Diff line number Diff line change
@@ -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<Settings['currentLang']>().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<Settings['currentTheme']>().notNull().default('light'),
jsonHighlight: integer({ mode: 'boolean' }).notNull().default(true),
logLevel: text().$type<Settings['logLevel']>().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
50 changes: 50 additions & 0 deletions apps/desktop/src/database/services/SettingsService.ts
Original file line number Diff line number Diff line change
@@ -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<SelectSettings> {
let data: SelectSettings | undefined
data = await db.query.settings.findFirst()
if (!data) {
data = await updateSettingsInDB()
}
updateSettings(data)
return data
}
async function updateSettingsInDB(data?: Partial<Settings>): Promise<SelectSettings> {
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,
}
}
43 changes: 38 additions & 5 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
@@ -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'),
Expand All @@ -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) => {
Expand Down Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions apps/desktop/src/renderer/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
22 changes: 14 additions & 8 deletions apps/desktop/src/renderer/src/main.ts
Original file line number Diff line number Diff line change
@@ -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>('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')
})
32 changes: 3 additions & 29 deletions apps/desktop/src/renderer/src/pages/settings.vue
Original file line number Diff line number Diff line change
@@ -1,35 +1,9 @@
<script setup lang="ts">
import type { SelectPosts } from '../../../database/schemas/posts'
import { database } from '../../../database/db.renderer'
import { posts } from '../../../database/schemas/posts'
import useSettingsService from '@database/services/SettingsService'

const postList = ref<SelectPosts[]>([])

async function insertMockData() {
await database.delete(posts).execute()
await database.insert(posts).values([
{
title: 'Hello World',
},
{
title: 'Hello World 2',
},
])
}

insertMockData().then(() => {
console.log('Mock data inserted')
database.query.posts.findMany().then((result) => {
console.log(result)
postList.value = result
})
})
const { settings } = useSettingsService()
</script>

<template>
<!-- <SettingsView /> -->
TODO: Implement desktop database settings
<pre>
{{ postList }}
</pre>
<SettingsView v-model="settings" />
</template>
3 changes: 2 additions & 1 deletion apps/desktop/tsconfig.web.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions apps/web/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/database/services/SettingsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
8 changes: 5 additions & 3 deletions apps/web/src/main.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -12,9 +13,10 @@ const database = createDatabase()

// Create Vue
const app = createApp(App)

const pinia = createPinia()

app.provide<PlatformType>('platformType', 'web')

app.use(router).use(pinia)

database.then(async (db) => {
Expand Down
2 changes: 2 additions & 0 deletions packages/types/base.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export type PlatformType = 'desktop' | 'web'

export type MQTTVersion = 3 | 4 | 5

export type Protocol = 'mqtt' | 'mqtts'
Expand Down
1 change: 1 addition & 0 deletions packages/ui/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
Loading
Loading