Skip to content
Closed
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
77 changes: 65 additions & 12 deletions src/core/settings.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
const fs = require('fs')
const path = require('path')

const SETTINGS_VERSION = 2

// Keys that were stored at the root in v1 and belong to mif-to-xlsx options now.
const LEGACY_MIF_TO_XLSX_KEYS = [
'recursive',
'paintRows',
'skipBlack',
'combineIntoOneWorkbook',
'combinedName',
'includeCsv',
'includeColorColumn',
'colorColumnName',
'freezeHeader',
'autofilter',
]

function getDefaultSettings() {
return {
version: SETTINGS_VERSION,
language: 'en',
inputMode: 'folder',
inputFolder: '',
outputFolder: '',
selectedFiles: [],
recursive: true,
skipBlack: true,
paintRows: true,
combineIntoOneWorkbook: false,
combinedName: '',
includeCsv: false,
includeColorColumn: true,
colorColumnName: '',
freezeHeader: true,
autofilter: true,
converterId: 'mif-to-xlsx',
converterOptions: {},
}
}

Expand All @@ -29,19 +38,63 @@ function loadSettings(settingsPath) {

const raw = fs.readFileSync(settingsPath, 'utf8')
const parsed = JSON.parse(raw)
return { ...getDefaultSettings(), ...parsed }
return migrate(parsed)
} catch (error) {
return getDefaultSettings()
}
}

function migrate(stored) {
const base = getDefaultSettings()
if (!stored || typeof stored !== 'object') {
return base
}

// Already on the latest format.
if (stored.version === SETTINGS_VERSION) {
return {
...base,
...stored,
converterOptions: { ...stored.converterOptions },
}
}

// v1: flat option keys at the root, no converterId, no converterOptions.
const carryOver = {
language: stored.language,
inputMode: stored.inputMode,
inputFolder: stored.inputFolder,
outputFolder: stored.outputFolder,
selectedFiles: stored.selectedFiles,
}

const mifToXlsxOptions = {}
for (const key of LEGACY_MIF_TO_XLSX_KEYS) {
if (Object.prototype.hasOwnProperty.call(stored, key)) {
mifToXlsxOptions[key] = stored[key]
}
}

return {
...base,
...Object.fromEntries(Object.entries(carryOver).filter(([, v]) => v !== undefined)),
converterId: 'mif-to-xlsx',
converterOptions: Object.keys(mifToXlsxOptions).length
? { 'mif-to-xlsx': mifToXlsxOptions }
: {},
}
}

function saveSettings(settingsPath, settings) {
fs.mkdirSync(path.dirname(settingsPath), { recursive: true })
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8')
const payload = { ...settings, version: SETTINGS_VERSION }
fs.writeFileSync(settingsPath, JSON.stringify(payload, null, 2), 'utf8')
}

module.exports = {
getDefaultSettings,
loadSettings,
saveSettings,
migrate,
SETTINGS_VERSION,
}
43 changes: 38 additions & 5 deletions src/main/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const { app, BrowserWindow, dialog, ipcMain, shell } = require('electron')
const path = require('path')
const { Worker } = require('worker_threads')
const { loadSettings, saveSettings } = require('../core/settings')
const converters = require('../core/converters')

let mainWindow

Expand Down Expand Up @@ -62,13 +63,11 @@ ipcMain.handle('dialog:selectOutputFolder', async () => {
return result.filePaths[0]
})

