diff --git a/CHANGELOG.md b/CHANGELOG.md index f448fc86..2421aab6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Add scheme type options for vector/raster tile - Add `tileSize` field for raster and raster-dem tile sources - Update Protomaps Light gallery style to v4 +- Add support to edit local files on the file system - _...Add new stuff here..._ ### 🐞 Bug fixes diff --git a/package-lock.json b/package-lock.json index 85f6f768..b68c2bd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,6 +94,7 @@ "@types/react-icon-base": "^2.1.6", "@types/string-hash": "^1.1.3", "@types/uuid": "^9.0.8", + "@types/wicg-file-system-access": "^2023.10.5", "@vitejs/plugin-react": "^4.2.1", "cors": "^2.8.5", "cypress": "^13.13.0", @@ -2395,6 +2396,13 @@ "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", "dev": true }, + "node_modules/@types/wicg-file-system-access": { + "version": "2023.10.5", + "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2023.10.5.tgz", + "integrity": "sha512-e9kZO9kCdLqT2h9Tw38oGv9UNzBBWaR1MzuAavxPcsV/7FJ3tWbU6RI3uB+yKIDPGLkGVbplS52ub0AcRLvrhA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", diff --git a/package.json b/package.json index fd307127..592f3cc7 100644 --- a/package.json +++ b/package.json @@ -125,6 +125,7 @@ "@types/react-icon-base": "^2.1.6", "@types/string-hash": "^1.1.3", "@types/uuid": "^9.0.8", + "@types/wicg-file-system-access": "^2023.10.5", "@vitejs/plugin-react": "^4.2.1", "cors": "^2.8.5", "cypress": "^13.13.0", diff --git a/src/components/App.tsx b/src/components/App.tsx index 3c5509cf..86a94de2 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -129,6 +129,7 @@ type AppState = { export: boolean debug: boolean } + fileHandle: FileSystemFileHandle | null } export default class App extends React.Component { @@ -284,6 +285,7 @@ export default class App extends React.Component { openlayersDebugOptions: { debugToolbox: false, }, + fileHandle: null, } this.layerWatcher = new LayerWatcher({ @@ -611,7 +613,8 @@ export default class App extends React.Component { } } - openStyle = (styleObj: StyleSpecification & {id: string}) => { + openStyle = (styleObj: StyleSpecification & {id: string}, fileHandle: FileSystemFileHandle | null) => { + this.setState({fileHandle: fileHandle}); styleObj = this.setDefaultValues(styleObj) this.onStyleChanged(styleObj) } @@ -847,6 +850,10 @@ export default class App extends React.Component { this.setModal(modalName, !this.state.isOpen[modalName]); } + onSetFileHandle(fileHandle: FileSystemFileHandle | null) { + this.setState({fileHandle: fileHandle}); + } + onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => { this.setState({ openlayersDebugOptions: { @@ -949,11 +956,14 @@ export default class App extends React.Component { onStyleChanged={this.onStyleChanged} isOpen={this.state.isOpen.export} onOpenToggle={this.toggleModal.bind(this, 'export')} + fileHandle={this.state.fileHandle} + onSetFileHandle={this.onSetFileHandle} /> { {t("Open")} - - {t("Export")} + + {t("Save")} diff --git a/src/components/ModalExport.tsx b/src/components/ModalExport.tsx index 1fa6b32b..684774c4 100644 --- a/src/components/ModalExport.tsx +++ b/src/components/ModalExport.tsx @@ -4,7 +4,7 @@ import {saveAs} from 'file-saver' import {version} from 'maplibre-gl/package.json' import {format} from '@maplibre/maplibre-gl-style-spec' import type {StyleSpecification} from 'maplibre-gl' -import {MdFileDownload} from 'react-icons/md' +import {MdMap, MdSave} from 'react-icons/md' import { WithTranslation, withTranslation } from 'react-i18next'; import FieldString from './FieldString' @@ -22,6 +22,8 @@ type ModalExportInternalProps = { onStyleChanged(...args: unknown[]): unknown isOpen: boolean onOpenToggle(...args: unknown[]): unknown + onSetFileHandle(fileHandle: FileSystemFileHandle | null): unknown + fileHandle: FileSystemFileHandle | null } & WithTranslation; @@ -47,7 +49,7 @@ class ModalExportInternal extends React.Component { } } - downloadHtml() { + createHtml() { const tokenStyle = this.tokenizedStyle(); const htmlTitle = this.props.mapStyle.name || this.props.t("Map"); const html = ` @@ -81,11 +83,49 @@ class ModalExportInternal extends React.Component { saveAs(blob, exportName + ".html"); } - downloadStyle() { + async saveStyle() { const tokenStyle = this.tokenizedStyle(); - const blob = new Blob([tokenStyle], {type: "application/json;charset=utf-8"}); - const exportName = this.exportName(); - saveAs(blob, exportName + ".json"); + + let fileHandle = this.props.fileHandle; + if (fileHandle == null) { + fileHandle = await this.createFileHandle(); + this.props.onSetFileHandle(fileHandle) + if (fileHandle == null) return; + } + + const writable = await fileHandle.createWritable(); + await writable.write(tokenStyle); + await writable.close(); + this.props.onOpenToggle(); + } + + async saveStyleAs() { + const tokenStyle = this.tokenizedStyle(); + + const fileHandle = await this.createFileHandle(); + this.props.onSetFileHandle(fileHandle) + if (fileHandle == null) return; + + const writable = await fileHandle.createWritable(); + await writable.write(tokenStyle); + await writable.close(); + this.props.onOpenToggle(); + } + + async createFileHandle() : Promise { + const pickerOpts: SaveFilePickerOptions = { + types: [ + { + description: "json", + accept: { "application/json": [".json"] }, + }, + ], + suggestedName: this.exportName(), + }; + + const fileHandle = await window.showSaveFilePicker(pickerOpts) as FileSystemFileHandle; + this.props.onSetFileHandle(fileHandle) + return fileHandle; } changeMetadataProperty(property: string, value: any) { @@ -107,14 +147,14 @@ class ModalExportInternal extends React.Component { data-wd-key="modal:export" isOpen={this.props.isOpen} onOpenToggle={this.props.onOpenToggle} - title={t('Export Style')} + title={t('Save Style')} className="maputnik-export-modal" >
-

