diff --git a/README.md b/README.md index f7fcbef2b..5b93499ee 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,27 @@ you work, but if you want the app to read story files initially again, you will need to restart the process. To create a release, run `npm run build`. Finished files will be found under -`dist/`. In order to build Windows apps on OS X or Linux, you will need to have +`dist/`. In order to build Windows apps on macOS or Linux, you will need to have [Wine](https://www.winehq.org/) and [makensis](http://nsis.sourceforge.net/) installed. A file named `2.json` is created under `dist/` which contains information relevant to the autoupdater process, and is currently posted to https://twinery.org/latestversion/2.json. +The build process looks for these environment variables when notarizing a macOS +build: + +- `APPLE_APP_ID`: The app ID to use. The convention is `country.company.appname`. +- `APPLE_ID`: User name of the Apple account to use for notarization. +- `APPLE_ID_PASSWORD`: App-specific password for the Apple account to use for + notarization. + +If any of these environment variables are not set, the build process will skip +notarizing. This means users will need to right-click the application and open +it manually. + +You must have the full Xcode app installed to notarize the app, not just the +Xcode command line tools. + `npm test` will test the source code respectively. `npm run clean` will delete existing files in `electron-build/` and `dist/`. \ No newline at end of file diff --git a/electron-builder.config.js b/electron-builder.config.js index e229e981f..b4267ba7a 100644 --- a/electron-builder.config.js +++ b/electron-builder.config.js @@ -1,10 +1,44 @@ -const child_process = require('child_process'); +const {notarize} = require('@electron/notarize'); +const path = require('path'); const pkg = require('./package.json'); const isPreview = /alpha|beta|pre/.test(pkg.version) || process.env.FORCE_PREVIEW; module.exports = { + async afterSign(context) { + if (context.packager.platform.name === 'mac') { + if (!('APPLE_APP_ID' in process.env)) { + console.log( + 'APPLE_APP_ID environment variable is not set, skipping notarization' + ); + return; + } + + if (!('APPLE_ID' in process.env)) { + console.log( + 'APPLE_ID environment variable is not set, skipping notarization' + ); + return; + } + + if (!('APPLE_ID_PASSWORD' in process.env)) { + console.log( + 'APPLE_ID_PASSWORD environment variable is not set, skipping notarization' + ); + return; + } + + console.log('Notarizing Mac app...'); + await notarize({ + appBundleId: process.env.APPLE_APP_ID, + appPath: path.join(context.appOutDir, `Twine.app`), + appleId: process.env.APPLE_ID, + appleIdPassword: process.env.APPLE_ID_PASSWORD + }); + } + }, + // This step was necessary to ad hoc sign the app. Otherwise, on Apple Silicon // you get repeated prompts for file access. This is commented out because we // are able to sign the app thanks to the Interactive Fiction Technology @@ -20,6 +54,7 @@ module.exports = { // ); // } // }, + appId: 'org.twinery.twine', directories: { output: 'dist/electron' }, diff --git a/package-lock.json b/package-lock.json index 44e05657d..8e68141de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "Twine", - "version": "2.5.1", + "version": "2.6.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "Twine", - "version": "2.5.1", + "version": "2.6.0", "license": "GPL-3.0", "dependencies": { "@popperjs/core": "^2.9.1", @@ -49,6 +49,7 @@ "use-error-boundary": "^2.0.6" }, "devDependencies": { + "@electron/notarize": "^1.2.3", "@playwright/test": "^1.27.1", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", @@ -81,6 +82,7 @@ "cross-var": "^1.1.0", "electron": "^18.3.6", "electron-builder": "^23.3.3", + "electron-builder-notarize": "^1.5.0", "faker": "^5.4.0", "history": "^5.1.0", "jest-axe": "^4.1.0", @@ -1721,6 +1723,34 @@ "node": ">= 4.0.0" } }, + "node_modules/@electron/notarize": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-1.2.3.tgz", + "integrity": "sha512-9oRzT56rKh5bspk3KpAVF8lPKHYQrBnRwcgiOeR0hdilVEQmszDaAu0IPCPrwwzJN0ugNs0rRboTreHMt/6mBQ==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@electron/universal": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.2.1.tgz", @@ -10695,6 +10725,74 @@ "node": ">=14.0.0" } }, + "node_modules/electron-builder-notarize": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/electron-builder-notarize/-/electron-builder-notarize-1.5.0.tgz", + "integrity": "sha512-kbVCnZX3pCKTXiPhyoMjCZYSQnwS04QmlTM2NB2D/2LCsab5UJA0Me9ZqDT3W35ENPglf1WYDKT+tx9i+xuaPA==", + "dev": true, + "dependencies": { + "dotenv": "^8.2.0", + "electron-notarize": "^1.1.1", + "js-yaml": "^3.14.0", + "read-pkg-up": "^7.0.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "electron-builder": ">= 20.44.4" + } + }, + "node_modules/electron-builder-notarize/node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-builder-notarize/node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron-builder-notarize/node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-builder-notarize/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/electron-builder/node_modules/@types/yargs": { "version": "17.0.8", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.8.tgz", @@ -10856,6 +10954,35 @@ "node": ">=12" } }, + "node_modules/electron-notarize": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/electron-notarize/-/electron-notarize-1.2.2.tgz", + "integrity": "sha512-ZStVWYcWI7g87/PgjPJSIIhwQXOaw4/XeXU+pWqMMktSLHaGMLHdyPPN7Cmao7+Cr7fYufA16npdtMndYciHNw==", + "deprecated": "Please use @electron/notarize moving forward. There is no API change, just a package name change", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/electron-osx-sign": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.6.0.tgz", @@ -30654,6 +30781,30 @@ } } }, + "@electron/notarize": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-1.2.3.tgz", + "integrity": "sha512-9oRzT56rKh5bspk3KpAVF8lPKHYQrBnRwcgiOeR0hdilVEQmszDaAu0IPCPrwwzJN0ugNs0rRboTreHMt/6mBQ==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1" + }, + "dependencies": { + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + } + } + }, "@electron/universal": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.2.1.tgz", @@ -38188,6 +38339,81 @@ } } }, + "electron-builder-notarize": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/electron-builder-notarize/-/electron-builder-notarize-1.5.0.tgz", + "integrity": "sha512-kbVCnZX3pCKTXiPhyoMjCZYSQnwS04QmlTM2NB2D/2LCsab5UJA0Me9ZqDT3W35ENPglf1WYDKT+tx9i+xuaPA==", + "dev": true, + "requires": { + "dotenv": "^8.2.0", + "electron-notarize": "^1.1.1", + "js-yaml": "^3.14.0", + "read-pkg-up": "^7.0.0" + }, + "dependencies": { + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, + "electron-notarize": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/electron-notarize/-/electron-notarize-1.2.2.tgz", + "integrity": "sha512-ZStVWYcWI7g87/PgjPJSIIhwQXOaw4/XeXU+pWqMMktSLHaGMLHdyPPN7Cmao7+Cr7fYufA16npdtMndYciHNw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1" + }, + "dependencies": { + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + } + } + }, "electron-osx-sign": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.6.0.tgz", diff --git a/package.json b/package.json index 37700ca39..446eb86f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Twine", - "version": "2.6.0", + "version": "2.6.1", "description": "a GUI for creating nonlinear stories", "author": "Chris Klimas ", "license": "GPL-3.0", @@ -54,6 +54,7 @@ "use-error-boundary": "^2.0.6" }, "devDependencies": { + "@electron/notarize": "^1.2.3", "@playwright/test": "^1.27.1", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", @@ -86,6 +87,7 @@ "cross-var": "^1.1.0", "electron": "^18.3.6", "electron-builder": "^23.3.3", + "electron-builder-notarize": "^1.5.0", "faker": "^5.4.0", "history": "^5.1.0", "jest-axe": "^4.1.0", diff --git a/public/locales/de.json b/public/locales/de.json index 3489dc514..aa1f7b0fb 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -417,7 +417,6 @@ "deletePassages": "Lösche Abschnitte", "movePassage": "Bewege Abschnitt", "movePassages": "Bewege Abschnitte", - "imortTag": "Entferne Tag", "renamePassage": "Ändere Abschnittsname", "removeTag": "Tag entfernen", "renameTag": "Tag umbenennen", diff --git a/public/locales/en-US.json b/public/locales/en-US.json index 35e7b31cf..eade1fa11 100644 --- a/public/locales/en-US.json +++ b/public/locales/en-US.json @@ -416,7 +416,6 @@ "deletePassages": "Delete Passages", "movePassage": "Move Passage", "movePassages": "Move Passages", - "imortTag": "Remove Tag", "renamePassage": "Rename Passage", "removeTag": "Remove Tag", "renameTag": "Rename Tag", diff --git a/public/locales/fr.json b/public/locales/fr.json index ef5c5cf1c..1df76f9ca 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -418,7 +418,6 @@ "deletePassages": "Supprimer les Passages", "movePassage": "Déplacer le Passage", "movePassages": "Déplacer les Passages", - "imortTag": "Supprimer la balise", "renamePassage": "Renommer le Passage", "removeTag": "Supprimer la balise", "renameTag": "Renommer la balise", diff --git a/public/locales/nl.json b/public/locales/nl.json index bd87edc0e..5c43bdc3e 100644 --- a/public/locales/nl.json +++ b/public/locales/nl.json @@ -1,113 +1,418 @@ { - "colors": {}, + "colors": { + "none": "Geen kleur", + "red": "Rood", + "orange": "Oranje", + "yellow": "Geel", + "green": "Groen", + "blue": "Blauw", + "purple": "Paars" + }, "common": { - "add": "Voeg toe", + "add": "Toevoegen", "appName": "Twine", + "back": "Terug", + "build": "Bouwen", "cancel": "Annuleren", + "close": "Sluiten", + "color": "Kleur", + "create": "Maken", + "custom": "Aangepast", "delete": "Verwijderen", - "duplicate": "Dupliceer", + "deleteCount": "Verwijderen ({{count}})", + "details": "Details", + "duplicate": "Duplicaten", "edit": "Bewerken", + "editCount": "Bewerken ({{count}})", + "help": "Help", + "import": "Importeren", + "maximize": "Maximaliseren", + "more": "Meer", + "new": "Nieuw", + "next": "Volgende", "ok": "Oke", - "play": "Speel", - "rename": "Hernoem", - "remove": "Verwijder", + "passage": "Passage", + "play": "Spelen", + "preferences": "Voorkeuren", + "publishToFile": "Publiceren naar bestand", + "redo": "Opnieuw doen", + "redoChange": "Opnieuw doen {{change}}", + "rename": "Hernoemen", + "renamePrompt": "Wat moet “{{name}}” naar hernoemt worden?", + "remove": "Verwijderen", + "selectAll": "Selecteer Alles", "skip": "Overslaan", + "story": "Verhaal", "storyFormat": "Verhaal formaat", "tag": "Label", "test": "Test", - "undo": "Ongedaan maken" + "twine": "Twine", + "undo": "Ongedaan maken", + "undoChange": "Maak {{change}} ongedaan", + "unmaximize": "Herstel grote", + "view": "Bekijken" }, "components": { - "addStoryFormatButton": { - "prompt": "Vul onder een adres in om een verhaal formaat toe te voegen." + "addTagButton": { + "alreadyAdded": "Dit label was al toegevoegd.", + "addLabel": "Voeg label toe", + "invalidName": "Vul een valide label naam in.", + "newTag": "Nieuw label", + "tagColorLabel": "Label kleur", + "tagNameLabel": "Label naam" + }, + "dialogCard": { + "contentsCrashed": "Er ging iets mis met dit dialoog. Probeer het te sluiten en opnieuw te openen." + }, + "fontSelect": { + "customScaleDetail": "Vul alstublieft alleen een percentage in.", + "customFamilyDetail": "Vul alstublieft alleen de naam van het lettertype in.", + "familyEmpty": "Vul een lettertype in.", + "font": "Lettertype", + "fonts": { + "monospaced": "Monospaced", + "serif": "Serif", + "system": "System" + }, + "fontSize": "Lettertype Grote", + "percentage": "{{percent}}%", + "percentageIsntNumber": "Vul alstublieft een nummer in.", + "percentageNotPositive": "Vul alstublieft een nummer groter dan 0 in." + }, + "indentButtons": { + "indent": "Inspringen", + "unindent": "Niet inspringen" + }, + "localStorageQuota": { + "measureAgain": "Meet opnieuw de opslag over", + "percentAvailable": "{percent}% opslag over" }, - "addTagButton": {}, - "fontSelect": {"fonts": {}}, - "indentButtons": {}, - "localStorageQuota": {}, "passageCard": { - "placeholderClick": "Klik twee keer op deze passage om het aan te passen.", - "placeholderTouch": "Druk op deze passage en dan het potlood icoon om het aan te passen." - }, - "renamePassageButton": {"emptyName": "Vul a.u.b. een naam in."}, - "renameStoryButton": {"emptyName": "Vul a.u.b. een naam in."}, - "safariWarningCard": {}, - "storyCard": {}, - "storyFormatCard": {}, - "storyFormatSelect": {}, - "tagEditor": {} + "placeholderClick": "Klik dubbel op deze passage om deze te bewerken.", + "placeholderTouch": "Klik op deze passage, klik dan bewerken in het passage tabblad om hem te bewerken." + }, + "renamePassageButton": { + "emptyName": "Vul alstublieft u naam in.", + "nameAlreadyUsed": "Een andere passage in dit verhaal heeft al deze naam." + }, + "renameStoryButton": { + "emptyName": "Vul alstublieft u naam in.", + "nameAlreadyUsed": "Een ander verhaal heeft deze naam al." + }, + "safariWarningCard": { + "archiveAndUseAnotherBrowser": "Archiveer alstublieft uw verhalen en gebruik een andere browser.", + "addToHomeScreen": "Voeg deze website toe aan uw thuisscherm om deze limitatie te voorkomen.", + "howToAddToHomeScreen": "How voeg ik deze website toe aan mijn thuisscherm?", + "learnMore": "Leer meer", + "message": "De browser die u nu gebruikt verwijderd alle verhalen na zeven dagen geen interactie." + }, + "storageQuota": { + "freeSpace": "{{percent}}% opslag over" + }, + "storyCard": { + "lastUpdated": "Voor het laatst bewerkt op {{date}}", + "passageCount": "1 passage", + "passageCount_plural": "{{count}} passages" + }, + "storyFormatCard": { + "author": "door {{author}}", + "builtIn": "Ingebouwd", + "defaultFormat": "Gebruikt als standaard", + "editorExtensionsDisabled": "Editor-extensies uitgeschakeld", + "license": "Licentie: {{license}}", + "loadingFormat": "Laden van dit formaat...", + "loadError": "Dit formaat kon niet worden geladen ({{errorMessage}}).", + "name": "{{name}} {{version}}", + "proofing": "Proeflezen", + "proofingFormat": "Gebruikt voor proeflezen", + "useEditorExtensions": "Gebruik editor-extensies", + "useFormat": "Gebruik een standaard verhaal formaat", + "useProofingFormat": "Gebruik een proeflees formaat" + }, + "storyFormatSelect": { + "loadingCount": "Laden van 1 verhaal formaat...", + "loadingCount_plural": "Laden van {{loadingCount}} verhaal formaten..." + }, + "tagEditor": { + "alreadyExists": "Een label met deze naam bestaat al." + } }, "dialogs": { "aboutTwine": { - "donateToTwine": "Help Twine te groeien met een donatie", - "codeHeader": "Code" - }, - "appDonation": {"noThanks": "Nee bedankt"}, - "appPrefs": {"language": "Taal"}, - "passageEdit": {}, - "passageTags": {}, - "storyInfo": {"stats": {"title": "Verhaal statistieken"}}, + "donateToTwine": "Help Twine Groeien Met Een Donatie", + "codeHeader": "Code", + "codeRepo": "Bezoek Broncode Repository", + "license": "Deze applicatie is uitgegeven onder de GPL v3 licentie, maar alles gemaakt met deze applicatie mag onder eigen licentie worden uitgegeven, inclusief commerciële.", + "localizationHeader": "Lokalisaties", + "title": "Over Twine {{version}}", + "twineDescription": "Twine is een open-sourcetoepassing voor het vertellen van interactieve, niet-lineaire verhalen." + }, + "appDonation": { + "donate": "Doneer aan Twine Development", + "onlyOnce": "(Dit bericht wordt slechts één keer aan u getoond. Als u in de toekomst wilt doneren aan de ontwikkeling van Twine, vindt u een link om dit te doen in het dialoogvenster Over Twine.)", + "supportMessage": "Als je van Twine houdt, overweeg dan om het te helpen groeien met een donatie. Twine is een open source-project dat altijd gratis te gebruiken is en met jouw hulp zal Twine blijven floreren.", + "noThanks": "Nee Bedankt", + "title": "Steun Twine Development" + }, + "appPrefs": { + "codeEditorFont": "Lettertype Voor Code-editor", + "codeEditorFontScale": "Lettertype Grote Voor Code-editor", + "dialogWidth": "Dialoog Breedte", + "dialogWidths": { + "default": "Standaard", + "wider": "Breder", + "widest": "Breedst" + }, + "editorCursorBlinks": "Knipperende Cursor in Editors", + "fontExplanation": "Het wijzigen van het lettertype hier heeft alleen invloed op de Twine-editor. Het verandert niet het lettertype dat een verhaal gebruikt wanneer het wordt afgespeeld.", + "language": "Taal", + "passageEditorFont": "Lettertype Passage-editor", + "passageEditorFontScale": "Lettertype Grote Passage-editor", + "themeLight": "Licht", + "themeDark": "Donker", + "themeSystem": "System", + "theme": "Thema", + "title": "Voorkeuren" + }, + "passageEdit": { + "editorCrashed": "Er is iets misgegaan met deze editor. Probeer het te sluiten en deze passage opnieuw te bewerken.", + "passageTextEditorLabel": "Passage Text", + "passageTextPlaceholder": "Voer hier de tekst van uw passage in. Zet twee brackets om de naam om naar een andere passage te linken, [[zoals dit bijvoorbeeld]].", + "setAsStart": "Begin Het Verhaal Hier", + "size": "Grote", + "sizeLarge": "Groot", + "sizeSmall": "Klein", + "sizeTall": "Hoog", + "sizeWide": "Breed" + }, + "passageTags": { + "noTags": "Er zijn geen labels toegevoegd aan passages in dit verhaal.", + "title": "Passage Labels" + }, + "storyImport": { + "deselectAll": "Deselecteer Alles", + "filePrompt": "Om verhalen in Twine te importeren, uploadt u hieronder een archief of een gepubliceerd verhaalbestand.", + "importDifferentFile": "Een ander bestand importeren", + "importSelected": "Geselecteerde bestanden importeren", + "importThisStory": "Importeer dit verhaal", + "noStoriesInFile": "Het lijkt erop dat er geen Twine-verhalen in het bestand staan dat u heeft geüpload. Kies een ander bestand.", + "storiesPrompt": "Kies welke verhalen je wilt importeren:", + "title": "Importeer Verhalen", + "willReplaceExisting": "Een verhaal met dezelfde naam in je bibliotheek wordt vervangen." + }, + "storyDetails": { + "storyFormatExplanation": "Wat is een verhaalformaat?", + "snapToGrid": "Uitlijnen op raster", + "stats": { + "brokenLinks": "Gebroken Links", + "characters": "Karakters", + "title": "Verhaal Statistieken", + "ifid": "De IFID van dit verhaal is {{ifid}}.", + "ifidExplanation": "Wat is een IFID?", + "lastUpdate": "Dit verhaal was voor het laatst gewijzigd op {{date}}.", + "links": "Links", + "passages": "Passages", + "words": "Woorden" + } + }, "storyJavaScript": { - "explanation": "JavaScript die u hier invoert zal gelijk uitgevoerd worden als uw verhaal in een Web browser wordt geopend" + "editorLabel": "Verhaal JavaScript", + "title": "Verhaal JavaScript", + "explanation": "JavaScript dat hier wordt ingevoerd, wordt onmiddellijk uitgevoerd wanneer uw verhaal wordt geopend in een webbrowser." }, "storySearch": { - "title": "Zoek en vervang", - "replaceWith": "Verplaatsen met" + "title": "Zoek en Vervang", + "find": "Zoek", + "includePassageNames": "Passagenamen opnemen", + "matchCase": "Match Case", + "matchCount": "{{count}} Bijpassende Passage", + "matchCount_plural": "{{count}} Bijpassende Passage", + "noMatches": "Geen Bijpassende Passage", + "replaceAll": "Vervang In Alle Passages", + "replaceWith": "Vervang Met", + "useRegexes": "Gebruik Regular Expressions" }, "storyStylesheet": { - "explanation": "CSS die hier is ingevoerd zal het standaard uiterlijk van je verhaal overschrijven" + "editorLabel": "Verhaal Stylesheet", + "title": "Verhaal Stylesheet", + "explanation": "CSS die hier wordt ingevoerd, overschrijft de standaardweergave van uw verhaal." }, - "storyTags": {} + "storyTags": { + "noTags": "Er zijn geen labels toegevoegd aan je verhalen.", + "title": "Verhaal Labels" + } }, "electron": { - "errors": {"storyFileChangedExternally": {}}, - "menuBar": {"edit": "Bewerken"}, - "storiesDirectoryName": "Verhalen" + "backupsDirectoryName": "Back-ups", + "errors": { + "jsonSave": "Er is iets misgegaan bij het opslaan van een instellingenbestand.", + "storyFileChangedExternally": { + "message": "Het bestand “{{fileName}}” in je verhalenbibliotheek is buiten Twine gewijzigd.", + "detail": "Als u wijzigingen opslaat, wordt dit bestand overschreven. Als u dit bestand wilt gebruiken in plaats van de versie in Twine, wordt Twine opnieuw gestart en wordt uw werk vervangen door het bestand.", + "overwriteChoice": "Sla Veranderingen Op In Twine", + "relaunchChoice": "Bestand Gebruiken en Opnieuw Starten" + }, + "storyDelete": "Er is iets misgegaan bij het verwijderen van een verhaal.", + "storyRename": "Er is iets misgegaan bij het hernoemen van een verhaal.", + "storySave": "Er is iets misgegaan bij het opslaan van een verhaal." + }, + "menuBar": { + "checkForUpdates": "Controleer op Updates...", + "edit": "Bewerk", + "showDevTools": "Foutopsporingsconsole weergeven", + "showStoryLibrary": "Verhalenbibliotheek weergeven", + "speech": "Spraak", + "troubleshooting": "Probleemoplossen", + "twineHelp": "Twine Help", + "view": "Bekijk" + }, + "storiesDirectoryName": "Verhalen", + "updateCheck": { + "download": "Download", + "error": "Er is iets misgegaan tijdens het zoeken naar een bijgewerkte versie van Twine.", + "updateAvailable": "Er is een nieuwere versie van Twine beschikbaar.", + "upToDate": "Dit is de nieuwste versie van Twine die beschikbaar is." + } }, "routes": { "storyEdit": { + "toolbar": { + "findAndReplace": "Zoek en Vervang", + "javaScript": "JavaScript", + "passageTags": "Passage Labels", + "snapToGrid": "Uitlijnen op raster", + "startStoryHere": "Begin verhaal hier", + "stylesheet": "Stylesheet", + "testFromHere": "Test vanaf hier" + }, "topBar": { - "addPassage": "Passage", - "editJavaScript": "Pas verhaal JavaScript aan", - "editStylesheet": "Pas verhaal StyleSheet aan", - "findAndReplace": "Zoek en vervang", - "proofStory": "Kijk naar controle kopie", - "publishToFile": "Naar bestand publiceren" + "editJavaScript": "Bewerk de JavaScript van dit verhaal", + "editStylesheet": "Bewerk de Stylesheet van dit verhaal", + "findAndReplace": "Zoek en Vervang", + "passageTags": "Edit Passage Labels", + "proofStory": "Bekijk proeflees kopie", + "publishToFile": "Publiceer naar Bestand", + "selectAllPassages": "Selecteer Alle Passages" + }, + "zoomButtons": { + "storyStructure": "Alleen verhaalstructuur weergeven", + "passageNames": "Alleen passagenamen weergeven", + "passageNamesAndExcerpts": "Passagenamen en fragmenten weergeven" } }, "storyFormatList": { - "title": {}, - "storyFormatExplanation": "Verhaal formaten passen het uiterlijk en gedrag van verhalen aan tijdens het spelen." + "noneVisible": "Er zijn geen verhaalformaten die voldoen aan de criteria die je hebt geselecteerd.", + "show": "Laat zien...", + "title": { + "all": "Alle Verhaal Formaten", + "current": "Huidige Verhaal Formaten", + "user": "Door Gebruiker Toegevoegde Formaten" + }, + "toolbar": { + "addStoryFormatButton": { + "addPreview": "{{storyFormatName}} {{storyFormatVersion}} wordt toegevoegd.", + "alreadyAdded": "{{storyFormatName}} {{storyFormatVersion}} is al toegevoegd.", + "fetchError": "Het verhaalformaat op dit adres kan niet worden opgehaald ({{errorMessage}}).", + "invalidUrl": "Voer een geldige URL in.", + "prompt": "Voer het adres hieronder in om een verhaalformaat toe te voegen." + }, + "disableFormatExtensions": "Editor-extensies uitschakelen", + "enableFormatExtensions": "Editor-extensies inschakelen", + "useAsDefaultFormat": "Gebruiken als standaardformaat", + "useAsProofingFormat": "Gebruik om verhalen proef te lezen" + }, + "storyFormatExplanation": "Verhaalformaten bepalen het uiterlijk en het gedrag van verhalen tijdens het spelen." }, - "storyImport": {}, "storyList": { - "noStories": "Er zijn geen opgeslagen verhalen in Twine op dit moment. Om te beginnen kun een nieuw verhaal maken", + "library": "Bibliotheek", + "noStories": "Er zijn momenteel geen verhalen opgeslagen in Twine. Om aan de slag te gaan, kunt u een nieuw verhaal maken of een bestaand verhaal uit een bestand importeren.", + "taggedTitleCount": "1 Getagd Verhaal", + "taggedTitleCount_0": "Geen Getagden Verhalen", + "taggedTitleCount_plural": "{{count}} Getagden Verhalen", + "titleCount": "1 Verhaal", + "titleCount_0": "Geen Verhalen", + "titleCount_plural": "{{count}} Verhalen", "titleGeneric": "Verhalen", - "topBar": { - "about": "Over Twine", + "toolbar": { "archive": "Archief", - "createStory": "Verhaal", - "help": "Help", - "sortName": "Naam", - "storyFormats": "Formaten" + "createStoryButton": { + "prompt": "Hoe moet uw verhaal heten? U kunt dit later wijzigen.", + "emptyName": "Vul asltublieft een naam in.", + "nameConflict": "Een ander verhaal heeft al deze naam." + }, + "deleteStoryButton": { + "warning": { + "electron": "Weet u zeker dat u “{{storyName}}” wilt verwijderen ? Het wordt naar de prullenbak verplaatst.", + "web": "Weet u zeker dat u “{{storyName}}” wilt verwijderen ? Het wordt voor altijd verwijderd. Dit kan niet ongedaan gemaakt worden." + } + }, + "showAllStories": "Toon Alle Verhalen", + "showTags": "Toon Labels", + "sort": "Sorteer Op", + "sortByDate": "Voor het laatst geupdate", + "sortByName": "Naam", + "storyTags": "Verhaal Labels" } }, "welcome": { - "autosaveTitle": "Je werk wordt automatisch opgeslagen.", - "doneTitle": "Dat is het!", - "gotoStoryList": "Ga naar de verhaal lijst", - "greetingTitle": "Hallo!", - "tellMeMore": "Vertel me meer", - "helpTitle": "Ben je hier nieuw?" + "autosave": "

Er is nu een map met de naam Twine in de map Documenten. Daarin bevindt zich een map Verhalen, waar al uw werk wordt opgeslagen. Twine slaat alles op terwijl u werkt, dus u hoeft niet zelf op te slaan. U kunt de map waarin uw verhalen zijn opgeslagen altijd openen door het item Bibliotheek weergeven te gebruiken in de map Twine.

Omdat Twine uw werk op de achtergrond opslaat, worden de bestanden in uw verhalenbibliotheek vergrendeld voor bewerking terwijl Twine gebruikt word.

Als u een Twine-verhaalbestand wilt openen dat u van iemand anders hebt ontvangen, kunt u het in uw bibliotheek importeren met behulp van de Importeren uit bestand link in de verhalenlijst.

", + "autosaveTitle": "Uw werk wordt automatisch opgeslagen.", + "browserStorage": "

Dit betekent dat u geen account hoeft aan te maken om Twine 2 te gebruiken, en alles wat u aanmaakt wordt niet ergens anders op een server opgeslagen— het blijft alleen in uw browser staan.

Twee zeer belangrijke dingen om te onthouden. Aangezien uw werk alleen in uw browser wordt opgeslagen, verliest u uw werk als u de browser gegevens wist! Niet erg handig. Vergeet niet om de knop Archief vaak te gebruiken. U kunt ook afzonderlijke verhalen naar een bestand publiceren met behulp van het menu dat in elk verhaal te vindin is. Zowel archief- als verhaalbestanden kunnen altijd opnieuw in Twine worden geïmporteerd.

Ten tweede kan iedereen die deze browser kan gebruiken, uw werk zien en wijzigingen aanbrengen. Dus als een kind heeft dat per ongeluk iets kan breken, kunt u overwegen om een eigen systeem profiel te maken.

", + "browserStorageTitle": "Uw werk wordt alleen in uw browser opgeslagen", + "done": "

Bedankt voor het lezen en veel plezier met Twine.

", + "doneTitle": "Dat was alles!", + "gotoStoryList": "Ga Naar Verhalen Lijst", + "greeting": "

Twine is een open-source tool voor het vertellen van interactieve, niet-lineaire verhalen. Er zijn een paar dingen die u moet weten voordat u aan de slag gaat.

", + "greetingTitle": "Hoi!", + "tellMeMore": "Vertel Mij Meer", + "help": "

Als u Twine nog nooit eerder hebt gebruikt, welkom! Het Twine Cookbook is een geweldig hulpmiddel om Twine te leren gebruiken. Als u Twine nog niet eerder hebt gebruikt, is dit een geweldige plek om te beginnen.

", + "helpTitle": "Nieuw hier?" + } + }, + "routeActions": { + "app": { + "aboutApp": "Over Twine", + "preferences": "Voorkeuren", + "reportBug": "Rapporteer een fout", + "storyFormats": "Verhaal Formaten" + }, + "build": { + "play": "Spelen", + "proof": "Proeflezen", + "publishToFile": "Publiceer naar Bestand", + "test": "Test" } }, "store": { - "errors": {}, + "archiveFilename": "{{timestamp}} Twine Archive.html", + "errors": { + "cantPersistPrefs": "Er is iets misgegaan bij het opslaan van uw voorkeuren ({{error}}).", + "cantPersistStories": "Er is iets misgegaan bij het opslaan van uw verhalen ({{error}}).", + "cantPersistStoryFormats": "Er is iets misgegaan bij het opslaan van uw verhaalformaten ({{error}}).", + "electronRemediation": "Het kan helpen om deze applicatie opnieuw te starten.", + "webRemediation": "Deze pagina opnieuw laden kan helpen." + }, "passageDefaults": { "name": "Naamloze passage" }, - "storyDefaults": {"name": "Naamloos verhaal"}, - "storyFormatDefaults": {"name": "Naamloos Verhaal Formaat"} + "storyDefaults": { + "name": "Naamloos Verhaal" + }, + "storyFormatDefaults": { + "name": "Naamloos Verhaal Formaat" + } }, - "undoChange": {"replaceAllText": "Alles vervangen"} -} + "undoChange": { + "addTag": "Voeg Label Toe", + "changeTagColor": "Verander Label Kleur", + "newPassage": "Nieuwe Passage", + "deletePassage": "Verwijder Passage", + "deletePassages": "Verwijder Passages", + "movePassage": "Verplaats Passage", + "movePassages": "Verplaats Passages", + "renamePassage": "Hernoem Passage", + "removeTag": "Verwijder Label", + "renameTag": "Hernoem Label", + "replaceAllText": "Vervang alles" + } +} \ No newline at end of file diff --git a/public/locales/pt-PT.json b/public/locales/pt-PT.json index 72726d247..23af070f0 100644 --- a/public/locales/pt-PT.json +++ b/public/locales/pt-PT.json @@ -402,7 +402,6 @@ "deletePassages": "Apagar as Passagens", "movePassage": "Mover a Passagem", "movePassages": "Mover as Passagens", - "imortTag": "Remover a Etiqueta", "renamePassage": "Mudar o Título da Passagem", "removeTag": "Remover a Etiqueta", "renameTag": "Mudar o Nome da Etiqueta", diff --git a/public/locales/tr.json b/public/locales/tr.json index 8ec4e8c65..b9c1bf883 100644 --- a/public/locales/tr.json +++ b/public/locales/tr.json @@ -418,7 +418,6 @@ "deletePassages": "Bölümleri Silme", "movePassage": "Bölüm Taşıma", "movePassages": "Bölümleri Taşıma", - "imortTag": "Etiket Kaldırma", "renamePassage": "Bölüm Yeniden Adlandırma", "removeTag": "Etiketi Kaldırma", "renameTag": "Etiketi Yeniden Adlandırma", diff --git a/public/locales/uk.json b/public/locales/uk.json index 4af45cfa2..210292ff4 100644 --- a/public/locales/uk.json +++ b/public/locales/uk.json @@ -415,7 +415,6 @@ "deletePassages": "видалення параграфів", "movePassage": "переміщення параграфа", "movePassages": "переміщення параграфів", - "imortTag": "видалення мітки", "renamePassage": "перейменування параграфа", "removeTag": "видалення мітки", "renameTag": "перейменування мітки", diff --git a/public/locales/zh-CN.json b/public/locales/zh-CN.json index d4508f661..4d3e581f4 100644 --- a/public/locales/zh-CN.json +++ b/public/locales/zh-CN.json @@ -416,7 +416,6 @@ "deletePassages": "删除片段", "movePassage": "移动片段", "movePassages": "移动片段", - "imortTag": "移除标签", "renamePassage": "重命名片段", "removeTag": "移除标签", "renameTag": "重命名标签", diff --git a/src/components/fuzzy-finder/fuzzy-finder.css b/src/components/fuzzy-finder/fuzzy-finder.css index 19a69e2d5..5939e3c73 100644 --- a/src/components/fuzzy-finder/fuzzy-finder.css +++ b/src/components/fuzzy-finder/fuzzy-finder.css @@ -32,6 +32,10 @@ padding: 0; } +.fuzzy-finder .results ol button { + width: 100%; +} + .fuzzy-finder .search + .results.has-results { margin-top: calc(-1 * var(--control-inner-padding)); } diff --git a/src/components/passage/__tests__/passage-card.test.tsx b/src/components/passage/__tests__/passage-card.test.tsx index e5d3a7b87..869ca94e6 100644 --- a/src/components/passage/__tests__/passage-card.test.tsx +++ b/src/components/passage/__tests__/passage-card.test.tsx @@ -3,13 +3,18 @@ import * as detectIt from 'detect-it'; import {axe} from 'jest-axe'; import * as React from 'react'; import {fakePassage} from '../../../test-util'; +import {passageIsEmpty} from '../../../util/passage-is-empty'; import {PassageCard, PassageCardProps} from '../passage-card'; jest.mock('../../tag/tag-stripe'); +jest.mock('../../../util/passage-is-empty'); describe('', () => { + const passageIsEmptyMock = passageIsEmpty as jest.Mock; const oldDeviceType = detectIt.deviceType; + beforeEach(() => passageIsEmptyMock.mockReturnValue(false)); + afterAll(() => { (detectIt as any).deviceType = oldDeviceType; }); @@ -41,14 +46,23 @@ describe('', () => { expect(screen.getByText(passage.text)).toBeInTheDocument(); }); + it("gives it an 'empty' CSS class if the passage is empty", () => { + passageIsEmptyMock.mockReturnValue(true); + renderComponent({passage: fakePassage()}); + expect(document.querySelector('.passage-card.empty')).toBeInTheDocument(); + }); + + it("doesn't give it an 'empty' CSS class if the passage is empty", () => { + passageIsEmptyMock.mockReturnValue(false); + renderComponent({passage: fakePassage()}); + expect( + document.querySelector('.passage-card.empty') + ).not.toBeInTheDocument(); + }); + describe('when passage text is empty', () => { const passage = fakePassage({text: ''}); - it("gives it an 'empty' CSS class", () => { - renderComponent({passage}); - expect(document.querySelector('.passage-card.empty')).toBeInTheDocument(); - }); - it('displays a touch-oriented placeholder message on a touch device', () => { (detectIt as any).deviceType = 'touchOnly'; renderComponent({passage}); diff --git a/src/components/passage/passage-card.tsx b/src/components/passage/passage-card.tsx index 26893451f..b2809c15a 100644 --- a/src/components/passage/passage-card.tsx +++ b/src/components/passage/passage-card.tsx @@ -3,10 +3,11 @@ import {deviceType} from 'detect-it'; import * as React from 'react'; import {DraggableCore, DraggableCoreProps} from 'react-draggable'; import {useTranslation} from 'react-i18next'; -import {Passage, TagColors} from '../../store/stories'; import {CardContent} from '../container/card'; import {SelectableCard} from '../container/card/selectable-card'; +import {Passage, TagColors} from '../../store/stories'; import {TagStripe} from '../tag/tag-stripe'; +import {passageIsEmpty} from '../../util/passage-is-empty'; import './passage-card.css'; export interface PassageCardProps { @@ -38,10 +39,10 @@ export const PassageCard: React.FC = React.memo(props => { const className = React.useMemo( () => classNames('passage-card', { - empty: passage.text === '' && passage.tags.length === 0, + empty: passageIsEmpty(passage), selected: passage.selected }), - [passage.selected, passage.tags.length, passage.text] + [passage] ); const container = React.useRef(null); const excerpt = React.useMemo(() => { diff --git a/src/dialogs/about-twine/credits.json b/src/dialogs/about-twine/credits.json index 5506c80bb..6441a5a0d 100644 --- a/src/dialogs/about-twine/credits.json +++ b/src/dialogs/about-twine/credits.json @@ -19,7 +19,7 @@ "Julian Brehmer, Moritz Rebbert (Deutsch)", "Valentin Rocher (Français)", "Marco Secchi (Italiano)", - "Stefan Peeters (Nederlands)", + "SjoerdHekking, Stefan Peeters (Nederlands)", "José Carlos Dias (Português)", "Alliah (Português Brasileiro)", "Anton Zhuchkov (Русский)", diff --git a/src/dialogs/story-search.tsx b/src/dialogs/story-search.tsx index 10a9f769d..b9a22acfc 100644 --- a/src/dialogs/story-search.tsx +++ b/src/dialogs/story-search.tsx @@ -29,7 +29,7 @@ export interface StorySearchDialogProps extends DialogComponentProps { } export const StorySearchDialog: React.FC = props => { - const {storyId, ...other} = props; + const {storyId, onClose, ...other} = props; const [flags, setFlags] = React.useState({ includePassageNames: true, matchCase: false, @@ -37,22 +37,43 @@ export const StorySearchDialog: React.FC = props => { }); const [replace, setReplace] = React.useState(''); const [find, setFind] = React.useState(''); - const {dispatch, stories} = useUndoableStoriesContext(); - const debouncedDispatch = React.useMemo( - () => debounce(dispatch, 250), - [dispatch] + const [debouncedFind, setDebouncedFind] = React.useState(''); + const closingRef = React.useRef(false); + const updateDebouncedFind = React.useMemo( + () => + debounce( + (value: string) => { + setDebouncedFind(value); + }, + 250, + {leading: false, trailing: true} + ), + [] ); + const {dispatch, stories} = useUndoableStoriesContext(); const {t} = useTranslation(); - const story = storyWithId(stories, storyId); const matches = passagesMatchingSearch(story.passages, find, flags); React.useEffect(() => { - debouncedDispatch(highlightPassagesMatchingSearch(story, find, flags)); + // If we are in the process of closing, don't dispatch any highlight + // changes. We don't want to overwrite the dispatch that occurs in + // handleClose. - return () => - debouncedDispatch(highlightPassagesMatchingSearch(story, '', {})); - }, [debouncedDispatch, find, flags, story]); + if (!closingRef.current) { + dispatch(highlightPassagesMatchingSearch(story, debouncedFind, flags)); + } + + // This doesn't return a cleanup function--cleanup occurs in handleClose + // instead. This is safe because we know this effect will only ever change + // highlight status of passages. + }, [debouncedFind, dispatch, flags, story]); + + function handleClose() { + closingRef.current = true; + dispatch(highlightPassagesMatchingSearch(story, '', {})); + onClose(); + } function handleReplaceWithChange( editor: CodeMirror.Editor, @@ -68,6 +89,7 @@ export const StorySearchDialog: React.FC = props => { text: string ) { setFind(text); + updateDebouncedFind(text); } function handleReplace() { @@ -87,6 +109,7 @@ export const StorySearchDialog: React.FC = props => { className="story-search-dialog" fixedSize headerLabel={t('dialogs.storySearch.title')} + onClose={handleClose} >
', () => { ); } - it('overrides the selected state of passages while the user is dragging', async () => { + it('overrides the selected state of passages while the user is dragging', () => { const passages = [ fakePassage({ selected: false, @@ -72,17 +72,15 @@ describe('', () => { screen.getByTestId(`mock-passage-${passages[1].id}`).dataset.selected ).toBe('false'); fireEvent.click(screen.getByText('onTemporarySelectRect')); - await waitFor(() => - expect( - screen.getByTestId(`mock-passage-${passages[0].id}`).dataset.selected - ).toBe('true') - ); + expect( + screen.getByTestId(`mock-passage-${passages[0].id}`).dataset.selected + ).toBe('true'); expect( screen.getByTestId(`mock-passage-${passages[1].id}`).dataset.selected ).toBe('false'); }); - it('overrides the selected state of passages while the user is dragging additively', async () => { + it('overrides the selected state of passages while the user is dragging additively', () => { const passages = [ fakePassage({ height: 100, @@ -108,11 +106,9 @@ describe('', () => { screen.getByTestId(`mock-passage-${passages[1].id}`).dataset.selected ).toBe('true'); fireEvent.click(screen.getByText('onTemporarySelectRect additive')); - await waitFor(() => - expect( - screen.getByTestId(`mock-passage-${passages[0].id}`).dataset.selected - ).toBe('true') - ); + expect( + screen.getByTestId(`mock-passage-${passages[0].id}`).dataset.selected + ).toBe('true'); expect( screen.getByTestId(`mock-passage-${passages[1].id}`).dataset.selected ).toBe('true'); diff --git a/src/routes/story-edit/marqueeable-passage-map.tsx b/src/routes/story-edit/marqueeable-passage-map.tsx index 999b8c221..78395d2c0 100644 --- a/src/routes/story-edit/marqueeable-passage-map.tsx +++ b/src/routes/story-edit/marqueeable-passage-map.tsx @@ -73,7 +73,7 @@ export const MarqueeablePassageMap: React.FC< <> diff --git a/src/store/stories/action-creators/highlight-passages.ts b/src/store/stories/action-creators/highlight-passages.ts index 1dccccbd6..5e5db3834 100644 --- a/src/store/stories/action-creators/highlight-passages.ts +++ b/src/store/stories/action-creators/highlight-passages.ts @@ -32,14 +32,14 @@ export function highlightPassagesMatchingSearch( flags ).map(passage => passage.id); - story.passages.forEach(passage => { + for (const passage of story.passages) { const oldHighlighted = passage.highlighted; const newHighlighted = matchIds.includes(passage.id); if (newHighlighted !== oldHighlighted) { passageUpdates[passage.id] = {highlighted: newHighlighted}; } - }); + } } if (Object.keys(passageUpdates).length > 0) { dispatch({ diff --git a/src/util/__tests__/twee.test.ts b/src/util/__tests__/twee.test.ts index eb19969b3..ddd97e0d3 100644 --- a/src/util/__tests__/twee.test.ts +++ b/src/util/__tests__/twee.test.ts @@ -102,6 +102,18 @@ describe('passageFromTwee()', () => { ); }); + it('converts a passage with escaped leading spaces properly', () => { + expect(passageFromTwee(':: \\ leading space')).toEqual( + passageObject({name: ' leading space'}) + ); + }); + + it('converts a passage with trailing leading spaces properly', () => { + expect(passageFromTwee(':: trailing space\\ ')).toEqual( + passageObject({name: 'trailing space '}) + ); + }); + it('converts a passage with escaped tags properly', () => { const passage = fakePassage({tags: ['\\', '[]', '{}']}); diff --git a/src/util/twee.ts b/src/util/twee.ts index 201e55851..44f75e735 100644 --- a/src/util/twee.ts +++ b/src/util/twee.ts @@ -50,10 +50,13 @@ export function passageToTwee(passage: Passage) { export function passageFromTwee(source: string): Omit { const [headerLine, ...lines] = source.split(linebreakRegExp); - // The first line should be the header, with name, tags, and metadata. - // Roughly translating this regexp: - // ::, whitespace, name, whitespace, [tags]?, whitespace, {metadata}?, whitespace - const headerBits = /^::\s*(.*?)\s*(\[.*?\])?\s*(\{.*?\})?\s*$/.exec( + // The first line should be the header, with name, tags, and metadata. Name + // needs to capture a trailing `\ `. Repeated trailing spaces should get + // captured by the main group, since they include non-whitespace. + // + // Roughly translating this regexp: ::, whitespace, name, whitespace, [tags]?, + // whitespace, {metadata}?, whitespace + const headerBits = /^::\s*(.*?(?:\\\s)?)\s*(\[.*?\])?\s*(\{.*?\})?\s*$/.exec( headerLine ); @@ -72,7 +75,11 @@ export function passageFromTwee(source: string): Omit { const passage: Omit = { ...passageDefaults(), id: uuid(), - name: unescapeForTweeHeader(rawName.trim()), + name: unescapeForTweeHeader( + rawName + .replace(/^(\\\s)+/g, match => ' '.repeat(match.length / 2)) + .replace(/(\\\s)+$/g, match => ' '.repeat(match.length / 2)) + ), tags: [], text: lines.map(unescapeForTweeText).join('\n').trim() };