ipcMain.handle('dialog:selectFiles', async () => {
ipcMain.handle('dialog:selectFiles', async (event, options) => {
const filters = buildFileFilters(options && options.extensions)
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile', 'multiSelections'],
filters: [
{ name: 'MapInfo files', extensions: ['mif', 'mid'] },
{ name: 'All files', extensions: ['*'] },
],
filters,
})

if (result.canceled || !result.filePaths.length) {
Expand All @@ -78,6 +77,40 @@ ipcMain.handle('dialog:selectFiles', async () => {
return result.filePaths
})

function buildFileFilters(extensions) {
const clean = Array.isArray(extensions)
? extensions.map((e) => String(e).replace(/^\./, '')).filter(Boolean)
: ['mif', 'mid']
return [
{ name: clean.join('/').toUpperCase() || 'Files', extensions: clean },
{ name: 'All files', extensions: ['*'] },
]
}

ipcMain.handle('converters:list', async () => {
return converters.list().map(serializeConverter)
})

function serializeConverter(c) {
return {
id: c.id,
name: c.name,
description: c.description || '',
inputs: { extensions: [...c.inputs.extensions], type: c.inputs.type },
outputs: { extensions: [...c.outputs.extensions], type: c.outputs.type },
options: c.options.map((o) => ({
key: o.key,
type: o.type,
label: o.label || o.key,
description: o.description || '',
default: o.default,
values: o.values ? [...o.values] : undefined,
min: o.min,
max: o.max,
})),
}
}

ipcMain.handle('settings:load', async () => {
return loadSettings(getSettingsPath())
})
Expand Down
3 changes: 2 additions & 1 deletion src/main/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ const { contextBridge, ipcRenderer, webUtils } = require('electron')
contextBridge.exposeInMainWorld('api', {
selectInputFolder: () => ipcRenderer.invoke('dialog:selectInputFolder'),
selectOutputFolder: () => ipcRenderer.invoke('dialog:selectOutputFolder'),
selectFiles: () => ipcRenderer.invoke('dialog:selectFiles'),
selectFiles: (options) => ipcRenderer.invoke('dialog:selectFiles', options),
listConverters: () => ipcRenderer.invoke('converters:list'),
loadSettings: () => ipcRenderer.invoke('settings:load'),
saveSettings: (settings) => ipcRenderer.invoke('settings:save', settings),
startConversion: (config) => ipcRenderer.invoke('convert:start', config),
Expand Down
34 changes: 31 additions & 3 deletions src/main/worker.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,42 @@
const { parentPort, workerData } = require('worker_threads')
const { runConversion } = require('../core/convert')
const converters = require('../core/converters')

const listeners = {
log: (message) => parentPort.postMessage({ type: 'log', message }),
progress: (payload) => parentPort.postMessage({ type: 'progress', payload }),
}

runConversion(workerData.config, listeners)
async function run() {
const { config } = workerData
const { converterId, inputs, output, options } = config
const converter = converters.get(converterId)

if (!converter) {
throw new Error(`Unknown converter: ${converterId || '<unset>'}`)
}

const validated = converters.validateOptions(converter, options || {})
if (validated.errors.length) {
throw new Error(`Invalid options:\n ${validated.errors.join('\n ')}`)
}

return converter.run(
{ inputs: inputs || [], output: output || '', options: validated.merged },
listeners,
)
}

run()
.then((result) => {
parentPort.postMessage({ type: 'done', result })
parentPort.postMessage({
type: 'done',
result: {
outputs: result.outputs || [],
processed: result.stats?.processed || 0,
skipped: result.stats?.skipped || 0,
errors: result.stats?.errors || [],
},
})
})
.catch((error) => {
parentPort.postMessage({
Expand Down
21 changes: 15 additions & 6 deletions src/renderer/i18n.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
;(function () {
const DICT = {
en: {
'app.subtitle': 'Convert MIF/MID into XLSX with optional row fill from region color.',
'app.subtitle': 'MapInfo data toolkit. Pick a converter and run it.',
'section.converter': 'Converter',
'field.converter': 'Conversion direction',
'hint.optionsEmpty': 'This converter has no options.',
'section.source': 'Source',
'mode.folder': 'Folder',
'mode.files': 'Files',
Expand Down Expand Up @@ -34,7 +37,7 @@
'status.done': 'Done. Processed: {n}',
'status.doneErrors': 'Done with errors. Processed: {n}',
'section.log': 'Log',
'drop.hint': 'Drop MIF/MID files or a folder here',
'drop.hint': 'Drop files or a folder here',
'field.language': 'Language',
'field.combinedName': 'Combined workbook file name',
'placeholder.combinedName': 'mapinfo-converted.xlsx',
Expand All @@ -43,7 +46,10 @@
'progress.of': '{done} of {total}',
},
ru: {
'app.subtitle': 'Конвертация MIF/MID в XLSX с опциональной заливкой строк по цвету региона.',
'app.subtitle': 'Набор инструментов для данных MapInfo. Выберите конвертер и запустите.',
'section.converter': 'Конвертер',
'field.converter': 'Направление конвертации',
'hint.optionsEmpty': 'У этого конвертера нет параметров.',
'section.source': 'Источник',
'mode.folder': 'Папка',
'mode.files': 'Файлы',
Expand Down Expand Up @@ -76,7 +82,7 @@
'status.done': 'Готово. Обработано: {n}',
'status.doneErrors': 'Завершено с ошибками. Обработано: {n}',
'section.log': 'Журнал',
'drop.hint': 'Перетащите сюда файлы MIF/MID или папку',
'drop.hint': 'Перетащите сюда файлы или папку',
'field.language': 'Язык',
'field.combinedName': 'Имя объединённой книги',
'placeholder.combinedName': 'mapinfo-converted.xlsx',
Expand All @@ -85,7 +91,10 @@
'progress.of': '{done} из {total}',
},
kk: {
'app.subtitle': 'MIF/MID файлдарын XLSX пішіміне түрлендіру, жол фонын аймақ түсімен бояу мүмкіндігі.',
'app.subtitle': 'MapInfo деректерімен жұмыс істеуге арналған құралдар жинағы. Конвертерді таңдап, іске қосыңыз.',
'section.converter': 'Конвертер',
'field.converter': 'Түрлендіру бағыты',
'hint.optionsEmpty': 'Бұл конвертерде параметрлер жоқ.',
'section.source': 'Дереккөз',
'mode.folder': 'Қалта',
'mode.files': 'Файлдар',
Expand Down Expand Up @@ -118,7 +127,7 @@
'status.done': 'Дайын. Өңделді: {n}',
'status.doneErrors': 'Қателермен аяқталды. Өңделді: {n}',
'section.log': 'Журнал',
'drop.hint': 'MIF/MID файлдарын немесе қалтаны осы жерге тастаңыз',
'drop.hint': 'Файлдарды немесе қалтаны осы жерге тастаңыз',
'field.language': 'Тіл',
'field.combinedName': 'Біріктірілген кітап атауы',
'placeholder.combinedName': 'mapinfo-converted.xlsx',
Expand Down
55 changes: 11 additions & 44 deletions src/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<header class="hero">
<div>
<h1>MifKit</h1>
<p data-i18n="app.subtitle">Convert MIF/MID into XLSX with optional row fill from region color.</p>
<p data-i18n="app.subtitle">MapInfo data toolkit. Pick a converter and run it.</p>
</div>
<div class="hero-actions">
<select
Expand All @@ -29,6 +29,13 @@ <h1>MifKit</h1>
</header>

<main class="grid">
<section class="card">
<h2 data-i18n="section.converter">Converter</h2>
<label class="field-label" for="converterSelect" data-i18n="field.converter">Conversion direction</label>
<select id="converterSelect" class="converter-select"></select>
<p id="converterDescription" class="hint"></p>
</section>

<section class="card drop-zone" id="sourceCard">
<h2 data-i18n="section.source">Source</h2>

Expand All @@ -49,10 +56,6 @@ <h2 data-i18n="section.source">Source</h2>
<input id="inputFolder" type="text" readonly data-i18n-placeholder="placeholder.inputFolder" />
<button id="selectInputFolderButton" data-i18n="btn.browse">Browse</button>
</div>
<label class="checkbox">
<input id="recursive" type="checkbox" checked />
<span data-i18n="opt.recursive">Scan subfolders</span>
</label>
</div>

<div id="filesModeBlock" class="stack hidden">
Expand All @@ -64,7 +67,7 @@ <h2 data-i18n="section.source">Source</h2>
<div id="selectedFilesList" class="file-list empty" data-i18n="list.noFiles">No files selected</div>
</div>

<div class="drop-hint" data-i18n="drop.hint">Drop MIF/MID files or a folder here</div>
<div id="dropHint" class="drop-hint" data-i18n="drop.hint">Drop files or a folder here</div>
</section>

<section class="card">
Expand All @@ -79,44 +82,8 @@ <h2 data-i18n="section.output">Output</h2>

<section class="card">
<h2 data-i18n="section.options">Options</h2>
<div class="options-grid">
<label class="checkbox">
<input id="paintRows" type="checkbox" checked />
<span data-i18n="opt.paintRows">Fill row background in Excel</span>
</label>
<label class="checkbox">
<input id="skipBlack" type="checkbox" checked />
<span data-i18n="opt.skipBlack">Skip black color (#000000)</span>
</label>
<label class="checkbox">
<input id="combineIntoOneWorkbook" type="checkbox" />
<span data-i18n="opt.combine">Combine all files into one workbook</span>
</label>
<label class="checkbox">
<input id="includeCsv" type="checkbox" />
<span data-i18n="opt.csv">Also export CSV</span>
</label>
<label class="checkbox">
<input id="includeColorColumn" type="checkbox" checked />
<span data-i18n="opt.colorColumn">Add color column</span>
</label>
<label class="checkbox">
<input id="freezeHeader" type="checkbox" checked />
<span data-i18n="opt.freeze">Freeze header row</span>
</label>
<label class="checkbox">
<input id="autofilter" type="checkbox" checked />
<span data-i18n="opt.autofilter">Add autofilter</span>
</label>
</div>
<div id="combinedNameBlock" class="combined-name-block hidden">
<label class="field-label" for="combinedName" data-i18n="field.combinedName">Combined workbook file name</label>
<input id="combinedName" type="text" data-i18n-placeholder="placeholder.combinedName" />
</div>
<div id="colorColumnNameBlock" class="combined-name-block">
<label class="field-label" for="colorColumnName" data-i18n="field.colorColumnName">Color column name</label>
<input id="colorColumnName" type="text" data-i18n-placeholder="placeholder.colorColumnName" />
</div>
<div id="optionsContainer" class="options-grid"></div>
<p id="optionsEmpty" class="hint hidden" data-i18n="hint.optionsEmpty">This converter has no options.</p>
</section>

<section class="card actions-card">
Expand Down
Loading