{t("Download Style")}

+

{t("Save Style")}

- {t("Download a JSON style to your computer.")} + {t("Save the JSON style to your computer.")}

@@ -140,17 +180,23 @@ class ModalExportInternal extends React.Component {
+ + {t("Save")} + + - - {t("Download Style")} + + {t("Save as")} - - {t("Download HTML")} + + {t("Create HTML")}
diff --git a/src/components/ModalOpen.tsx b/src/components/ModalOpen.tsx index d2450336..2761d14e 100644 --- a/src/components/ModalOpen.tsx +++ b/src/components/ModalOpen.tsx @@ -1,7 +1,6 @@ import React, { FormEvent } from 'react' import {MdFileUpload} from 'react-icons/md' import {MdAddCircleOutline} from 'react-icons/md' -import FileReaderInput, { Result } from 'react-file-reader-input' import { Trans, WithTranslation, withTranslation } from 'react-i18next'; import ModalLoading from './ModalLoading' @@ -47,6 +46,7 @@ type ModalOpenInternalProps = { isOpen: boolean onOpenToggle(...args: unknown[]): unknown onStyleOpen(...args: unknown[]): unknown + fileHandle: FileSystemFileHandle | null } & WithTranslation; type ModalOpenState = { @@ -135,29 +135,37 @@ class ModalOpenInternal extends React.Component { - const [, file] = files[0]; - const reader = new FileReader(); - + onOpenFile = async () => { this.clearError(); - reader.readAsText(file, "UTF-8"); - reader.onload = e => { - let mapStyle; - try { - mapStyle = JSON.parse(e.target?.result as string) - } - catch(err) { - this.setState({ - error: (err as Error).toString() - }); - return; - } - mapStyle = style.ensureStyleValidity(mapStyle) - this.props.onStyleOpen(mapStyle); - this.onOpenToggle(); + const pickerOpts: OpenFilePickerOptions = { + types: [ + { + description: "json", + accept: { "application/json": [".json"] }, + }, + ], + multiple: false, + }; + + const [fileHandle] = await window.showOpenFilePicker(pickerOpts) as Array; + const file = await fileHandle.getFile(); + const content = await file.text(); + + let mapStyle; + try { + mapStyle = JSON.parse(content) + } catch (err) { + this.setState({ + error: (err as Error).toString() + }); + return; } - reader.onerror = e => console.log(e.target); + mapStyle = style.ensureStyleValidity(mapStyle) + + this.props.onStyleOpen(mapStyle, fileHandle); + this.onOpenToggle(); + return file; } onOpenToggle() { @@ -196,7 +204,7 @@ class ModalOpenInternal extends React.Component {errorElement}
-

{t("Upload Style")}

-

{t("Upload a JSON style from your computer.")}

- - {t("Upload")} - +

{t("Open local Style")}

+

{t("Open a local JSON style from your computer.")}

+
+ {t("Open Style")} + +
diff --git a/src/locales/README.md b/src/locales/README.md index 6e456701..f031262b 100644 --- a/src/locales/README.md +++ b/src/locales/README.md @@ -1,6 +1,6 @@ ## Internationalization -The process of internationlization is pretty straight forward for Maputnik. +The process of internationalization is pretty straight forward for Maputnik. ## Add a new language diff --git a/src/locales/de/translation.json b/src/locales/de/translation.json index 1aaab7f6..75072939 100644 --- a/src/locales/de/translation.json +++ b/src/locales/de/translation.json @@ -29,7 +29,7 @@ "Map view": "Kartenansicht", "Maputnik on GitHub": "Maputnik auf GitHub", "Open": "Öffnen", - "Export": "Exportieren", + "Save": "Speichern", "Data Sources": "Datenquellen", "Style Settings": "Stileinstellungen", "View": "Ansicht", @@ -81,17 +81,14 @@ "Close modal": "Modale Fenster schließen", "Debug": "Debug", "Options": "Optionen", - "<0>Open in OSM — Opens the current view on openstreetmap.org": "<0>In OSM öffnen — Öffnet die aktuelle Ansicht auf openstreetmap.org", - "Export Style": "Stil exportieren", - "Download Style": "Stil herunterladen", - "Download a JSON style to your computer.": "Lade einen JSON-Stil auf deinen Computer herunter.", - "Download HTML": "HTML herunterladen", + "Save Style": "Stil Speichern", + "Save the JSON style to your computer.": "Speichere den JSON Stil auf deinem Computer.", + "Save as": "Speichern unter", + "Create HTML": "HTML erstellen", "Cancel": "Abbrechen", "Open Style": "Stil öffnen", - "Upload Style": "Stil hochladen", - "Upload a JSON style from your computer.": "Lade einen JSON-Stil von deinem Computer hoch.", - "Style file": "Stildatei", - "Upload": "Hochladen", + "Open local Style": "Lokalen Stil öffnen", + "Open a local JSON style from your computer.": "Öffne einen lokalen JSON Stil von deinem Computer.", "Load from URL": "Von URL laden", "Load from a URL. Note that the URL must have <1>CORS enabled.": "Von einer URL laden. Beachte, dass die URL <1>CORS aktiviert haben muss.", "Style URL": "Stil-URL", diff --git a/src/locales/fr/translation.json b/src/locales/fr/translation.json index bcbf8d8c..e4957469 100644 --- a/src/locales/fr/translation.json +++ b/src/locales/fr/translation.json @@ -29,7 +29,7 @@ "Map view": "Vue de la carte", "Maputnik on GitHub": "Maputnik sur GitHub", "Open": "Ouvrir", - "Export": "Exporter", + "Save": "Enregistrer", "Data Sources": "Sources de données", "Style Settings": "Paramètres du style", "View": "Vue", @@ -81,17 +81,14 @@ "Close modal": "Fermer la fenêtre modale", "Debug": "Déboguer", "Options": "Options", - "<0>Open in OSM — Opens the current view on openstreetmap.org": "<0>Ouvrir dans OSM — Ouvre la vue actuelle sur openstreetmap.org", - "Export Style": "Exporter le style", - "Download Style": "Télécharger le style", - "Download a JSON style to your computer.": "Téléchargez un style JSON sur votre ordinateur.", - "Download HTML": "Télécharger HTML", + "Save Style": "Enregistrer le style", + "Save the JSON style to your computer.": "Enregistrer le style JSON sur votre ordinateur.", + "Save as": "Enregistrer sous", + "Create HTML": "Créer le HTML", "Cancel": "Annuler", "Open Style": "Ouvrir le style", - "Upload Style": "Transférer un style", - "Upload a JSON style from your computer.": "Transférer un style JSON depuis votre ordinateur.", - "Style file": "Fichier de style", - "Upload": "Transférer", + "Open local Style": "Ouvrir un style local", + "Open a local JSON style from your computer.": "Ouvrir un style JSON local depuis votre ordinateur.", "Load from URL": "Charger depuis une URL", "Load from a URL. Note that the URL must have <1>CORS enabled.": "Charger depuis une URL. Notez que l'URL doit avoir les <1>CORS activés.", "Style URL": "URL du style", diff --git a/src/locales/he/translation.json b/src/locales/he/translation.json index 561eda93..5ed72d4d 100644 --- a/src/locales/he/translation.json +++ b/src/locales/he/translation.json @@ -29,7 +29,7 @@ "Map view": "תצוגת מפה", "Maputnik on GitHub": "מפוטניק בגיטהב", "Open": "פתיחה", - "Export": "ייצוא", + "Save": "שמור", "Data Sources": "מקורות מידע", "Style Settings": "הגדרות הסטייל", "View": "תצוגה", @@ -81,16 +81,14 @@ "Close modal": "סגירת חלונית", "Debug": "דיבאג", "Options": "אפשרויות", - "Export Style": "ייצוא של הסטייל", - "Download Style": "הורדה של הסטייל", - "Download a JSON style to your computer.": "הורדה של הסטייל למחשב", - "Download HTML": "הורדה כ-HTML", + "Save Style": "שמירת הסטייל", + "Save the JSON style to your computer.": "שמירת הסטייל JSON במחשב שלך.", + "Save as": "שמירה בשם", + "Create HTML": "צור HTML", "Cancel": "ביטול", "Open Style": "פתיחת סטייל", - "Upload Style": "העלאה של סטייל", - "Upload a JSON style from your computer.": "העלאה של סטייל מהמחשב", - "Style file": "קובץ סטייל", - "Upload": "העלאה", + "Open local Style": "פתיחת סטייל מקומי", + "Open a local JSON style from your computer.": "פתיחת סטייל JSON מקומי מהמחשב שלך.", "Load from URL": "פתיחה מתוך כתובת", "Load from a URL. Note that the URL must have <1>CORS enabled.": "פתיחה מכתובת, שימו לב: הכתובת צריכה לתמוך ב- CORS", "Style URL": "כתוסת סטייל", diff --git a/src/locales/ja/translation.json b/src/locales/ja/translation.json index a2b48a0f..ba380259 100644 --- a/src/locales/ja/translation.json +++ b/src/locales/ja/translation.json @@ -29,7 +29,7 @@ "Map view": "地図画面", "Maputnik on GitHub": "GitHubのMaputnik", "Open": "開く", - "Export": "エクスポート", + "Save": "保存", "Data Sources": "データソース", "Style Settings": "スタイル設定", "View": "表示", @@ -81,16 +81,14 @@ "Close modal": "モーダルを閉じる", "Debug": "デバッグ", "Options": "設定", - "Export Style": "スタイルをエクスポート", - "Download Style": "スタイルをダウンロード", - "Download a JSON style to your computer.": "パソコンにJSONスタイルをダウンロードします。", - "Download HTML": "HTMLをダウンロード", + "Save Style": "スタイルを保存", + "Save the JSON style to your computer.": "JSONスタイルをコンピュータに保存します。", + "Save as": "名前を付けて保存", + "Create HTML": "HTMLを作成", "Cancel": "キャンセル", "Open Style": "スタイルを開く", - "Upload Style": "スタイルをアップロードする", - "Upload a JSON style from your computer.": "JSONスタイルをパソコンからアップロードする", - "Style file": "スタイルファイル", - "Upload": "アップロード", + "Open local Style": "ローカルスタイルを開く", + "Open a local JSON style from your computer.": "コンピュータからローカルJSONスタイルを開きます。", "Load from URL": "URLから読み込む", "Load from a URL. Note that the URL must have <1>CORS enabled.": "URLから読み込む。注意: URLは <1>CORSを有効にする 必要があります。", "Style URL": "スタイルURL", diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index fc93a80a..63a76063 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -29,7 +29,7 @@ "Map view": "地图视图", "Maputnik on GitHub": "GitHub上的Maputnik", "Open": "打开", - "Export": "导出", + "Save": "保存", "Data Sources": "数据源", "Style Settings": "样式设置", "View": "视图", @@ -81,16 +81,14 @@ "Close modal": "关闭模态框", "Debug": "调试", "Options": "选项", - "Export Style": "导出样式", - "Download Style": "下载样式", - "Download a JSON style to your computer.": "将JSON样式下载到您的电脑。", - "Download HTML": "下载HTML", + "Save Style": "保存样式", + "Save the JSON style to your computer.": "将JSON样式保存到您的计算机。", + "Save as": "另存为", + "Create HTML": "创建HTML", "Cancel": "取消", "Open Style": "打开样式", - "Upload Style": "上传样式", - "Upload a JSON style from your computer.": "从您的电脑上传JSON样式。", - "Style file": "样式文件", - "Upload": "上传", + "Open local Style": "打开本地样式", + "Open a local JSON style from your computer.": "从您的计算机打开本地JSON样式。", "Load from URL": "从URL加载", "Load from a URL. Note that the URL must have <1>CORS enabled.": "从URL加载。注意:URL必须启用 <1>CORS。", "Style URL": "样式URL", diff --git a/tsconfig.json b/tsconfig.json index 24f33888..46f4ef8f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], - "types": ["geojson"], + "types": ["geojson", "@types/wicg-file-system-access"], "module": "ESNext", "skipLibCheck": true, @@ -27,7 +27,7 @@ "ts-node": { "compilerOptions": { "module": "ESNext", - "moduleResolution": "Node" + "moduleResolution": "Node", } } -} \ No newline at end of file +}