diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 0bf2f73d4..838f2e63f 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -92,5 +92,7 @@ body: required: false - label: I have done a search and believe that an issue does not already exist for this bug in the GitHub repository. required: true + - label: If this problem is occurring with the app version of Twine, it still happens after I remove the `user.css` file from my Twine folder. (You would've added this file yourself. If you don't understand this question, you can safely check this box.) + required: true - label: I have read and agree to abide by this project's Code of Conduct. required: true diff --git a/docs/en/src/SUMMARY.md b/docs/en/src/SUMMARY.md index 29d1c774a..5d4ddcc8e 100644 --- a/docs/en/src/SUMMARY.md +++ b/docs/en/src/SUMMARY.md @@ -42,7 +42,9 @@ - [Removing a Story Format](story-formats/removing.md) - [Disabling Story Format Extensions](story-formats/extensions.md) - [How Twine Manages Story Format Versions](story-formats/versions.md) -- [Customing Twine With Preferences](preferences/index.md) +- [Customizing Twine](customizing/index.md) + - [Setting Preferences](customizing/preferences.md) + - [Advanced Customization](customizing/advanced.md) - [Limitations](limitations/index.md) - [Large Stories](limitations/large-stories.md) - [Combining Stories](limitations/combining.md) diff --git a/docs/en/src/customizing/advanced.md b/docs/en/src/customizing/advanced.md new file mode 100644 index 000000000..2620a3060 --- /dev/null +++ b/docs/en/src/customizing/advanced.md @@ -0,0 +1,56 @@ +# Advanced Customization + +## CSS + +App Twine allows advanced customization its interface by creating a special +file, `user.css`, in your Twine folder. You'll need to create this file outside +of Twine using a text editor. + +If `user.css` exists, then app Twine will add the contents of it to the UI as +CSS rules, potentially overriding the default styling. CSS allows changing the +appearance of the application--using different fonts, for example, or +colors--but does not allow changing Twine's functionality or adding new +features. + +Here's a sample `user.css` that replaces the graph paper background of the story +map with a plain gray color: + +``` +.passage-map { + background: hsl(0, 0%, 75%) !important; +} + +[data-app-theme="dark"] .passage-map { + background: hsl(0, 0%, 30%) !important; +} +``` + +`user.css` is only available in app Twine. If you'd like to customize browser +Twine using CSS, browser extensions like +[Stylus](https://github.com/openstyles/stylus/wiki) might help. + +Some important things to keep in mind working with `user.css`: + +- **The structure of Twine's UI can and will change on every release, even for + patch-level version changes.** Because these changes are often numerous, they + will not be part of release notes. +- The file must named exactly `user.css`--all lowercase. `User.css` will not + work. +- Changes to `user.css` will take effect the next time you start Twine. +- To determine what [CSS + selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) to + use, you can either use developer tools in browser Twine--the DOM structure is + identical between browser Twine and app Twine--or open developer tools in + Twine itself by going to _Troubleshooting_ under the _Help_ menu, then + choosing _Show Debug Console_. +- You can use the in-app debug console to test your CSS rules. The rules you set + in `user.css` will be listed as _injected stylesheet_ in the developer + console. +- `user.css` must be at the same folder level as your `Stories` and `Backups` + folders, directly below the `Twine` folder. +- If there's a problem loading `user.css`, Twine will load as normal and not + apply any customizations. If any of your CSS rules are incorrectly written, + they will be ignored. Twine will not show a warning in any of these cases. +- In order for `user.css` to [win + specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity), + you might need to add `!important` to the end of your declarations. diff --git a/docs/en/src/customizing/index.md b/docs/en/src/customizing/index.md new file mode 100644 index 000000000..dd5b98a25 --- /dev/null +++ b/docs/en/src/customizing/index.md @@ -0,0 +1,3 @@ +# Customizing Twine + +This section describes how you can customize Twine to fit your needs. \ No newline at end of file diff --git a/docs/en/src/preferences/index.md b/docs/en/src/customizing/preferences.md similarity index 86% rename from docs/en/src/preferences/index.md rename to docs/en/src/customizing/preferences.md index 9695de0b0..beb929f35 100644 --- a/docs/en/src/preferences/index.md +++ b/docs/en/src/customizing/preferences.md @@ -1,9 +1,9 @@ -# Customing Twine With Preferences +# Setting Preferences -To customize Twine, choose _Preferences_ from the _Twine_ top toolbar tab. This -tab is available throughout Twine. A dialog will appear that lets you change -settings. These changes will take effect as soon as you make them, and Twine -will remember them between sessions. +To customize Twine's preferences, choose _Preferences_ from the _Twine_ top +toolbar tab. This tab is available throughout Twine. A dialog will appear that +lets you change settings. These changes will take effect as soon as you make +them, and Twine will remember them between sessions. ## Changing Twine's Language diff --git a/public/locales/en-US.json b/public/locales/en-US.json index 9c359cec2..8387e4033 100644 --- a/public/locales/en-US.json +++ b/public/locales/en-US.json @@ -276,6 +276,9 @@ "error": "Something went wrong while checking for an updated version of Twine.", "updateAvailable": "A newer version of Twine is available.", "upToDate": "This is the newest version of Twine available." + }, + "userCss": { + "filename": "user.css" } }, "routes": { diff --git a/src/electron/main-process/__tests__/init-app.test.ts b/src/electron/main-process/__tests__/init-app.test.ts index dc344b8d0..7d527ad0b 100644 --- a/src/electron/main-process/__tests__/init-app.test.ts +++ b/src/electron/main-process/__tests__/init-app.test.ts @@ -55,6 +55,7 @@ describe('initApp', () => { }); it.todo('creates the main window'); + it.todo('injects user CSS into the main window if available'); it('does not show an error dialog when everything loads', async () => { await initApp(); diff --git a/src/electron/main-process/__tests__/user-css.test.ts b/src/electron/main-process/__tests__/user-css.test.ts new file mode 100644 index 000000000..f54326abe --- /dev/null +++ b/src/electron/main-process/__tests__/user-css.test.ts @@ -0,0 +1,31 @@ +import {readFile} from 'fs-extra'; +import {getUserCss} from '../user-css'; + +jest.mock('fs-extra'); + +describe('getUserCss', () => { + const readFileMock = readFile as jest.Mock; + + it("returns the contents of user.css in the user's Twine directory", async () => { + readFileMock.mockReturnValue('mock-css'); + + expect(await getUserCss()).toBe('mock-css'); + expect(readFileMock.mock.calls).toEqual([ + [ + 'mock-electron-app-path-documents/common.appName/electron.userCss.filename', + 'utf8' + ] + ]); + }); + + it('returns undefined and if the file could not be read', async () => { + const warnSpy = jest + .spyOn(global.console, 'warn') + .mockImplementation(() => {}); + + readFileMock.mockImplementation(() => Promise.reject(new Error())); + + expect(await getUserCss()).toBeUndefined(); + expect(warnSpy).toBeCalledTimes(1); + }); +}); diff --git a/src/electron/main-process/init-app.ts b/src/electron/main-process/init-app.ts index 3247b1cd7..c129b943f 100644 --- a/src/electron/main-process/init-app.ts +++ b/src/electron/main-process/init-app.ts @@ -4,6 +4,7 @@ import {initIpc} from './ipc'; import {initLocales} from './locales'; import {initMenuBar} from './menu-bar'; import {backupStoryDirectory, createStoryDirectory} from './story-directory'; +import {getUserCss} from './user-css'; let mainWindow: BrowserWindow | null; @@ -28,7 +29,14 @@ async function createWindow() { `file://${path.resolve(__dirname, '../../../../renderer/index.html')}` ); - mainWindow.once('ready-to-show', () => { + mainWindow.once('ready-to-show', async () => { + const userCss = await getUserCss(); + + if (userCss) { + console.log('Adding user CSS'); + mainWindow!.webContents.insertCSS(userCss); + } + mainWindow!.show(); if (!app.isPackaged) { diff --git a/src/electron/main-process/user-css.ts b/src/electron/main-process/user-css.ts new file mode 100644 index 000000000..a596669e2 --- /dev/null +++ b/src/electron/main-process/user-css.ts @@ -0,0 +1,21 @@ +import {app} from 'electron'; +import {readFile} from 'fs-extra'; +import {join} from 'path'; +import {i18n} from './locales'; + +export async function getUserCss(): Promise { + try { + return await readFile( + join( + app.getPath('documents'), + i18n.t('common.appName'), + i18n.t('electron.userCss.filename') + ), + 'utf8' + ); + } catch (error) { + console.warn( + `Error while loading user CSS, skipping: ${(error as Error).message}` + ); + } +}