├── .editorconfig ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .windsurf └── rules │ └── translations.md ├── LICENSE ├── README.md ├── eslint.config.mjs ├── next.config.ts ├── package-lock.json ├── package.json ├── public ├── dumps │ ├── 00001.bin │ ├── 00002.bin │ ├── 00003.bin │ ├── 00004.bin │ ├── clear │ └── list ├── env-pages.json └── env.json ├── runme.sh ├── scripts ├── compareLangFiles.ts ├── copyAndGZ.mjs ├── generateWebManifest.ts ├── replaceInFiles.mjs └── shortenStatic.mjs ├── src ├── app │ ├── (pages) │ │ ├── add-plugin │ │ │ └── page.tsx │ │ ├── frames │ │ │ └── page.tsx │ │ ├── gallery │ │ │ └── page.tsx │ │ ├── globals.scss │ │ ├── import │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── palettes │ │ │ └── page.tsx │ │ ├── settings │ │ │ ├── dropbox │ │ │ │ └── page.tsx │ │ │ ├── generic │ │ │ │ └── page.tsx │ │ │ ├── git │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ ├── plugins │ │ │ │ └── page.tsx │ │ │ └── wifi │ │ │ │ └── page.tsx │ │ └── webusb │ │ │ └── page.tsx │ ├── (remote) │ │ ├── db │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── remote.scss │ │ └── remote │ │ │ └── page.tsx │ ├── favicon.ico │ └── layout.tsx ├── assets │ └── images │ │ ├── favicon.png │ │ ├── favicon.svg │ │ └── greys.png ├── components │ ├── AddPlugin │ │ └── index.tsx │ ├── BatchButtons │ │ └── index.tsx │ ├── ColorPicker │ │ └── index.tsx │ ├── ColorSlider │ │ └── index.tsx │ ├── ConnectPrinter │ │ └── index.tsx │ ├── CopyDatabase │ │ ├── index.tsx │ │ └── styles.scss │ ├── Debug │ │ └── index.tsx │ ├── EditFrameStartLine │ │ ├── index.tsx │ │ └── posTiles.ts │ ├── EditImageTabs │ │ └── index.tsx │ ├── EnvInfo │ │ └── index.tsx │ ├── Errors │ │ ├── ErrorMessage.tsx │ │ └── index.tsx │ ├── ExportSettings │ │ └── index.tsx │ ├── FolderBreadcrumb │ │ └── index.tsx │ ├── FolderTreeDialog │ │ └── index.tsx │ ├── Frame │ │ └── index.tsx │ ├── FrameContextMenu │ │ └── index.tsx │ ├── FrameSelect │ │ └── index.tsx │ ├── Frames │ │ └── index.tsx │ ├── Gallery │ │ └── index.tsx │ ├── GalleryGrid │ │ └── index.tsx │ ├── GalleryGridItem │ │ └── index.tsx │ ├── GalleryGroup │ │ └── index.tsx │ ├── GalleryGroupContextMenu │ │ └── index.tsx │ ├── GalleryHeader │ │ └── index.tsx │ ├── GalleryImage │ │ └── index.tsx │ ├── GalleryImageContextMenu │ │ └── index.tsx │ ├── GalleryNumbers │ │ └── index.tsx │ ├── GalleryViewSelect │ │ └── index.tsx │ ├── GameBoyImage │ │ └── index.tsx │ ├── GlobalAppInit │ │ └── index.tsx │ ├── GreySelect │ │ └── index.tsx │ ├── ImageLoading │ │ └── index.tsx │ ├── ImageMeta │ │ └── index.tsx │ ├── ImageRender │ │ └── index.tsx │ ├── Import │ │ ├── dummy.ts │ │ ├── index.tsx │ │ └── useImport.ts │ ├── ImportPreviewImage │ │ └── index.tsx │ ├── Lightbox │ │ └── index.tsx │ ├── MarkdownStack │ │ └── index.tsx │ ├── MetaTable │ │ └── index.tsx │ ├── MuiCleanThemeProvider │ │ └── index.tsx │ ├── Navigation │ │ ├── Skeleton.tsx │ │ └── index.tsx │ ├── Overlays │ │ ├── BitmapQueue │ │ │ └── index.tsx │ │ ├── Confirm │ │ │ └── index.tsx │ │ ├── ConnectSerial │ │ │ └── index.tsx │ │ ├── DownloadOptions │ │ │ ├── DownloadOptionsForm.tsx │ │ │ └── index.tsx │ │ ├── DragOver │ │ │ └── index.tsx │ │ ├── EditForm │ │ │ └── index.tsx │ │ ├── EditFrame │ │ │ ├── EditFrameForm.tsx │ │ │ └── index.tsx │ │ ├── EditImageGroup │ │ │ └── index.tsx │ │ ├── EditPalette │ │ │ └── index.tsx │ │ ├── EditRGBN │ │ │ ├── RGBNPreviewImage.tsx │ │ │ ├── Select.tsx │ │ │ └── index.tsx │ │ ├── FilterForm │ │ │ ├── FilterFormFrame.tsx │ │ │ ├── FilterFormPalette.tsx │ │ │ ├── FilterFormTab.tsx │ │ │ ├── FilterFormTag.tsx │ │ │ └── index.tsx │ │ ├── FrameQueue │ │ │ └── index.tsx │ │ ├── ImportQueue │ │ │ ├── ImportRow.tsx │ │ │ └── index.tsx │ │ ├── LightboxImages │ │ │ ├── LightBoxImage.tsx │ │ │ └── index.tsx │ │ ├── PickColors │ │ │ └── index.tsx │ │ ├── ProgressBox │ │ │ └── index.tsx │ │ ├── ProgressLogBox │ │ │ └── index.tsx │ │ ├── Serials │ │ │ └── index.tsx │ │ ├── SortForm │ │ │ └── index.tsx │ │ ├── SyncSelect │ │ │ └── index.tsx │ │ ├── Trashbin │ │ │ └── index.tsx │ │ ├── VideoParamsForm │ │ │ └── index.tsx │ │ └── index.tsx │ ├── Pagination │ │ └── index.tsx │ ├── PaginationButton │ │ └── index.tsx │ ├── Palette │ │ └── index.tsx │ ├── PaletteContextMenu │ │ └── index.tsx │ ├── PaletteIcon │ │ └── index.tsx │ ├── PalettePreview │ │ └── index.tsx │ ├── PaletteSelect │ │ └── index.tsx │ ├── Palettes │ │ └── index.tsx │ ├── PluginSelect │ │ └── index.tsx │ ├── PrinterReport │ │ └── index.tsx │ ├── RGBNSelect │ │ └── index.tsx │ ├── Remote │ │ └── index.tsx │ ├── SettingsDropbox │ │ └── index.tsx │ ├── SettingsGeneric │ │ └── index.tsx │ ├── SettingsGit │ │ └── index.tsx │ ├── SettingsPlugins │ │ ├── PluginConfig.tsx │ │ ├── PluginInputField.tsx │ │ └── index.tsx │ ├── SettingsTabs │ │ └── index.tsx │ ├── SettingsWiFi │ │ ├── APConfig.tsx │ │ └── index.tsx │ ├── StorageWarning │ │ └── index.tsx │ ├── TagsList │ │ └── index.tsx │ ├── TagsSelect │ │ ├── InputNewTag.tsx │ │ └── index.tsx │ └── WebUSBGreeting │ │ ├── EnableWebUSB.tsx │ │ └── index.tsx ├── consts │ ├── GalleryClickAction.ts │ ├── GalleryViews.ts │ ├── SavImportOrder.ts │ ├── SpecialTags.ts │ ├── batchActionTypes.ts │ ├── bitmapQueueSettings.ts │ ├── blendModes.ts │ ├── defaults.ts │ ├── dialog.ts │ ├── exportFrameModes.ts │ ├── exportTypes.ts │ ├── fileNameStyles.ts │ ├── gbxCart.ts │ ├── handleLine.ts │ ├── paletteSortModes.ts │ ├── plugins.ts │ ├── ports.ts │ ├── printerFunction.ts │ ├── sync.ts │ ├── textFieldSlotDefaults.ts │ └── theme.ts ├── contexts │ ├── envContext │ │ └── index.tsx │ ├── galleryTree │ │ ├── Provider.tsx │ │ └── index.ts │ ├── i18nContext │ │ └── index.tsx │ ├── navigationTools │ │ ├── NavigationToolsProvider.tsx │ │ └── index.ts │ ├── plugins │ │ ├── Provider.tsx │ │ ├── functions │ │ │ ├── collectImageData.ts │ │ │ ├── initPlugin.ts │ │ │ └── pluginContextFunctions.ts │ │ └── index.ts │ ├── ports │ │ ├── Provider.tsx │ │ └── index.ts │ └── remotePrinter │ │ ├── RemotePrinterContextProvider.tsx │ │ └── index.ts ├── hooks │ ├── useAddPlugin.ts │ ├── useAsPasswordField.tsx │ ├── useAvailableTags.ts │ ├── useBatchButtons.ts │ ├── useBatchUpdate.ts │ ├── useDateFormat.ts │ ├── useDebugValueChange.ts │ ├── useDialog.ts │ ├── useDownload.ts │ ├── useDropboxSettings.ts │ ├── useEditForm.ts │ ├── useEditFrame.ts │ ├── useEditImageGroup.ts │ ├── useEditPalette.ts │ ├── useEditRGBNImages.ts │ ├── useFileDrop.ts │ ├── useFilterForm.ts │ ├── useFrame.ts │ ├── useFrames.ts │ ├── useGBXCart.ts │ ├── useGallery.ts │ ├── useGalleryGroup.ts │ ├── useGalleryImage.ts │ ├── useGalleryImageContext.ts │ ├── useGetPortSettings.ts │ ├── useHandleHashParams.ts │ ├── useIdle.ts │ ├── useIframeLoaded.ts │ ├── useImageDimensions.ts │ ├── useImageGroups.ts │ ├── useImageRender.ts │ ├── useImportExportSettings.ts │ ├── useImportFile.ts │ ├── useImportPlainText.ts │ ├── useLightboxImage.ts │ ├── useNavigation.ts │ ├── useOverlayGlobalKeys.ts │ ├── usePalette.ts │ ├── usePaletteFromFile.ts │ ├── usePaletteSort.ts │ ├── usePathSegments.ts │ ├── usePreviewImages.ts │ ├── usePrinter.ts │ ├── useProcessMarkdownLinks.ts │ ├── useProgressLog.ts │ ├── useRemoteWindow.ts │ ├── useRunImport.ts │ ├── useSaveRGBNImages.ts │ ├── useScreenDimensions.ts │ ├── useSetEditPalette.ts │ ├── useShareImage.ts │ ├── useSortForm.ts │ ├── useStorageInfo.ts │ ├── useStoragePersist.ts │ ├── useStores.ts │ ├── useSuperPrinterInterface.ts │ ├── useSyncSelect.ts │ ├── useTrashbin.ts │ ├── useUrl.ts │ └── useVideoForm.ts ├── i18n │ ├── formats.ts │ ├── locales.ts │ ├── markdown │ │ ├── Startpage │ │ │ ├── de.md │ │ │ ├── en.md │ │ │ ├── es.md │ │ │ ├── fr.md │ │ │ ├── it.md │ │ │ └── nl.md │ │ └── WebUSB │ │ │ ├── de.md │ │ │ ├── en.md │ │ │ ├── es.md │ │ │ ├── fr.md │ │ │ ├── it.md │ │ │ └── nl.md │ ├── messages │ │ ├── de.json │ │ ├── en.json │ │ ├── es.json │ │ ├── fr.json │ │ ├── it.json │ │ ├── nl.json │ │ ├── no.json │ │ ├── pt.json │ │ └── sv.json │ └── request.ts ├── scss │ └── generic.scss ├── stores │ ├── constants.ts │ ├── dialogsStore.ts │ ├── editStore.ts │ ├── filtersStore.ts │ ├── importsStore.ts │ ├── interactionsStore.ts │ ├── itemsStore.ts │ ├── migrations │ │ ├── cleanupItems.ts │ │ └── history │ │ │ └── 0 │ │ │ ├── State.d.ts │ │ │ ├── cleanImages.ts │ │ │ ├── hashFrames.ts │ │ │ └── migrateItems.ts │ ├── progressStore.ts │ ├── settingsStore.ts │ └── storagesStore.ts ├── styles │ ├── components │ │ ├── appBar.ts │ │ ├── button.ts │ │ ├── cardActionArea.ts │ │ ├── cardContent.ts │ │ ├── dialogTitle.ts │ │ ├── formControl.ts │ │ ├── inputLabel.ts │ │ ├── link.ts │ │ ├── list.ts │ │ ├── menuItem.ts │ │ ├── outlinedInput.ts │ │ ├── paper.ts │ │ ├── select.ts │ │ ├── switch.ts │ │ ├── tab.ts │ │ ├── tabs.ts │ │ ├── textField.ts │ │ ├── toggleButtonGroup.ts │ │ └── toolbar.ts │ ├── themes.ts │ └── tools │ │ ├── generateTheme.ts │ │ ├── getHoverColor.ts │ │ └── getPreStyles.ts ├── tools │ ├── appendUint8Arrays │ │ └── index.ts │ ├── applyBitmapFilter │ │ ├── generateBaseValues.ts │ │ ├── generatePattern.ts │ │ ├── generateValueRange.ts │ │ ├── index.ts │ │ └── orderPatterns.ts │ ├── applyFrame │ │ ├── frameData.ts │ │ ├── index.ts │ │ └── wildDummy.ts │ ├── applyTagChanges │ │ └── index.ts │ ├── blobToArrayBuffer │ │ └── index.ts │ ├── canShare │ │ └── index.ts │ ├── chars │ │ └── index.ts │ ├── cleanPath │ │ └── index.ts │ ├── cleanUrl │ │ └── index.ts │ ├── comms │ │ ├── CommonPort │ │ │ └── index.ts │ │ ├── DeviceAPIs │ │ │ ├── BaseCommsDevice.ts │ │ │ ├── CaptureCommsDevice.ts │ │ │ ├── GBXCartCommsDevice.ts │ │ │ ├── InactiveCommsDevice.ts │ │ │ └── SuperPrinterCommsDevice.ts │ │ ├── WebSerial │ │ │ ├── SerialPort.ts │ │ │ └── SerialPorts.ts │ │ └── WebUSBSerial │ │ │ ├── USBPort.ts │ │ │ └── USBPorts.ts │ ├── createAnimation │ │ └── index.ts │ ├── createTreeRoot │ │ └── index.ts │ ├── database │ │ ├── dbGetSet.ts │ │ └── lsGetSet.ts │ ├── delay │ │ └── index.ts │ ├── download │ │ ├── download.ts │ │ ├── getPrepareFiles.ts │ │ ├── getTxtFile.ts │ │ └── index.ts │ ├── dropboxStorage │ │ ├── DropboxClient │ │ │ ├── dropboxContentHasher.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ └── main.ts │ ├── ensureSingleUsage │ │ └── index.ts │ ├── filterDeleteNew │ │ └── index.ts │ ├── findSubarray │ │ └── index.ts │ ├── generateDebugImages │ │ └── index.ts │ ├── generateFileName │ │ └── index.ts │ ├── generateGradient │ │ └── index.ts │ ├── getChannelColor │ │ └── index.ts │ ├── getFilteredImages │ │ ├── count.ts │ │ ├── filterSpecial.ts │ │ ├── filterTags.ts │ │ └── index.ts │ ├── getFrameFromFullTiles │ │ └── index.ts │ ├── getFrameGroups │ │ └── index.ts │ ├── getFramesForGroup │ │ └── index.ts │ ├── getHandleFileImport │ │ ├── index.ts │ │ └── prepareFile.ts │ ├── getImagePalettes │ │ └── index.ts │ ├── getMonochromeImageCreationParams │ │ └── index.ts │ ├── getPaletteSettings │ │ └── index.ts │ ├── getPrepareRemoteFiles │ │ └── index.ts │ ├── getScrollParent │ │ └── index.ts │ ├── getSettings │ │ ├── getFrames.ts │ │ ├── getFramesForExport.ts │ │ ├── getImageHashesForExport.ts │ │ ├── getImages.ts │ │ └── index.ts │ ├── getTrash │ │ └── index.ts │ ├── getUploadFiles │ │ ├── getUploadFrames.ts │ │ ├── getUploadImages.ts │ │ └── index.ts │ ├── gitStorage │ │ ├── OctoClient.ts │ │ ├── index.ts │ │ └── main.ts │ ├── handleLines │ │ └── index.ts │ ├── hashCleanup │ │ └── index.ts │ ├── hexToRgbString │ │ └── index.ts │ ├── importExportSettings │ │ └── getImportJSON.ts │ ├── isGoodScaleFactor │ │ └── index.ts │ ├── isRGBNImage │ │ └── index.ts │ ├── loadImageTiles │ │ └── index.ts │ ├── localforageInstance │ │ ├── createWrappedInstance.ts │ │ └── index.ts │ ├── mergeStates │ │ └── index.ts │ ├── modifyTagChanges │ │ └── index.ts │ ├── moveBitmapsToImport │ │ └── index.ts │ ├── pack │ │ └── index.ts │ ├── padToHeight │ │ └── index.ts │ ├── parseAuthParams │ │ └── index.ts │ ├── randomId │ │ └── index.ts │ ├── readFileAs │ │ └── index.ts │ ├── reduceArray │ │ └── index.ts │ ├── remote │ │ ├── commands │ │ │ ├── checkPrinter.ts │ │ │ ├── clearPrinter.ts │ │ │ ├── fetchImages.ts │ │ │ └── testFile.ts │ │ ├── fetchDumpRetry.ts │ │ ├── initCommands.ts │ │ └── startHeartbeat.ts │ ├── replaceDuplicateFilenames │ │ └── index.ts │ ├── saveLocalStorageItems │ │ └── index.ts │ ├── saveNewImage │ │ └── index.ts │ ├── shorten │ │ └── index.ts │ ├── sortImages │ │ └── index.ts │ ├── sortby │ │ └── index.ts │ ├── storage │ │ ├── dummyImage.ts │ │ └── index.ts │ ├── supportedCanvasImageFormats │ │ └── index.ts │ ├── textToTiles │ │ └── index.ts │ ├── toCreationDate │ │ └── index.ts │ ├── transferLocalStorage │ │ └── index.ts │ ├── transformBin │ │ └── index.ts │ ├── transformBitmaps │ │ ├── getImageData.ts │ │ └── index.ts │ ├── transformCapture │ │ └── index.ts │ ├── transformClassic │ │ └── index.ts │ ├── transformPlainText │ │ └── index.ts │ ├── transformReduced │ │ └── index.ts │ ├── transformSav │ │ ├── charMap.ts │ │ ├── getFileMeta.ts │ │ ├── importSav.ts │ │ ├── index.ts │ │ ├── mapCartFrameToHash.ts │ │ ├── parseCustomMetadata.ts │ │ ├── transformImage.ts │ │ └── valueDefs.ts │ └── unique │ │ ├── by.ts │ │ └── index.ts ├── types │ ├── BitmapFilter.ts │ ├── Dialog.ts │ ├── Export.d.ts │ ├── ExportState.d.ts │ ├── Frame.d.ts │ ├── FrameGroup.d.ts │ ├── Image.d.ts │ ├── ImageActions.d.ts │ ├── ImageGroup.d.ts │ ├── ImportItem.d.ts │ ├── Palette.d.ts │ ├── PickColors.d.ts │ ├── Plugin.ts │ ├── PluginCompatibility.ts │ ├── Printer.d.ts │ ├── QueueImage.d.ts │ ├── ReactCss.d.ts │ ├── Sync.d.ts │ ├── VideoParams.d.ts │ ├── download.ts │ ├── galleryTreeContext.d.ts │ ├── handleFileImport.ts │ ├── handleLine.ts │ ├── ports.d.ts │ ├── transformSav.ts │ └── types.d.ts └── workers │ ├── portsContextWorker.ts │ └── treeContextWorker.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = crlf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.svg] 16 | insert_final_newline = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | dist/ 4 | releases/ 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | .npm 10 | .eslintcache 11 | .node_repl_history 12 | .yarn-integrity 13 | .idea 14 | config.json 15 | dumps/* 16 | !dumps/.gitkeep 17 | .DS_Store 18 | *.iml 19 | 20 | # generated 21 | /public/fav 22 | 23 | # next.js 24 | /.next/ 25 | /out/ 26 | /o/ 27 | 28 | # production 29 | /build 30 | 31 | # env files (can opt-in for committing if needed) 32 | .env* 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.windsurf/rules/translations.md: -------------------------------------------------------------------------------- 1 | --- 2 | trigger: model_decision 3 | description: Translating language JSON files 4 | globs: 5 | --- 6 | 7 | * When generating translations, the keys in the source and target need to match 100% 8 | * Do not omit special i18n formatting commands for placeholders. 9 | * Do not alter message placeholders. 10 | * Translation is only completed if _all_ keys are translated 11 | * Translation is only completed if no extra keys are created 12 | * Do not change the order of sections or messages in any way 13 | * Translates messages should not use ascii char 34 quotes (") or ascii char 35 quotes ('). 14 | * Use ~o~ and ~c~ as placeholder for opening/closing typographic quotes so I can find/replace them easily. 15 | * The files to be translated have over 600 lines, so translate them in steps/parts of ~100-200 lines. Don't try to create a complete translation as this will fail. 16 | * Don't give me code to copy/paste into the files. Just edit the file in steps/parts 17 | * run `npm run comparelangs ` to verify basic integrity of the translation 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Andreas Hahn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { FlatCompat } from '@eslint/eslintrc'; 2 | import { fileURLToPath } from 'url'; 3 | import { dirname } from 'path'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends( 14 | 'next/core-web-vitals', 15 | 'next/typescript', 16 | ), 17 | { 18 | files: ['**/*.{js,ts,jsx,tsx}'], 19 | 20 | rules: { 21 | 'quotes': ['error', 'single'], 22 | 'object-curly-spacing': ['error', 'always'], 23 | 'comma-dangle': ['error', 'always-multiline'], 24 | 'semi': ['error', 'always'], 25 | 'array-bracket-spacing': ['error', 'never'], 26 | 'no-shadow': 'off', 27 | '@typescript-eslint/no-shadow': ['error'], 28 | 'no-unused-vars': 'off', 29 | '@typescript-eslint/no-unused-vars': ['warn'], 30 | 'no-bitwise': ['error'], 31 | 32 | 'import/order': [ 33 | 'error', 34 | { 35 | groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], 36 | alphabetize: { 37 | order: 'asc', 38 | caseInsensitive: true 39 | }, 40 | 'newlines-between': 'never', 41 | }, 42 | ], 43 | }, 44 | }, 45 | ]; 46 | 47 | export default eslintConfig; 48 | -------------------------------------------------------------------------------- /public/dumps/00001.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerrZatacke/gb-printer-web/3e0b54054cd1ff7f9b4f8aa08daa3869f7bee30b/public/dumps/00001.bin -------------------------------------------------------------------------------- /public/dumps/00002.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerrZatacke/gb-printer-web/3e0b54054cd1ff7f9b4f8aa08daa3869f7bee30b/public/dumps/00002.bin -------------------------------------------------------------------------------- /public/dumps/00003.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerrZatacke/gb-printer-web/3e0b54054cd1ff7f9b4f8aa08daa3869f7bee30b/public/dumps/00003.bin -------------------------------------------------------------------------------- /public/dumps/00004.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerrZatacke/gb-printer-web/3e0b54054cd1ff7f9b4f8aa08daa3869f7bee30b/public/dumps/00004.bin -------------------------------------------------------------------------------- /public/dumps/clear: -------------------------------------------------------------------------------- 1 | {"deleted":2} 2 | -------------------------------------------------------------------------------- /public/dumps/list: -------------------------------------------------------------------------------- 1 | { 2 | "dumps": [ 3 | "/dumps/00001.bin", 4 | "/dumps/00002.bin", 5 | "/dumps/00003.bin", 6 | "/dumps/00004.bin" 7 | ], 8 | "fs": { 9 | "total": 2072576, 10 | "used": 786432, 11 | "available": 1286144, 12 | "dumpcount": 4, 13 | "maximages": 150 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /public/env-pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "n/a", 3 | "maximages": "∞", 4 | "env": "gh-pages", 5 | "fstype": "n/a", 6 | "bootmode": "n/a", 7 | "oled": false 8 | } 9 | -------------------------------------------------------------------------------- /public/env.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "n/a", 3 | "maximages": "∞", 4 | "env": "webpack-dev", 5 | "fstype": "n/a", 6 | "bootmode": "n/a", 7 | "oled": false 8 | } 9 | -------------------------------------------------------------------------------- /runme.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname "$0")" 3 | npm serve 4 | -------------------------------------------------------------------------------- /scripts/copyAndGZ.mjs: -------------------------------------------------------------------------------- 1 | import { createReadStream, createWriteStream } from 'node:fs'; 2 | import { pipeline } from 'node:stream'; 3 | import { createGzip } from 'node:zlib'; 4 | 5 | const copyAndGZ = async (source, destination) => { 6 | return new Promise((resolve) => { 7 | pipeline( 8 | createReadStream(source), 9 | createGzip(), 10 | createWriteStream(`${destination}.gz`), 11 | resolve, 12 | ); 13 | }); 14 | }; 15 | 16 | export default copyAndGZ; 17 | -------------------------------------------------------------------------------- /scripts/replaceInFiles.mjs: -------------------------------------------------------------------------------- 1 | import { globby } from 'globby'; 2 | import fs from 'node:fs'; 3 | 4 | const outDir = 'out'; 5 | const filesToShorten = await globby([`${outDir}/**/*.html`]); 6 | 7 | for (const filePath of filesToShorten) { 8 | const replacementRegex = //i; 9 | const fileContents = await fs.promises.readFile(filePath, { encoding: 'utf-8' }); 10 | const match = fileContents.match(replacementRegex); 11 | 12 | if (match) { 13 | const contentFile = match[1]; 14 | const replacement = await fs.promises.readFile(contentFile, { encoding: 'utf-8' }); 15 | const changedContent = fileContents.replace(replacementRegex, replacement); 16 | await fs.promises.writeFile(filePath, changedContent, { encoding: 'utf-8' }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /scripts/shortenStatic.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import crypto from 'node:crypto'; 4 | import { globby } from 'globby'; 5 | import copyAndGZ from './copyAndGZ.mjs'; 6 | 7 | const inBaseDir = 'out'; 8 | const outBaseDir = 'o'; 9 | const outFilesDir = path.posix.join(outBaseDir, 'w'); 10 | 11 | const filesToShorten = await globby([`${inBaseDir}/**/*.*`]); 12 | 13 | const getShortName = async (filePath) => { 14 | const fileContents = await fs.promises.readFile(filePath); 15 | const hash = crypto.createHash('sha256') 16 | .update(filePath) 17 | .update(fileContents) 18 | .digest('hex'); 19 | return hash.slice(0, 6); 20 | }; 21 | 22 | // Rename files and store mapping 23 | const nameMap = new Map(); 24 | for (const filePath of filesToShorten) { 25 | const ext = path.posix.extname(filePath); 26 | const newName = await getShortName(filePath) + ext; 27 | const newPath = path.posix.join(outFilesDir, newName); 28 | 29 | await fs.promises.mkdir(path.posix.dirname(newPath), { recursive: true }); 30 | await copyAndGZ(filePath, newPath); 31 | 32 | const newRelPath = path.posix.relative(outFilesDir, newPath); 33 | const originalRelPath = path.posix.relative(inBaseDir, filePath); 34 | 35 | const found = nameMap.get(newRelPath); 36 | 37 | if (found) { 38 | throw new Error(`Duplicate file hash! "${found}" / "${originalRelPath}"`); 39 | } 40 | 41 | nameMap.set(newRelPath, originalRelPath); 42 | } 43 | 44 | // Create map file 45 | const mapFileContent = []; 46 | for (const [replacement, original] of nameMap.entries()) { 47 | mapFileContent.push(`${original};${replacement}`) 48 | } 49 | await fs.promises.writeFile(path.posix.join(outBaseDir, 'pathmap.txt'), mapFileContent.join('\n'), { encoding: 'utf-8' }); 50 | 51 | -------------------------------------------------------------------------------- /src/app/(pages)/add-plugin/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Typography from '@mui/material/Typography'; 4 | import dynamic from 'next/dynamic'; 5 | import { useTranslations } from 'next-intl'; 6 | 7 | const AddPlugin = dynamic(() => import('@/components/AddPlugin'), { 8 | ssr: false, 9 | }); 10 | 11 | export default function AddPluginPage() { 12 | const t = useTranslations('Navigation'); 13 | 14 | return ( 15 | <> 16 | 20 | {t('addPlugin')} 21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/app/(pages)/frames/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Typography from '@mui/material/Typography'; 4 | import { useTranslations } from 'next-intl'; 5 | import Frames from '@/components/Frames'; 6 | 7 | export default function FramesPage() { 8 | const t = useTranslations('Navigation'); 9 | 10 | return ( 11 | <> 12 | 16 | {t('frames')} 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/(pages)/gallery/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Typography from '@mui/material/Typography'; 4 | import { useTranslations } from 'next-intl'; 5 | import Gallery from '@/components/Gallery'; 6 | 7 | export default function GalleryPage() { 8 | const t = useTranslations('Navigation'); 9 | 10 | return ( 11 | <> 12 | 16 | {t('gallery')} 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/(pages)/globals.scss: -------------------------------------------------------------------------------- 1 | @keyframes spin { 2 | from { transform: rotate(0deg); } 3 | to { transform: rotate(360deg); } 4 | } 5 | 6 | @keyframes pulse-bg { 7 | 0% { 8 | background-color: rgba(115, 91, 154, 1); 9 | } 10 | 50% { 11 | background-color: rgba(115, 91, 154, 0.3); 12 | } 13 | 100% { 14 | background-color: rgba(115, 91, 154, 1); 15 | } 16 | } 17 | 18 | :root { 19 | --navigation-height: 64px; 20 | } 21 | -------------------------------------------------------------------------------- /src/app/(pages)/import/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Typography from '@mui/material/Typography'; 4 | import { useTranslations } from 'next-intl'; 5 | import Import from '@/components/Import'; 6 | 7 | export default function ImportPage() { 8 | const t = useTranslations('Navigation'); 9 | 10 | return ( 11 | <> 12 | 16 | {t('import')} 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/(pages)/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Card from '@mui/material/Card'; 4 | import CardContent from '@mui/material/CardContent'; 5 | import MuiMarkdown from 'mui-markdown'; 6 | import { useEffect, useState } from 'react'; 7 | import MarkdownStack from '@/components/MarkdownStack'; 8 | import useProcessMarkdownLinks from '@/hooks/useProcessMarkdownLinks'; 9 | import { shortLocales } from '@/i18n/locales'; 10 | import readmeEn from '@/i18n/markdown/Startpage/en.md'; 11 | import useSettingsStore from '@/stores/settingsStore'; 12 | 13 | export default function Home() { 14 | const [readme, setReadme] = useState(readmeEn); 15 | const { preferredLocale } = useSettingsStore(); 16 | const processedReadme = useProcessMarkdownLinks(readme); 17 | 18 | useEffect(() => { 19 | const set = async () => { 20 | let langFile = preferredLocale.split('-')[0]; 21 | 22 | if (!shortLocales.includes(langFile)) { 23 | langFile = 'en'; 24 | } 25 | 26 | try { 27 | setReadme((await import(`@/i18n/markdown/Startpage/${langFile}.md`)).default); 28 | } catch { 29 | setReadme(readmeEn); 30 | } 31 | }; 32 | 33 | set(); 34 | }, [preferredLocale]); 35 | 36 | return ( 37 | 38 | 39 | 40 | {processedReadme} 41 | 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/app/(pages)/palettes/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Typography from '@mui/material/Typography'; 4 | import { useTranslations } from 'next-intl'; 5 | import Palettes from '@/components/Palettes'; 6 | 7 | export default function PalettesPage() { 8 | const t = useTranslations('Navigation'); 9 | 10 | return ( 11 | <> 12 | 16 | {t('palettes')} 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/(pages)/settings/dropbox/page.tsx: -------------------------------------------------------------------------------- 1 | import SettingsDropbox from '@/components/SettingsDropbox'; 2 | 3 | export default async function DropboxSettingsPage() { 4 | return ( 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/app/(pages)/settings/generic/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SettingsGeneric from '@/components/SettingsGeneric'; 3 | 4 | export default async function GenericSettingsPage() { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/(pages)/settings/git/page.tsx: -------------------------------------------------------------------------------- 1 | import SettingsGit from '@/components/SettingsGit'; 2 | 3 | export default async function GitSettingsPage() { 4 | return ( 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/app/(pages)/settings/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Box from '@mui/material/Box'; 4 | import Stack from '@mui/material/Stack'; 5 | import Typography from '@mui/material/Typography'; 6 | import { useTranslations } from 'next-intl'; 7 | import { PropsWithChildren } from 'react'; 8 | import EnvInfo from '@/components/EnvInfo'; 9 | import ExportSettings from '@/components/ExportSettings'; 10 | import SettingsTabs from '@/components/SettingsTabs'; 11 | import { useIdle } from '@/hooks/useIdle'; 12 | 13 | export default function SettingsLayout({ children }: Readonly) { 14 | const t = useTranslations('Navigation'); 15 | const isIdle = useIdle(); 16 | 17 | return ( 18 | <> 19 | 23 | {t('settings')} 24 | 25 | {isIdle && ( 26 | 32 | 36 | 37 | {children} 38 | 39 | 40 | 41 | 42 | 43 | )} 44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/app/(pages)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | 3 | export default function SettingsRedirectPage() { 4 | redirect('/settings/generic'); 5 | } 6 | -------------------------------------------------------------------------------- /src/app/(pages)/settings/plugins/page.tsx: -------------------------------------------------------------------------------- 1 | import SettingsPlugins from '@/components/SettingsPlugins'; 2 | 3 | export default async function PluginSettingsPage() { 4 | return ( 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/app/(pages)/settings/wifi/page.tsx: -------------------------------------------------------------------------------- 1 | import SettingsWiFi from '@/components/SettingsWiFi'; 2 | 3 | export default async function WiFiSettingsPage() { 4 | return ( 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/app/(pages)/webusb/page.tsx: -------------------------------------------------------------------------------- 1 | import WebUSBGreeting from '@/components/WebUSBGreeting'; 2 | 3 | export default async function WebUSBPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/(remote)/db/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | import CopyDatabase from '@/components/CopyDatabase'; 3 | 4 | export default function DatabasePage() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/app/(remote)/layout.tsx: -------------------------------------------------------------------------------- 1 | import 'reset-css/reset.css'; 2 | import '../../scss/generic.scss'; 3 | import './remote.scss'; 4 | 5 | export default function RootLayout({ 6 | children, 7 | }: Readonly<{ 8 | children: React.ReactNode; 9 | }>) { 10 | return ( 11 | <> 12 | { children } 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/(remote)/remote.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background: #ffffff; 3 | } 4 | 5 | .remote-info { 6 | font-size: 16px; 7 | margin-bottom: 15px; 8 | padding: 20px; 9 | 10 | &--ip { 11 | color: #1a1a1a; 12 | 13 | html.theme-dark & { 14 | color: #e5e5e5; 15 | } 16 | } 17 | 18 | &--em { 19 | color: #ff0000; 20 | 21 | html.theme-dark & { 22 | color: #cb0003; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/(remote)/remote/page.tsx: -------------------------------------------------------------------------------- 1 | import Remote from '@/components/Remote'; 2 | 3 | export default async function RemotePage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerrZatacke/gb-printer-web/3e0b54054cd1ff7f9b4f8aa08daa3869f7bee30b/src/app/favicon.ico -------------------------------------------------------------------------------- /src/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerrZatacke/gb-printer-web/3e0b54054cd1ff7f9b4f8aa08daa3869f7bee30b/src/assets/images/favicon.png -------------------------------------------------------------------------------- /src/assets/images/greys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerrZatacke/gb-printer-web/3e0b54054cd1ff7f9b4f8aa08daa3869f7bee30b/src/assets/images/greys.png -------------------------------------------------------------------------------- /src/components/ColorSlider/index.tsx: -------------------------------------------------------------------------------- 1 | import RestoreIcon from '@mui/icons-material/Restore'; 2 | import IconButton from '@mui/material/IconButton'; 3 | import Slider from '@mui/material/Slider'; 4 | import Stack from '@mui/material/Stack'; 5 | import React, { useMemo } from 'react'; 6 | import type { RGBNHashes } from '@/types/Image'; 7 | 8 | interface Props { 9 | color: keyof RGBNHashes, 10 | onChange: (values: readonly number[]) => void, 11 | values: number[], 12 | } 13 | 14 | function ColorSlider({ color, onChange, values }: Props) { 15 | const colorCode = useMemo(() => { 16 | switch (color) { 17 | case 'r': 18 | return '#ff0000'; 19 | case 'g': 20 | return '#00dd00'; 21 | case 'b': 22 | return '#0000ff'; 23 | case 'n': 24 | default: 25 | return '#888888'; 26 | } 27 | }, [color]); 28 | 29 | return ( 30 | 36 | { 39 | onChange([0x00, 0x55, 0xaa, 0xff]); 40 | }} 41 | > 42 | 43 | 44 | { 53 | onChange(value as number[]); 54 | }} 55 | disableSwap 56 | /> 57 | 58 | ); 59 | } 60 | 61 | export default ColorSlider; 62 | -------------------------------------------------------------------------------- /src/components/CopyDatabase/styles.scss: -------------------------------------------------------------------------------- 1 | .database-page { 2 | padding: 40px; 3 | 4 | &.is-child { 5 | display: none; 6 | } 7 | } 8 | 9 | .url-load { 10 | display: flex; 11 | flex-direction: row; 12 | gap: 12px; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Debug/index.tsx: -------------------------------------------------------------------------------- 1 | import Typography from '@mui/material/Typography'; 2 | import type { Theme } from '@mui/system'; 3 | import React from 'react'; 4 | import useSettingsStore from '@/stores/settingsStore'; 5 | import { getPreStyles } from '@/styles/tools/getPreStyles'; 6 | 7 | interface Props { 8 | text: string, 9 | } 10 | 11 | 12 | function Debug({ text }: Props) { 13 | const { enableDebug } = useSettingsStore(); 14 | 15 | if (!enableDebug) { 16 | return null; 17 | } 18 | 19 | return ( 20 | getPreStyles(theme, { 26 | px: 0.5, 27 | py: 0, 28 | mt: 1, 29 | mb: 0, 30 | backgroundColor: theme.palette.warning[theme.palette.mode], 31 | })} 32 | > 33 | { text } 34 | 35 | ); 36 | } 37 | 38 | export default Debug; 39 | -------------------------------------------------------------------------------- /src/components/EnvInfo/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Stack from '@mui/material/Stack'; 4 | import Typography from '@mui/material/Typography'; 5 | import { useTranslations } from 'next-intl'; 6 | import { useMemo } from 'react'; 7 | import { useEnv } from '@/contexts/envContext'; 8 | 9 | function EnvInfo() { 10 | const env = useEnv(); 11 | const t = useTranslations('EnvInfo'); 12 | 13 | const infos = useMemo(() => { 14 | if (!env) { 15 | return []; 16 | } 17 | 18 | return ([ 19 | t('appVersion', { value: process.env.NEXT_PUBLIC_VERSION || '' }), 20 | t('appBranch', { value: process.env.NEXT_PUBLIC_BRANCH || '' }), 21 | t('printerVersion', { value: env.version }), 22 | t('maxImages', { value: env.maximages }), 23 | t('storageDriver', { value: env.localforage }), 24 | t('envType', { value: env.env }), 25 | t('filesystem', { value: env.fstype }), 26 | t('bootmode', { value: env.bootmode }), 27 | t('hasOled', { value: env.oled ? t('yes') : t('no') }), 28 | ]); 29 | }, [env, t]); 30 | 31 | return ( 32 | 36 | { infos.map((text) => ( 37 | 45 | {text} 46 | 47 | )) } 48 | 49 | ); 50 | } 51 | 52 | export default EnvInfo; 53 | -------------------------------------------------------------------------------- /src/components/Errors/index.tsx: -------------------------------------------------------------------------------- 1 | import Stack from '@mui/material/Stack'; 2 | import type { Theme } from '@mui/system'; 3 | import React from 'react'; 4 | import useInteractionsStore from '@/stores/interactionsStore'; 5 | import ErrorMessage from './ErrorMessage'; 6 | 7 | function Error() { 8 | 9 | const { errors, dismissError } = useInteractionsStore(); 10 | 11 | return ( 12 | ( 16 | { 17 | position: 'fixed', 18 | top: theme.spacing(10), 19 | right: theme.spacing(2), 20 | zIndex: 30, 21 | } 22 | )} 23 | > 24 | { errors.map((errorMessage, index) => ( 25 | dismissError(index)} 28 | errorMessage={errorMessage} 29 | /> 30 | ))} 31 | 32 | ); 33 | } 34 | 35 | export default Error; 36 | -------------------------------------------------------------------------------- /src/components/FrameContextMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import DeleteIcon from '@mui/icons-material/Delete'; 2 | import EditIcon from '@mui/icons-material/Edit'; 3 | import ListItemIcon from '@mui/material/ListItemIcon'; 4 | import ListItemText from '@mui/material/ListItemText'; 5 | import Menu from '@mui/material/Menu'; 6 | import MenuItem from '@mui/material/MenuItem'; 7 | import { useTranslations } from 'next-intl'; 8 | import React from 'react'; 9 | 10 | interface Props { 11 | deleteFrame: () => void, 12 | editFrame: () => void, 13 | menuAnchor: HTMLElement | null, 14 | onClose: () => void, 15 | } 16 | 17 | 18 | function GalleryGroupContextMenu({ deleteFrame, editFrame, menuAnchor, onClose }: Props) { 19 | const t = useTranslations('FrameContextMenu'); 20 | 21 | if (!menuAnchor) { 22 | return null; 23 | } 24 | 25 | return ( 26 | { 31 | ev.stopPropagation(); 32 | onClose(); 33 | }} 34 | > 35 | 39 | 40 | 41 | 42 | 43 | {t('edit')} 44 | 45 | 46 | 50 | 51 | 52 | 53 | 54 | {t('delete')} 55 | 56 | 57 | 58 | ); 59 | } 60 | 61 | export default GalleryGroupContextMenu; 62 | -------------------------------------------------------------------------------- /src/components/GalleryGroupContextMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import DeleteIcon from '@mui/icons-material/Delete'; 2 | import EditIcon from '@mui/icons-material/Edit'; 3 | import ListItemIcon from '@mui/material/ListItemIcon'; 4 | import ListItemText from '@mui/material/ListItemText'; 5 | import Menu from '@mui/material/Menu'; 6 | import MenuItem from '@mui/material/MenuItem'; 7 | import { useTranslations } from 'next-intl'; 8 | import React from 'react'; 9 | import { useImageGroups } from '@/hooks/useImageGroups'; 10 | 11 | interface Props { 12 | groupId: string, 13 | menuAnchor: HTMLElement | null, 14 | onClose: () => void, 15 | } 16 | 17 | function GalleryGroupContextMenu({ groupId, menuAnchor, onClose }: Props) { 18 | const t = useTranslations('GalleryGroupContextMenu'); 19 | const { deleteGroup, editGroup } = useImageGroups(); 20 | 21 | if (!menuAnchor) { 22 | return null; 23 | } 24 | 25 | return ( 26 | { 31 | ev.stopPropagation(); 32 | onClose(); 33 | }} 34 | > 35 | editGroup(groupId)} 37 | title={t('edit')} 38 | > 39 | 40 | 41 | 42 | 43 | {t('edit')} 44 | 45 | 46 | deleteGroup(groupId)} 48 | title={t('delete')} 49 | > 50 | 51 | 52 | 53 | 54 | {t('delete')} 55 | 56 | 57 | 58 | ); 59 | } 60 | 61 | export default GalleryGroupContextMenu; 62 | -------------------------------------------------------------------------------- /src/components/GalleryHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import Stack from '@mui/material/Stack'; 2 | import React from 'react'; 3 | import BatchButtons from '@/components/BatchButtons'; 4 | import GalleryViewSelect from '@/components/GalleryViewSelect'; 5 | 6 | interface Props { 7 | page: number, 8 | isBottom?: boolean, 9 | isSticky?: boolean, 10 | } 11 | 12 | function GalleryHeader({ isBottom, page, isSticky }: Props) { 13 | return ( 14 | ({ 20 | zIndex: 20, 21 | backgroundColor: theme.palette.background.default, 22 | ...(isSticky ? { 23 | position: 'sticky', 24 | top: 'var(--navigation-height)', 25 | py: 1, 26 | mx: -1, 27 | } : {}), 28 | ...(isBottom ? { 29 | '@media (max-height: 900px)': { 30 | display: 'none', 31 | }, 32 | } : {}), 33 | })} 34 | > 35 | 36 | 37 | 38 | ); 39 | } 40 | 41 | export default GalleryHeader; 42 | -------------------------------------------------------------------------------- /src/components/GalleryNumbers/index.tsx: -------------------------------------------------------------------------------- 1 | import Typography from '@mui/material/Typography'; 2 | import { useTranslations } from 'next-intl'; 3 | import React from 'react'; 4 | 5 | interface Props { 6 | imageCount: number, 7 | selectedCount: number, 8 | filteredCount: number, 9 | } 10 | 11 | function GalleryNumbers(props: Props) { 12 | const t = useTranslations('GalleryNumbers'); 13 | 14 | const textParts = [ 15 | t('imageCount', { count: props.imageCount }), 16 | ]; 17 | 18 | if (props.filteredCount) { 19 | textParts.push(t('filteredCount', { count: props.filteredCount })); 20 | } 21 | 22 | if (props.selectedCount) { 23 | textParts.push(t('selectedCount', { count: props.selectedCount })); 24 | } 25 | 26 | return ( 27 | 28 | {textParts.join(t('separator'))} 29 | 30 | ); 31 | } 32 | 33 | export default GalleryNumbers; 34 | -------------------------------------------------------------------------------- /src/components/GlobalAppInit/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { PropsWithChildren, useEffect } from 'react'; 4 | import useFileDrop from '@/hooks/useFileDrop'; 5 | import { useHandleHashParams } from '@/hooks/useHandleHashParams'; 6 | import { useImportExportSettings } from '@/hooks/useImportExportSettings'; 7 | import { useStores } from '@/hooks/useStores'; 8 | import useTrashbin from '@/hooks/useTrashbin'; 9 | import { dropboxStorageTool } from '@/tools/dropboxStorage'; 10 | 11 | function GlobalAppInit({ children }: PropsWithChildren) { 12 | useFileDrop(); 13 | useHandleHashParams(); 14 | 15 | const stores = useStores(); 16 | const { remoteImport } = useImportExportSettings(); 17 | 18 | useEffect(() => { 19 | const { subscribe } = dropboxStorageTool(stores, remoteImport); 20 | // gitStorageTool(remoteImport); 21 | 22 | return subscribe(); 23 | }, [remoteImport, stores]); 24 | 25 | const { checkUpdateTrashCount } = useTrashbin(); 26 | useEffect(() => { 27 | const handle = window.setTimeout(() => { 28 | checkUpdateTrashCount(); 29 | }, 1); 30 | 31 | return () => window.clearTimeout(handle); 32 | }, [checkUpdateTrashCount]); 33 | 34 | return children; 35 | } 36 | 37 | export default GlobalAppInit; 38 | -------------------------------------------------------------------------------- /src/components/ImageLoading/index.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box'; 2 | import Skeleton from '@mui/material/Skeleton'; 3 | import React from 'react'; 4 | import { type Dimensions } from '@/hooks/useImageDimensions'; 5 | 6 | interface Props { 7 | dimensions: Dimensions, 8 | } 9 | 10 | function ImageLoading({ dimensions }: Props) { 11 | const isLandscape = dimensions.width > dimensions.height; 12 | return ( 13 | 20 | 31 | 36 | 37 | 38 | ); 39 | } 40 | 41 | export default ImageLoading; 42 | -------------------------------------------------------------------------------- /src/components/ImageRender/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useImageDimensions } from '@/hooks/useImageDimensions'; 3 | import { type Overrides, useImageRender } from '@/hooks/useImageRender'; 4 | import GameBoyImage from '../GameBoyImage'; 5 | import ImageLoading from '../ImageLoading'; 6 | 7 | interface Props { 8 | hash: string, 9 | asThumb?: boolean, 10 | overrides?: Overrides, 11 | } 12 | 13 | function ImageRender({ hash, asThumb, overrides }: Props) { 14 | const { gbImageProps } = useImageRender(hash, overrides); 15 | const { dimensions } = useImageDimensions(hash); 16 | 17 | return gbImageProps ? ( 18 | 23 | ) : ( 24 | 25 | ); 26 | } 27 | 28 | export default ImageRender; 29 | -------------------------------------------------------------------------------- /src/components/Import/useImport.ts: -------------------------------------------------------------------------------- 1 | import type { ExportTypes } from '@/consts/exportTypes'; 2 | import { useImportExportSettings } from '@/hooks/useImportExportSettings'; 3 | import useImportFile from '@/hooks/useImportFile'; 4 | import useInteractionsStore from '@/stores/interactionsStore'; 5 | import useSettingsStore from '@/stores/settingsStore'; 6 | 7 | interface UseImport { 8 | printerUrl?: string, 9 | printerConnected: boolean, 10 | importPlainText: (textDump: string) => void, 11 | importFiles: (files: File[]) => void, 12 | exportJson: (what: ExportTypes) => void, 13 | } 14 | 15 | export const useImport = (): UseImport => { 16 | const { printerUrl } = useSettingsStore(); 17 | const { printerFunctions } = useInteractionsStore(); 18 | const { downloadSettings } = useImportExportSettings(); 19 | 20 | const fullPrinterUrl = printerUrl ? `${printerUrl}remote.html` : undefined; 21 | const printerConnected = printerFunctions.length > 0; 22 | 23 | const { handleFileImport } = useImportFile(); 24 | 25 | return { 26 | printerUrl: fullPrinterUrl, 27 | printerConnected, 28 | importPlainText: (textDump) => { 29 | const file = new File([...textDump], 'Text input.txt', { type: 'text/plain' }); 30 | handleFileImport([file]); 31 | }, 32 | importFiles: handleFileImport, 33 | exportJson: (what: ExportTypes) => downloadSettings(what), 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/MarkdownStack/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Stack from '@mui/material/Stack'; 4 | import { Theme } from '@mui/system'; 5 | import type { PropsWithChildren } from 'react'; 6 | 7 | export default function MarkdownStack({ children }: PropsWithChildren) { 8 | return ( 9 | ({ 13 | a: { 14 | color: theme.palette.secondary.light, 15 | textDecorationColor: 'currentColor', 16 | 17 | '&:hover': { 18 | color: theme.palette.secondary.main, 19 | }, 20 | }, 21 | code: { 22 | fontFamily: 'monospace', 23 | }, 24 | ul: { 25 | listStyleType: 'disc', 26 | paddingInlineStart: theme.spacing(2), 27 | }, 28 | 'h1,h2,h3,h4,h5,h6,p': { 29 | mb: '0.25em', 30 | }, 31 | 'h1,h2,h3,h4,h5,h6': { 32 | mt: '1.25em', 33 | }, 34 | '& > :first-child': { 35 | mt: 0, 36 | }, 37 | })} 38 | > 39 | {children} 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/components/MuiCleanThemeProvider/index.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider, useTheme } from '@mui/material/styles'; 2 | import type { Theme } from '@mui/system'; 3 | import React, { useMemo, type PropsWithChildren } from 'react'; 4 | 5 | function MuiCleanThemeProvider({ children }: PropsWithChildren) { 6 | const theme = useTheme(); 7 | 8 | // can be removed after https://github.com/mui/mui-x/issues/14684 9 | const cleanTheme = useMemo((): Theme => { 10 | if (!theme.components?.MuiTextField) { 11 | return theme; 12 | } 13 | 14 | const textFieldTheme = { 15 | ...theme.components.MuiTextField, 16 | }; 17 | 18 | if (textFieldTheme.defaultProps?.slotProps?.htmlInput) { 19 | delete textFieldTheme.defaultProps.slotProps.htmlInput; 20 | } 21 | 22 | return ({ 23 | ...theme, 24 | components: { 25 | ...theme.components, 26 | MuiTextField: textFieldTheme, 27 | }, 28 | }); 29 | }, [theme]); 30 | 31 | return ( 32 | 33 | {children} 34 | 35 | ); 36 | } 37 | 38 | export default MuiCleanThemeProvider; 39 | -------------------------------------------------------------------------------- /src/components/Navigation/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import AppBar from '@mui/material/AppBar'; 4 | import Container from '@mui/material/Container'; 5 | import { ThemeProvider } from '@mui/material/styles'; 6 | import Toolbar from '@mui/material/Toolbar'; 7 | import React from 'react'; 8 | import { lightTheme } from '@/styles/themes'; 9 | 10 | function NavigationSkeleton() { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | 22 | export default NavigationSkeleton; 23 | -------------------------------------------------------------------------------- /src/components/Overlays/DownloadOptions/index.tsx: -------------------------------------------------------------------------------- 1 | import Stack from '@mui/material/Stack'; 2 | import { useTranslations } from 'next-intl'; 3 | import React from 'react'; 4 | import Lightbox from '@/components/Lightbox'; 5 | import DownloadOptionsForm from '@/components/Overlays/DownloadOptions/DownloadOptionsForm'; 6 | import useDownload from '@/hooks/useDownload'; 7 | import useInteractionsStore from '@/stores/interactionsStore'; 8 | 9 | function DownloadOptions() { 10 | const t = useTranslations('DownloadOptions'); 11 | const { downloadHashes, setDownloadHashes } = useInteractionsStore(); 12 | const { downloadImages } = useDownload(); 13 | 14 | return ( 15 | { 18 | downloadImages(downloadHashes); 19 | setDownloadHashes([]); 20 | }} 21 | deny={() => setDownloadHashes([])} 22 | > 23 | 27 | 28 | 29 | 30 | ); 31 | } 32 | 33 | export default DownloadOptions; 34 | -------------------------------------------------------------------------------- /src/components/Overlays/DragOver/index.tsx: -------------------------------------------------------------------------------- 1 | import CloudUploadIcon from '@mui/icons-material/CloudUpload'; 2 | import { alpha } from '@mui/material'; 3 | import Paper from '@mui/material/Paper'; 4 | import Stack from '@mui/material/Stack'; 5 | import { useTheme } from '@mui/material/styles'; 6 | import Typography from '@mui/material/Typography'; 7 | import { useTranslations } from 'next-intl'; 8 | import React from 'react'; 9 | 10 | function DragOver() { 11 | const theme = useTheme(); 12 | const t = useTranslations('DragOver'); 13 | 14 | return ( 15 | 29 | 36 | 42 | 43 | 44 | {t('dropFilesHere')} 45 | 46 | 51 | {t('supportedFileTypes')} 52 | 53 | 54 | 55 | 56 | ); 57 | } 58 | 59 | export default DragOver; 60 | -------------------------------------------------------------------------------- /src/components/Overlays/EditRGBN/RGBNPreviewImage.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box'; 2 | import { RGBNTiles } from 'gb-image-decoder'; 3 | import React, { useCallback, useEffect, useState } from 'react'; 4 | import GameBoyImage from '@/components/GameBoyImage'; 5 | import { defaultRGBNPalette } from '@/consts/defaults'; 6 | import useItemsStore from '@/stores/itemsStore'; 7 | import { loadImageTiles as getLoadImageTiles } from '@/tools/loadImageTiles'; 8 | import { type RGBNHashes } from '@/types/Image'; 9 | 10 | interface Props { 11 | rgbnHashes: RGBNHashes; 12 | } 13 | 14 | function RGBNPreviewImage({ rgbnHashes }: Props) { 15 | const [tiles, setTiles] = useState(null); 16 | const { frames: allFrames, images: allImages } = useItemsStore(); 17 | 18 | const loadImageTiles = useCallback( 19 | async (hashesOverride?: RGBNHashes): Promise => { 20 | const imageLoader = getLoadImageTiles(allImages, allFrames); 21 | 22 | return (await imageLoader('', undefined, undefined, hashesOverride) as RGBNTiles); 23 | }, 24 | [allImages, allFrames], 25 | ); 26 | 27 | useEffect(()=> { 28 | const handle = window.setTimeout(async () => { 29 | setTiles(await loadImageTiles(rgbnHashes)); 30 | }, 1); 31 | 32 | return () => window.clearTimeout(handle); 33 | }); 34 | 35 | if (!tiles) { return null; } 36 | 37 | return ( 38 | 39 | 44 | 45 | ); 46 | } 47 | 48 | export default RGBNPreviewImage; 49 | -------------------------------------------------------------------------------- /src/components/Overlays/EditRGBN/Select.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { DialogOption } from '@/types/Dialog'; 3 | 4 | interface Props { 5 | id: string, 6 | label?: string, 7 | disabled: boolean, 8 | value: string, 9 | setSelected: (value: string) => void, 10 | options: DialogOption[], 11 | } 12 | 13 | function Select({ id, label, disabled, value, setSelected, options }: Props) { 14 | return ( 15 |
18 | { label ? ( 19 | 22 | ) : null } 23 | 41 |
42 | ); 43 | } 44 | 45 | // ToDo: sill used? 46 | export default Select; 47 | -------------------------------------------------------------------------------- /src/components/Overlays/FilterForm/FilterFormPalette.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@mui/material/styles'; 2 | import { type Theme } from '@mui/system'; 3 | import { useTranslations } from 'next-intl'; 4 | import React from 'react'; 5 | import PaletteIcon from '@/components/PaletteIcon'; 6 | import { ActiveFilterUpdateMode } from '@/hooks/useFilterForm'; 7 | import { Palette } from '@/types/Palette'; 8 | 9 | interface Props { 10 | palette: Palette, 11 | paletteActive: boolean, 12 | togglePalette: (mode: ActiveFilterUpdateMode) => void, 13 | } 14 | 15 | function FilterFormPalette({ paletteActive, togglePalette, palette }: Props) { 16 | const t = useTranslations('FilterFormPalette'); 17 | const theme: Theme = useTheme(); 18 | 19 | return ( 20 | 29 | ); 30 | } 31 | 32 | export default FilterFormPalette; 33 | -------------------------------------------------------------------------------- /src/components/Overlays/FilterForm/FilterFormTab.tsx: -------------------------------------------------------------------------------- 1 | import Stack from '@mui/material/Stack'; 2 | import React, { type PropsWithChildren } from 'react'; 3 | 4 | export interface Props { 5 | hidden: boolean; 6 | } 7 | 8 | function FilterFormTabPanel({ children, hidden }: Props & PropsWithChildren) { 9 | if (hidden) { return null; } 10 | 11 | return ( 12 | 18 | {children} 19 | 20 | ); 21 | } 22 | 23 | export default FilterFormTabPanel; 24 | -------------------------------------------------------------------------------- /src/components/Overlays/FilterForm/FilterFormTag.tsx: -------------------------------------------------------------------------------- 1 | import Chip from '@mui/material/Chip'; 2 | import { useTranslations } from 'next-intl'; 3 | import React from 'react'; 4 | import { ActiveFilterUpdateMode } from '@/hooks/useFilterForm'; 5 | 6 | interface Props { 7 | title: string, 8 | tagActive: boolean, 9 | toggleTag: (mode: ActiveFilterUpdateMode) => void, 10 | } 11 | 12 | function FilterFormTag({ tagActive, toggleTag, title }: Props) { 13 | const t = useTranslations('FilterFormTag'); 14 | 15 | return ( 16 | toggleTag(tagActive ? ActiveFilterUpdateMode.REMOVE : ActiveFilterUpdateMode.ADD)} 21 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 22 | // @ts-ignore 23 | color={tagActive ? 'tertiary' : 'default'} 24 | /> 25 | ); 26 | } 27 | 28 | export default FilterFormTag; 29 | -------------------------------------------------------------------------------- /src/components/Overlays/LightboxImages/LightBoxImage.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box'; 2 | import Stack from '@mui/material/Stack'; 3 | import React from 'react'; 4 | import ImageRender from '@/components/ImageRender'; 5 | import { useImageDimensions } from '@/hooks/useImageDimensions'; 6 | 7 | export interface LightBoxImageProps { 8 | hash: string, 9 | } 10 | 11 | function LightBoxImage({ hash }: LightBoxImageProps) { 12 | const { dimensions } = useImageDimensions(hash); 13 | return ( 14 | 24 | 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | export default LightBoxImage; 37 | -------------------------------------------------------------------------------- /src/components/Overlays/ProgressBox/index.tsx: -------------------------------------------------------------------------------- 1 | import LinearProgress from '@mui/material/LinearProgress'; 2 | import Stack from '@mui/material/Stack'; 3 | import Typography from '@mui/material/Typography'; 4 | import { useTranslations } from 'next-intl'; 5 | import React from 'react'; 6 | import Lightbox from '@/components/Lightbox'; 7 | import useProgressStore from '@/stores/progressStore'; 8 | 9 | function ProgressBox() { 10 | const t = useTranslations('ProgressBox'); 11 | const { progress } = useProgressStore(); 12 | 13 | return ( 14 | 17 | 21 | { progress.map((progressItem) => ( 22 | 27 | { progressItem.label } 28 | 33 | 34 | )) } 35 | 36 | 37 | ); 38 | } 39 | 40 | export default ProgressBox; 41 | -------------------------------------------------------------------------------- /src/components/Overlays/Serials/index.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslations } from 'next-intl'; 2 | import React from 'react'; 3 | import Lightbox from '@/components/Lightbox'; 4 | import ConnectSerial from '@/components/Overlays/ConnectSerial'; 5 | import useInteractionsStore from '@/stores/interactionsStore'; 6 | 7 | function Serials() { 8 | const t = useTranslations('Serials'); 9 | const { setShowSerials } = useInteractionsStore(); 10 | 11 | return setShowSerials(false)} 14 | deny={() => setShowSerials(false)} 15 | > 16 | 17 | ; 18 | } 19 | 20 | export default Serials; 21 | -------------------------------------------------------------------------------- /src/components/PaginationButton/index.tsx: -------------------------------------------------------------------------------- 1 | import IconButton from '@mui/material/Button'; 2 | import NextLink from 'next/link'; 3 | import { useTranslations } from 'next-intl'; 4 | import React from 'react'; 5 | import { useGalleryTreeContext } from '@/contexts/galleryTree'; 6 | 7 | export interface Props { 8 | children: React.ReactNode, 9 | disabled: boolean, 10 | page: number, 11 | } 12 | 13 | function PaginationButton(props: Props) { 14 | const t = useTranslations('PaginationButton'); 15 | const { getUrl } = useGalleryTreeContext(); 16 | 17 | return ( 18 | 24 | { props.children } 25 | 26 | ); 27 | } 28 | 29 | export default PaginationButton; 30 | -------------------------------------------------------------------------------- /src/components/PaletteIcon/index.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box'; 2 | import React from 'react'; 3 | import { generateGradient, GradientType } from '@/tools/generateGradient'; 4 | 5 | interface Props { 6 | palette: string[], 7 | fontSize?: string, 8 | } 9 | 10 | function PaletteIcon({ 11 | palette, 12 | fontSize, 13 | }: Props) { 14 | return ( 15 | 30 | ); 31 | } 32 | 33 | export default PaletteIcon; 34 | -------------------------------------------------------------------------------- /src/components/PalettePreview/index.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box'; 2 | import Stack from '@mui/material/Stack'; 3 | import React from 'react'; 4 | import ImageRender from '@/components/ImageRender'; 5 | import usePreviewImages from '@/hooks/usePreviewImages'; 6 | 7 | interface Props { 8 | palette: string[], 9 | } 10 | 11 | function PalettePreview({ palette }: Props) { 12 | const { previewImages } = usePreviewImages(); 13 | 14 | return ( 15 | 21 | { 22 | previewImages.map((imageHash) => ( 23 | 30 | 31 | 32 | )) 33 | } 34 | 35 | ); 36 | } 37 | 38 | export default PalettePreview; 39 | -------------------------------------------------------------------------------- /src/components/Remote/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useTranslations } from 'next-intl'; 4 | import React, { ReactNode } from 'react'; 5 | import { ParentType, useRemoteWindow } from '@/hooks/useRemoteWindow'; 6 | 7 | function Remote() { 8 | const t = useTranslations('Remote'); 9 | const { parentType } = useRemoteWindow(); 10 | 11 | const formats = { 12 | p: (chunks: ReactNode) => (

{chunks}

), 13 | em: (chunks: ReactNode) => ({chunks}), 14 | }; 15 | 16 | switch (parentType) { 17 | case ParentType.IFRAME: { 18 | return ( 19 |
20 | {t.rich('iframe', formats)} 21 |
22 | ); 23 | } 24 | 25 | case ParentType.POPUP: { 26 | return ( 27 |
28 | {t.rich('popup', { 29 | ...formats, 30 | host: window.location.host, 31 | })} 32 |
33 | ); 34 | } 35 | 36 | case ParentType.NONE: 37 | default: { 38 | return ( 39 |
40 | {t.rich('noReference', formats)} 41 |
42 | ); 43 | } 44 | } 45 | } 46 | 47 | export default Remote; 48 | -------------------------------------------------------------------------------- /src/components/SettingsPlugins/PluginInputField.tsx: -------------------------------------------------------------------------------- 1 | import TextField from '@mui/material/TextField'; 2 | import React, { useState } from 'react'; 3 | import { ConfigParamType } from '@/consts/plugins'; 4 | 5 | const inputValueFromType = (type: ConfigParamType, value: string): string | number => { 6 | switch (type) { 7 | case ConfigParamType.NUMBER: { 8 | const num = parseFloat(value); 9 | return isNaN(num) ? 0 : num; 10 | } 11 | 12 | case ConfigParamType.STRING: 13 | case ConfigParamType.MULTILINE: 14 | return value || ''; 15 | 16 | default: 17 | return value; 18 | } 19 | }; 20 | 21 | const toStringValue = (value?: string | number): string => { 22 | switch (typeof value) { 23 | case 'string': 24 | return value; 25 | case 'number': 26 | return value.toString(10) || '0'; 27 | default: 28 | return ''; 29 | } 30 | }; 31 | 32 | interface Props { 33 | id: string, 34 | label: string, 35 | type: ConfigParamType, 36 | value?: string | number, 37 | onChange: (value: string | number) => void, 38 | } 39 | 40 | function PluginInputField({ id, label, type, value, onChange }: Props) { 41 | const [fieldValue, setFieldValue] = useState(toStringValue(value)); 42 | 43 | return ( 44 | { 51 | setFieldValue(ev.target.value); 52 | }} 53 | onBlur={() => { 54 | setFieldValue(toStringValue(inputValueFromType(type, fieldValue))); 55 | onChange(inputValueFromType(type, fieldValue)); 56 | }} 57 | /> 58 | ); 59 | } 60 | 61 | export default PluginInputField; 62 | -------------------------------------------------------------------------------- /src/components/StorageWarning/index.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box'; 2 | import LinearProgress from '@mui/material/LinearProgress'; 3 | import Typography from '@mui/material/Typography'; 4 | import bytes from 'bytes'; 5 | import { useTranslations } from 'next-intl'; 6 | import React from 'react'; 7 | import { useStorageInfo } from '@/hooks/useStorageInfo'; 8 | 9 | function StorageWarning() { 10 | const t = useTranslations('StorageWarning'); 11 | const { storageEstimate } = useStorageInfo(); 12 | 13 | if (!storageEstimate) { 14 | return null; 15 | } 16 | 17 | // Get the translated storage type label 18 | const storageType = t(`storageTypes.${storageEstimate.type}`); 19 | 20 | return ( 21 | 25 | 31 | 40 | {t('usageWarning', { 41 | percentage: storageEstimate.percentage, 42 | type: storageType, 43 | })} 44 | 45 | 46 | ); 47 | } 48 | 49 | export default StorageWarning; 50 | -------------------------------------------------------------------------------- /src/components/WebUSBGreeting/EnableWebUSB.tsx: -------------------------------------------------------------------------------- 1 | import FormControlLabel from '@mui/material/FormControlLabel'; 2 | import Switch from '@mui/material/Switch'; 3 | import { useTranslations } from 'next-intl'; 4 | import React from 'react'; 5 | import useSettingsStore from '../../stores/settingsStore'; 6 | 7 | function EnableWebUSB() { 8 | const { useSerials, setUseSerials } = useSettingsStore(); 9 | const t = useTranslations('WebUSBGreeting'); 10 | 11 | return ( 12 | { 18 | setUseSerials(target.checked); 19 | }} 20 | /> 21 | )} 22 | /> 23 | ); 24 | } 25 | 26 | export default EnableWebUSB; 27 | -------------------------------------------------------------------------------- /src/consts/GalleryClickAction.ts: -------------------------------------------------------------------------------- 1 | export enum GalleryClickAction { 2 | SELECT = 'select', 3 | EDIT = 'edit', 4 | VIEW = 'view', 5 | } 6 | 7 | -------------------------------------------------------------------------------- /src/consts/GalleryViews.ts: -------------------------------------------------------------------------------- 1 | export enum GalleryViews { 2 | GALLERY_VIEW_SMALL = 'small', 3 | GALLERY_VIEW_1X = '1x', 4 | GALLERY_VIEW_2X = '2x', 5 | GALLERY_VIEW_MAX = 'max', 6 | PALETTE_VIEW = 'res2x', 7 | } 8 | -------------------------------------------------------------------------------- /src/consts/SavImportOrder.ts: -------------------------------------------------------------------------------- 1 | export enum SavImportOrder { 2 | CART_INDEX = 'CART_INDEX', 3 | RAM_INDEX = 'RAM_INDEX', 4 | } 5 | 6 | interface SavImportOption { 7 | translationKey: string, 8 | value: SavImportOrder, 9 | } 10 | 11 | export const savImportOptions: SavImportOption[] = [ 12 | { 13 | translationKey: 'importOrders.cartIndex', 14 | value: SavImportOrder.CART_INDEX, 15 | }, 16 | { 17 | translationKey: 'importOrders.ramIndex', 18 | value: SavImportOrder.RAM_INDEX, 19 | }, 20 | ]; 21 | -------------------------------------------------------------------------------- /src/consts/SpecialTags.ts: -------------------------------------------------------------------------------- 1 | export enum SpecialTags { 2 | FILTER_UNTAGGED = '__filter:untagged__', 3 | FILTER_NEW = '__filter:new__', 4 | FILTER_MONOCHROME = '__filter:mono__', 5 | FILTER_RGB = '__filter:rgb__', 6 | FILTER_RECENT = '__filter:recent__', 7 | FILTER_FAVOURITE = '__filter:favourite__', 8 | FILTER_COMMENTS = '__filter:comments__', 9 | FILTER_USERNAME = '__filter:username__', 10 | } 11 | 12 | export const specialTags: string[] = [ 13 | SpecialTags.FILTER_UNTAGGED, 14 | SpecialTags.FILTER_NEW, 15 | SpecialTags.FILTER_MONOCHROME, 16 | SpecialTags.FILTER_RGB, 17 | SpecialTags.FILTER_RECENT, 18 | SpecialTags.FILTER_FAVOURITE, 19 | SpecialTags.FILTER_COMMENTS, 20 | SpecialTags.FILTER_USERNAME, 21 | ]; 22 | 23 | export const NEW_PALETTE_SHORT = '__new:palette__'; 24 | -------------------------------------------------------------------------------- /src/consts/batchActionTypes.ts: -------------------------------------------------------------------------------- 1 | import type { MonochromeImage, Image } from '@/types/Image'; 2 | 3 | export enum BatchActionType { 4 | DELETE = 'delete', 5 | ANIMATE = 'animate', 6 | DOWNLOAD = 'download', 7 | EDIT = 'edit', 8 | RGB = 'rgb', 9 | } 10 | 11 | export enum Updatable { 12 | LOCK_FRAME = 'lockFrame', 13 | FRAME = 'frame', 14 | PALETTE = 'palette', 15 | TITLE = 'title', 16 | TAGS = 'tags', 17 | CREATED = 'created', 18 | ROTATION = 'rotation' 19 | } 20 | 21 | export enum UpdatableMonochrome { 22 | INVERT_PALETTE = 'invertPalette', 23 | FRAME_PALETTE = 'framePalette', 24 | INVERT_FRAME_PALETTE = 'invertFramePalette', 25 | } 26 | 27 | export type ImageUpdatable = (keyof Image | keyof MonochromeImage); 28 | 29 | export const UPDATATABLES: ImageUpdatable[] = [ 30 | Updatable.LOCK_FRAME, 31 | Updatable.FRAME, 32 | Updatable.TITLE, 33 | Updatable.TAGS, 34 | Updatable.CREATED, 35 | Updatable.ROTATION, 36 | Updatable.PALETTE, 37 | UpdatableMonochrome.INVERT_PALETTE, 38 | UpdatableMonochrome.FRAME_PALETTE, 39 | UpdatableMonochrome.INVERT_FRAME_PALETTE, 40 | ]; 41 | -------------------------------------------------------------------------------- /src/consts/bitmapQueueSettings.ts: -------------------------------------------------------------------------------- 1 | export enum ImportContrastValue { 2 | WIDER = 'wider', 3 | WIDE = 'wide', 4 | NORMAL = 'normal', 5 | NARROW = 'narrow', 6 | NARROWER = 'narrower', 7 | EMULATOR = 'emulator', 8 | } 9 | 10 | interface ImportContrast { 11 | translationKey: string, 12 | baseValues: number[], 13 | value: ImportContrastValue, 14 | } 15 | 16 | export const contrastSettings: ImportContrast[] = [ 17 | { 18 | translationKey: 'contrastSettings.wider', 19 | baseValues: [0x00, 0x44, 0xBB, 0xFF], 20 | value: ImportContrastValue.WIDER, 21 | }, 22 | { 23 | translationKey: 'contrastSettings.wide', 24 | baseValues: [0x00, 0x55, 0xAA, 0xFF], 25 | value: ImportContrastValue.WIDE, 26 | }, 27 | { 28 | translationKey: 'contrastSettings.normal', 29 | baseValues: [0x33, 0x66, 0x99, 0xCC], 30 | value: ImportContrastValue.NORMAL, 31 | }, 32 | { 33 | translationKey: 'contrastSettings.narrow', 34 | baseValues: [0x45, 0x73, 0xA2, 0xD0], 35 | value: ImportContrastValue.NARROW, 36 | }, 37 | { 38 | translationKey: 'contrastSettings.narrower', 39 | baseValues: [0x55, 0x71, 0x8D, 0xAA], 40 | value: ImportContrastValue.NARROWER, 41 | }, 42 | { 43 | // Using src/assets/images/greys.png for reference 44 | translationKey: 'contrastSettings.emulator', 45 | baseValues: [0x40, 0x90, 0xE0, 0xFF], 46 | value: ImportContrastValue.EMULATOR, 47 | }, 48 | ]; 49 | -------------------------------------------------------------------------------- /src/consts/blendModes.ts: -------------------------------------------------------------------------------- 1 | import { BlendMode } from 'gb-image-decoder'; 2 | 3 | export interface BlendModeLabel { 4 | id: BlendMode, 5 | translationKey: string, 6 | } 7 | 8 | export const blendModeLabels: BlendModeLabel[] = [ 9 | { 10 | id: BlendMode.NORMAL, 11 | translationKey: 'blendModes.normal', 12 | }, 13 | { 14 | id: BlendMode.NORMAL_S, 15 | translationKey: 'blendModes.normalS', 16 | }, 17 | { 18 | id: BlendMode.LIGHTEN, 19 | translationKey: 'blendModes.lighten', 20 | }, 21 | { 22 | id: BlendMode.SCREEN, 23 | translationKey: 'blendModes.screen', 24 | }, 25 | { 26 | id: BlendMode.DODGE, 27 | translationKey: 'blendModes.dodge', 28 | }, 29 | { 30 | id: BlendMode.ADDITION, 31 | translationKey: 'blendModes.addition', 32 | }, 33 | { 34 | id: BlendMode.DARKEN, 35 | translationKey: 'blendModes.darken', 36 | }, 37 | { 38 | id: BlendMode.MULTIPLY, 39 | translationKey: 'blendModes.multiply', 40 | }, 41 | { 42 | id: BlendMode.BURN, 43 | translationKey: 'blendModes.burn', 44 | }, 45 | { 46 | id: BlendMode.OVERLAY, 47 | translationKey: 'blendModes.overlay', 48 | }, 49 | { 50 | id: BlendMode.SOFTLIGHT, 51 | translationKey: 'blendModes.softLight', 52 | }, 53 | { 54 | id: BlendMode.HARDLIGHT, 55 | translationKey: 'blendModes.hardLight', 56 | }, 57 | { 58 | id: BlendMode.DIFFERENCE, 59 | translationKey: 'blendModes.difference', 60 | }, 61 | { 62 | id: BlendMode.EXCLUSION, 63 | translationKey: 'blendModes.exclusion', 64 | }, 65 | ]; 66 | -------------------------------------------------------------------------------- /src/consts/defaults.ts: -------------------------------------------------------------------------------- 1 | import { BlendMode } from 'gb-image-decoder'; 2 | import type { Palette } from '@/types/Palette'; 3 | 4 | export const defaultGreys = [0x00, 0x55, 0xaa, 0xff]; 5 | 6 | export const initLine = '{"command":"INIT"}'; 7 | export const moreLine = '{"command":"DATA", "compressed":0, "more":1}'; 8 | export const finalLine = '{"command":"DATA","compressed":0,"more":0}'; 9 | export const terminatorLine = '{"command":"PRNT","sheets":1,"margin_upper":1,"margin_lower":3,"pallet":228,"density":64 }'; 10 | 11 | export const defaultRGBNPalette = { 12 | r: defaultGreys.slice(), 13 | g: defaultGreys.slice(), 14 | b: defaultGreys.slice(), 15 | n: defaultGreys.slice(), 16 | blend: BlendMode.MULTIPLY, 17 | }; 18 | 19 | export const missingGreyPalette: Palette = { 20 | shortName: '-', 21 | name: 'Missing Palette', 22 | palette: ['#aaaaaa', '#999999', '#888888', '#777777'], 23 | isPredefined: true, 24 | origin: 'technical', 25 | }; 26 | -------------------------------------------------------------------------------- /src/consts/dialog.ts: -------------------------------------------------------------------------------- 1 | export enum DialoqQuestionType { 2 | CHECKBOX = 'checkbox', 3 | SELECT = 'select', 4 | TEXT = 'text', 5 | NUMBER = 'number', 6 | INFO = 'info', 7 | IMAGE = 'image', 8 | META = 'meta', 9 | } 10 | -------------------------------------------------------------------------------- /src/consts/exportFrameModes.ts: -------------------------------------------------------------------------------- 1 | import { ExportFrameMode } from 'gb-image-decoder'; 2 | 3 | const exportFrameModes = [ 4 | { 5 | id: ExportFrameMode.FRAMEMODE_KEEP, 6 | name: 'frameModes.keepFrame', 7 | }, 8 | { 9 | id: ExportFrameMode.FRAMEMODE_CROP, 10 | name: 'frameModes.cropFrame', 11 | }, 12 | { 13 | id: ExportFrameMode.FRAMEMODE_SQUARE_BLACK, 14 | name: 'frameModes.squareBlack', 15 | }, 16 | { 17 | id: ExportFrameMode.FRAMEMODE_SQUARE_WHITE, 18 | name: 'frameModes.squareWhite', 19 | }, 20 | ]; 21 | 22 | export default exportFrameModes; 23 | -------------------------------------------------------------------------------- /src/consts/exportTypes.ts: -------------------------------------------------------------------------------- 1 | 2 | export enum ExportTypes { 3 | ALL = 'all', 4 | JSON_EXPORT = 'json_export', 5 | FRAMES = 'frames', 6 | CURRENT_FRAMEGROUP = 'current_framegroup', 7 | IMAGES = 'images', 8 | SELECTED_IMAGES = 'selected_images', 9 | PALETTES = 'palettes', 10 | PLUGINS = 'plugins', 11 | } 12 | -------------------------------------------------------------------------------- /src/consts/fileNameStyles.ts: -------------------------------------------------------------------------------- 1 | export enum FileNameStyle { 2 | FULL = 'FULL', 3 | TITLE_ONLY = 'TITLE_ONLY', 4 | DATE_TITLE = 'DATE_TITLE', 5 | } 6 | 7 | interface FileNameStyleLabel { 8 | id: FileNameStyle, 9 | name: string, 10 | } 11 | 12 | export const fileNameStyleLabels: FileNameStyleLabel[] = [ 13 | { 14 | id: FileNameStyle.FULL, 15 | name: 'filenameStyles.fullTitle', 16 | }, 17 | { 18 | id: FileNameStyle.DATE_TITLE, 19 | name: 'filenameStyles.dateTitle', 20 | }, 21 | { 22 | id: FileNameStyle.TITLE_ONLY, 23 | name: 'filenameStyles.titleOnly', 24 | }, 25 | ]; 26 | -------------------------------------------------------------------------------- /src/consts/handleLine.ts: -------------------------------------------------------------------------------- 1 | export enum HandleLine { 2 | NEW_LINES = 'NEW_LINES', 3 | IMAGE_COMPLETE = 'IMAGE_COMPLETE', 4 | PARSE_ERROR = 'PARSE_ERROR', 5 | } 6 | -------------------------------------------------------------------------------- /src/consts/paletteSortModes.ts: -------------------------------------------------------------------------------- 1 | export enum PaletteSortMode { 2 | DEFAULT_ASC= 'default_asc', 3 | DEFAULT_DESC = 'default_desc', 4 | USAGE_ASC = 'usage_asc', 5 | USAGE_DESC = 'usage_desc', 6 | NAME_ASC = 'name_asc', 7 | NAME_DESC = 'name_desc', 8 | } 9 | -------------------------------------------------------------------------------- /src/consts/plugins.ts: -------------------------------------------------------------------------------- 1 | export enum CompatibilityActionType { 2 | CONFIRM_ASK = 'CONFIRM_ASK', 3 | CONFIRM_ANSWERED = 'CONFIRM_ANSWERED', 4 | ADD_IMAGES = 'ADD_IMAGES', 5 | IMPORT_FILES = 'IMPORT_FILES', 6 | } 7 | 8 | export enum ConfigParamType { 9 | NUMBER = 'number', 10 | STRING = 'string', 11 | MULTILINE = 'multiline', 12 | } 13 | -------------------------------------------------------------------------------- /src/consts/ports.ts: -------------------------------------------------------------------------------- 1 | export enum PortType { 2 | USB = 'USB', 3 | SERIAL = 'SERIAL', 4 | } 5 | 6 | export enum PortDeviceType { 7 | INACTIVE = 'inactive', // Could not be identified, set to be inactive 8 | PACKET_CAPTURE = 'packet_capture', 9 | SUPER_PRINTER_INTERFACE = 'super_printer_interface', 10 | GBXCART = 'gbxcart', 11 | } 12 | 13 | export const usbDeviceFilters: USBDeviceFilter[] = [ 14 | { vendorId: 0x2341, productId: 0x8036 }, // Arduino Leonardo 15 | { vendorId: 0x2341, productId: 0x8037 }, // Arduino Micro 16 | { vendorId: 0x2341, productId: 0x804d }, // Arduino/Genuino Zero 17 | { vendorId: 0x2341, productId: 0x804e }, // Arduino/Genuino MKR1000 18 | { vendorId: 0x2341, productId: 0x804f }, // Arduino MKRZERO 19 | { vendorId: 0x2341, productId: 0x8050 }, // Arduino MKR FOX 1200 20 | { vendorId: 0x2341, productId: 0x8052 }, // Arduino MKR GSM 1400 21 | { vendorId: 0x2341, productId: 0x8053 }, // Arduino MKR WAN 1300 22 | { vendorId: 0x2341, productId: 0x8054 }, // Arduino MKR WiFi 1010 23 | { vendorId: 0x2341, productId: 0x8055 }, // Arduino MKR NB 1500 24 | { vendorId: 0x2341, productId: 0x8056 }, // Arduino MKR Vidor 4000 25 | { vendorId: 0x2341, productId: 0x8057 }, // Arduino NANO 33 IoT 26 | { vendorId: 0x239A }, // Adafruit Boards! 27 | 28 | // Some devices' vendor IDs (CH380=GBXCart or JoexJR) seem to be blocked by chrome 29 | // { vendorId: 0x0483, productId: 0x5740 }, // JoeyJR? 30 | // { vendorId: 0x1a86, productId: 0x7523 }, // GBXCart 31 | ]; 32 | -------------------------------------------------------------------------------- /src/consts/printerFunction.ts: -------------------------------------------------------------------------------- 1 | export enum PrinterFunction { 2 | TESTFILE = 'testFile', 3 | CHECKPRINTER = 'checkPrinter', 4 | FETCHIMAGES = 'fetchImages', 5 | CLEARPRINTER = 'clearPrinter', 6 | TEAR = 'tear', 7 | } 8 | -------------------------------------------------------------------------------- /src/consts/sync.ts: -------------------------------------------------------------------------------- 1 | export enum SyncDirection { 2 | UP = 'up', 3 | DOWN = 'down', 4 | } 5 | 6 | export enum StorageType { 7 | GIT = 'git', 8 | DROPBOX = 'dropbox', 9 | DROPBOXIMAGES = 'dropboximages', 10 | } 11 | -------------------------------------------------------------------------------- /src/consts/textFieldSlotDefaults.ts: -------------------------------------------------------------------------------- 1 | export const textFieldSlotDefaults = { 2 | htmlInput: { 3 | autoComplete: 'off', 4 | autoCorrect: 'off', 5 | autoCapitalize: 'off', 6 | spellCheck: false, 7 | }, 8 | inputLabel: { 9 | shrink: true, 10 | }, 11 | fullWidth: true, 12 | size: 'small', 13 | }; 14 | -------------------------------------------------------------------------------- /src/consts/theme.ts: -------------------------------------------------------------------------------- 1 | export enum ThemeName { 2 | BRIGHT = 'theme-bright', 3 | DARK = 'theme-dark', 4 | } 5 | -------------------------------------------------------------------------------- /src/contexts/galleryTree/index.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { galleryTreeContext } from '@/contexts/galleryTree/Provider'; 3 | import { type GalleryTreeContextType } from '@/types/galleryTreeContext'; 4 | 5 | export const useGalleryTreeContext = (): GalleryTreeContextType => useContext(galleryTreeContext); 6 | -------------------------------------------------------------------------------- /src/contexts/i18nContext/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import defu from 'defu'; 4 | import { NextIntlClientProvider } from 'next-intl'; 5 | import { PropsWithChildren, useEffect, useState } from 'react'; 6 | import { formats } from '@/i18n/formats'; 7 | import { defaultLocale, locales, shortLocales } from '@/i18n/locales'; 8 | import messagesEn from '@/i18n/messages/en.json'; 9 | import useSettingsStore from '@/stores/settingsStore'; 10 | 11 | function I18nContext({ children }: PropsWithChildren) { 12 | const [locale, setLocale] = useState('en'); 13 | const [messages, setMessages] = useState(messagesEn); 14 | const [timeZone, setTimeZone] = useState('UTC'); 15 | 16 | const { preferredLocale, setPreferredLocale } = useSettingsStore(); 17 | 18 | useEffect(() => { 19 | if (!locales.includes(preferredLocale)) { 20 | setPreferredLocale(defaultLocale); 21 | } 22 | }, [preferredLocale, setPreferredLocale]); 23 | 24 | useEffect(() => { 25 | const set = async () => { 26 | let langFile = preferredLocale.split('-')[0]; 27 | 28 | if (!shortLocales.includes(langFile)) { 29 | langFile = 'en'; 30 | } 31 | 32 | const localeMessages = (await import(`@/i18n/messages/${langFile}.json`)).default; 33 | 34 | setMessages(defu(localeMessages, messagesEn)); 35 | setLocale(preferredLocale); 36 | setTimeZone(Intl.DateTimeFormat().resolvedOptions().timeZone); 37 | }; 38 | 39 | set(); 40 | }, [preferredLocale]); 41 | 42 | return ( 43 | 49 | {children} 50 | 51 | ); 52 | } 53 | 54 | export default I18nContext; 55 | -------------------------------------------------------------------------------- /src/contexts/navigationTools/NavigationToolsProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { createContext, useContext } from 'react'; 4 | import type { PropsWithChildren } from 'react'; 5 | import { type UseNavigationTools, useNavigationTools } from './index'; 6 | 7 | const NavigationToolsContext = createContext(null); 8 | 9 | export function NavigationToolsProvider({ children }: PropsWithChildren) { 10 | const value = useNavigationTools(); 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | } 17 | 18 | export const useNavigationToolsContext = () => { 19 | const context = useContext(NavigationToolsContext); 20 | if (!context) { 21 | throw new Error('useNavigationToolsContext must be used within an NavigationToolsProvider'); 22 | } 23 | 24 | return context; 25 | }; 26 | -------------------------------------------------------------------------------- /src/contexts/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | import type { Context } from 'react'; 3 | import { PluginsContext } from '@/types/Plugin'; 4 | 5 | export const pluginsContext: Context = createContext({ 6 | runWithImage: async () => { throw new Error('Plugin Context is missing'); }, 7 | runWithImages: async () => { throw new Error('Plugin Context is missing'); }, 8 | validateAndAddPlugin: async () => { throw new Error('Plugin Context is missing'); }, 9 | }); 10 | 11 | export const usePluginsContext = (): PluginsContext => useContext(pluginsContext); 12 | -------------------------------------------------------------------------------- /src/contexts/ports/index.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | import type { Context } from 'react'; 3 | import { PortsContextValue } from '@/types/ports'; 4 | 5 | export const portsContext: Context = createContext({ 6 | connectedDevices: [], 7 | isReceiving: false, 8 | webSerialEnabled: false, 9 | openWebSerial: () => { throw new Error('PortsContext is missing'); }, 10 | webUSBEnabled: false, 11 | openWebUSB: () => { throw new Error('PortsContext is missing'); }, 12 | unknownDeviceResponse: null, 13 | hasInactiveDevices: false, 14 | }); 15 | 16 | export const usePortsContext = (): PortsContextValue => useContext(portsContext); 17 | -------------------------------------------------------------------------------- /src/contexts/remotePrinter/index.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | import type { Context } from 'react'; 3 | import type { PrinterFunction } from '@/consts/printerFunction'; 4 | 5 | export interface RemotePrinterContext { 6 | callRemoteFunction: (functionType: PrinterFunction) => Promise, 7 | } 8 | 9 | export const remotePrinterContext: Context = createContext({ 10 | callRemoteFunction: async () => { /**/ }, 11 | }); 12 | 13 | export const useRemotePrinterContext = (): RemotePrinterContext => ( 14 | useContext(remotePrinterContext) 15 | ); 16 | -------------------------------------------------------------------------------- /src/hooks/useAsPasswordField.tsx: -------------------------------------------------------------------------------- 1 | import Visibility from '@mui/icons-material/Visibility'; 2 | import VisibilityOff from '@mui/icons-material/VisibilityOff'; 3 | import IconButton from '@mui/material/IconButton'; 4 | import { useTranslations } from 'next-intl'; 5 | import React, { useMemo, useState } from 'react'; 6 | import type { ReactNode } from 'react'; 7 | 8 | interface UseAsPasswordField { 9 | type: 'text' | 'password', 10 | setShowPassword: (show: boolean) => void, 11 | button: ReactNode, 12 | } 13 | 14 | export const useAsPasswordField = (): UseAsPasswordField => { 15 | const [showPassword, setShowPassword] = useState(false); 16 | const t = useTranslations('useAsPasswordField'); 17 | 18 | const button = useMemo(() => ( 19 | { 24 | setShowPassword(!showPassword); 25 | }} 26 | > 27 | {showPassword ? : } 28 | 29 | ), [t, showPassword]); 30 | 31 | const type = useMemo(() => ( 32 | showPassword ? 'text' : 'password' 33 | ), [showPassword]); 34 | 35 | return { 36 | type, 37 | button, 38 | setShowPassword, 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /src/hooks/useAvailableTags.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { SpecialTags } from '@/consts/SpecialTags'; 3 | import useItemsStore from '@/stores/itemsStore'; 4 | import type { Image } from '@/types/Image'; 5 | 6 | export const getAvailableTags = (images: Image[]): string[] => { 7 | const tagSet = new Set(); 8 | 9 | for (const { tags } of images) { 10 | for (const tag of tags) { 11 | if (tag !== SpecialTags.FILTER_FAVOURITE) { 12 | tagSet.add(tag); 13 | } 14 | } 15 | } 16 | 17 | return Array.from(tagSet).sort((a, b) => ( 18 | a.toLowerCase().localeCompare(b.toLowerCase()) 19 | )); 20 | }; 21 | 22 | export interface UseAvailableTags { 23 | availableTags: string[], 24 | } 25 | 26 | export const useAvailableTags = (): UseAvailableTags => { 27 | const { images } = useItemsStore(); 28 | const [availableTags, setAvailableTags] = useState(getAvailableTags(images)); 29 | 30 | useEffect(() => { 31 | setAvailableTags(getAvailableTags(images)); 32 | }, [images]); 33 | 34 | return { availableTags }; 35 | }; 36 | -------------------------------------------------------------------------------- /src/hooks/useDateFormat.ts: -------------------------------------------------------------------------------- 1 | import { useFormatter } from 'next-intl'; 2 | import { useCallback } from 'react'; 3 | import useSettingsStore from '@/stores/settingsStore'; 4 | 5 | interface UseDateFormat { 6 | formatterGallery: (date: string) => string | null; 7 | formatter: (date: number |string | Date) => string; 8 | } 9 | 10 | export const useDateFormat = (): UseDateFormat => { 11 | const { hideDates } = useSettingsStore(); 12 | 13 | const format = useFormatter(); 14 | 15 | const formatter = useCallback((date: number | string | Date) => { 16 | let dateObject = new Date(date); 17 | 18 | if (typeof date === 'string' && isNaN(dateObject.getTime())) { 19 | const fixed = date.replace(/(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}):(\d{3})/, '$1T$2.$3'); 20 | dateObject = new Date(fixed); 21 | } 22 | 23 | return format.dateTime(dateObject, 'short'); 24 | }, [format]); 25 | 26 | const formatterGallery = useCallback((date: string) => { 27 | if (hideDates || !date) { 28 | return null; 29 | } 30 | 31 | return formatter(date); 32 | }, [formatter, hideDates]); 33 | 34 | return { 35 | formatterGallery, 36 | formatter, 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /src/hooks/useDebugValueChange.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export const useDebugValueChange = (value: T, label: string = 'Value') => { 4 | const prevRef = useRef(undefined); 5 | 6 | useEffect(() => { 7 | if (prevRef.current !== undefined && prevRef.current !== value) { 8 | console.log(`---- [${label}] changed`); 9 | console.log('---- Previous:', prevRef.current); 10 | console.log('---- Current:', value); 11 | } 12 | prevRef.current = value; 13 | }, [label, value]); 14 | }; 15 | -------------------------------------------------------------------------------- /src/hooks/useDropboxSettings.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { useImportExportSettings } from '@/hooks/useImportExportSettings'; 3 | import { useStores } from '@/hooks/useStores'; 4 | import useStoragesStore from '@/stores/storagesStore'; 5 | import { dropboxStorageTool } from '@/tools/dropboxStorage'; 6 | import type { DropBoxSettings } from '@/types/Sync'; 7 | 8 | interface UseDropboxSettings { 9 | use: boolean, 10 | loggedIn: boolean, 11 | path: string, 12 | autoDropboxSync: boolean, 13 | debugText: string, 14 | logout: () => void, 15 | setDropboxStorage: (dropboxStorage: DropBoxSettings) => void, 16 | startAuth: () => void, 17 | } 18 | 19 | export const useDropboxSettings = (): UseDropboxSettings => { 20 | const stores = useStores(); 21 | const { remoteImport } = useImportExportSettings(); 22 | const { dropboxStorage, dropboxLogout, setDropboxStorage } = useStoragesStore(); 23 | 24 | const debugText = useMemo(() => { 25 | const debugObject: Record = { ...dropboxStorage }; 26 | debugObject.refreshToken = `length: ${dropboxStorage.refreshToken?.length.toString(10) || null}`; 27 | debugObject.accessToken = `length: ${dropboxStorage.accessToken?.length.toString(10) || null}`; 28 | return ( 29 | JSON.stringify(debugObject, null, 2) 30 | ); 31 | }, [dropboxStorage]); 32 | 33 | return { 34 | use: !!dropboxStorage.use, 35 | loggedIn: !!dropboxStorage.refreshToken, 36 | path: dropboxStorage.path || '', 37 | autoDropboxSync: dropboxStorage?.autoDropboxSync || false, 38 | debugText, 39 | logout: dropboxLogout, 40 | setDropboxStorage, 41 | startAuth: () => ( 42 | dropboxStorageTool(stores, remoteImport).startAuth() 43 | ), 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /src/hooks/useGalleryGroup.ts: -------------------------------------------------------------------------------- 1 | import { useGalleryTreeContext } from '@/contexts/galleryTree'; 2 | import type { TreeImageGroup } from '@/types/ImageGroup'; 3 | 4 | interface UseGalleryGroup { 5 | group: TreeImageGroup | null, 6 | path: string | null, 7 | } 8 | 9 | export const useGalleryGroup = (hash: string): UseGalleryGroup => { 10 | const { paths, view } = useGalleryTreeContext(); 11 | 12 | const group: TreeImageGroup | null = view.groups.find(({ coverImage }) => coverImage === hash) || null; 13 | 14 | const path: string | null = paths.find(({ group: { id } }) => (id === group?.id))?.absolutePath || null; 15 | 16 | return { group, path }; 17 | }; 18 | -------------------------------------------------------------------------------- /src/hooks/useIdle.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export function useIdle(): boolean { 4 | const [isIdle, setIsIdle] = useState(false); 5 | 6 | useEffect(() => { 7 | if (typeof window === 'undefined') return; 8 | 9 | if (typeof window.requestIdleCallback === 'function') { 10 | const idleId = requestIdleCallback(() => setIsIdle(true)); 11 | return () => { 12 | window.cancelIdleCallback(idleId); 13 | }; 14 | } 15 | 16 | const timeout = window.setTimeout(() => setIsIdle(true), 1000); 17 | return () => { 18 | window.clearTimeout(timeout); 19 | }; 20 | }, []); 21 | 22 | return isIdle; 23 | } 24 | -------------------------------------------------------------------------------- /src/hooks/useIframeLoaded.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import useInteractionsStore from '@/stores/interactionsStore'; 3 | import useSettingsStore from '@/stores/settingsStore'; 4 | 5 | 6 | export interface UseIframeLoaded { 7 | failed: boolean, 8 | loaded: boolean, 9 | printerUrl?: string, 10 | printerConnected: boolean, 11 | } 12 | 13 | const useIframeLoaded = (timeout: number): UseIframeLoaded => { 14 | const { printerUrl, printerParams } = useSettingsStore(); 15 | 16 | const encodedPrinterParams = printerParams ? `#${encodeURI(printerParams)}` : ''; 17 | const fullPrinterUrl = printerUrl ? `${printerUrl}remote.html${encodedPrinterParams}` : undefined; 18 | 19 | const { printerFunctions } = useInteractionsStore(); 20 | const printerConnected = printerFunctions.length > 0; 21 | 22 | const [loaded, setLoaded] = useState(false); 23 | const [failed, setFailed] = useState(false); 24 | const timer = useRef(undefined); 25 | 26 | useEffect(() => { 27 | if (!loaded && !failed && !timer.current) { 28 | timer.current = window.setTimeout(() => { 29 | setFailed(true); 30 | }, timeout); 31 | } 32 | 33 | return () => { 34 | window.clearTimeout(timer.current); 35 | timer.current = undefined; 36 | }; 37 | }, [failed, loaded, timeout]); 38 | 39 | if (!loaded && printerConnected) { 40 | window.clearTimeout(timer.current); 41 | timer.current = undefined; 42 | setLoaded(true); 43 | } 44 | 45 | return { 46 | failed, 47 | loaded, 48 | printerUrl: fullPrinterUrl, 49 | printerConnected, 50 | }; 51 | }; 52 | 53 | export default useIframeLoaded; 54 | -------------------------------------------------------------------------------- /src/hooks/useImportFile.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from 'react'; 2 | import useInteractionsStore from '@/stores/interactionsStore'; 3 | import getHandleFileImport, { type HandeFileImportFn, type HandeFileImportOptions } from '@/tools/getHandleFileImport'; 4 | import { useImportExportSettings } from './useImportExportSettings'; 5 | 6 | interface UseImportFile { 7 | handleFileImport: HandeFileImportFn, 8 | } 9 | 10 | const useImportFile = (): UseImportFile => { 11 | const { setError } = useInteractionsStore.getState(); 12 | const { jsonImport } = useImportExportSettings(); 13 | 14 | const handleFileImportJson = useMemo(() => getHandleFileImport(jsonImport), [jsonImport]); 15 | 16 | const handleFileImport = useCallback(async (files: File[], options?: HandeFileImportOptions): Promise => { 17 | try { 18 | await handleFileImportJson(files, options); 19 | } catch (error) { 20 | setError(error as Error); 21 | } 22 | }, [handleFileImportJson, setError]); 23 | 24 | return { 25 | handleFileImport, 26 | }; 27 | }; 28 | 29 | 30 | export default useImportFile; 31 | -------------------------------------------------------------------------------- /src/hooks/useImportPlainText.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import useImportFile from '@/hooks/useImportFile'; 3 | 4 | const useImportPlainText = () => { 5 | const { handleFileImport } = useImportFile(); 6 | const importPlainText = useCallback((textDump: string) => { 7 | const file = new File([...textDump], 'Text input.txt', { type: 'text/plain' }); 8 | 9 | handleFileImport([file]); 10 | }, [handleFileImport]); 11 | 12 | return importPlainText; 13 | }; 14 | 15 | export default useImportPlainText; 16 | -------------------------------------------------------------------------------- /src/hooks/useOverlayGlobalKeys.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | export interface OverlayGlobalKeysParams { 4 | confirm?: () => void, 5 | canConfirm: boolean, 6 | deny?: () => void, 7 | } 8 | 9 | const useOverlayGlobalKeys = ({ 10 | confirm, 11 | canConfirm, 12 | deny, 13 | }: OverlayGlobalKeysParams): void => { 14 | useEffect(() => { 15 | const listener = (ev: KeyboardEvent) => { 16 | if (ev.key === 'Escape') { 17 | if (deny) { 18 | deny(); 19 | } 20 | } 21 | 22 | if (ev.key === 'Enter' && ev.ctrlKey) { 23 | if (confirm && canConfirm) { 24 | confirm(); 25 | } 26 | } 27 | }; 28 | 29 | document.addEventListener('keydown', listener); 30 | 31 | return () => { 32 | document.removeEventListener('keydown', listener); 33 | }; 34 | }, [confirm, canConfirm, deny]); 35 | }; 36 | 37 | export default useOverlayGlobalKeys; 38 | -------------------------------------------------------------------------------- /src/hooks/usePalette.ts: -------------------------------------------------------------------------------- 1 | import { useTranslations } from 'next-intl'; 2 | import useEditPalette from '@/hooks/useSetEditPalette'; 3 | import { useStores } from '@/hooks/useStores'; 4 | import useDialogsStore from '@/stores/dialogsStore'; 5 | import useItemsStore from '@/stores/itemsStore'; 6 | import useSettingsStore from '@/stores/settingsStore'; 7 | 8 | interface UsePalette { 9 | isActive: boolean 10 | setActive: () => void, 11 | deletePalette: () => void, 12 | editPalette: () => void, 13 | clonePalette: () => void, 14 | } 15 | 16 | export const usePalette = (shortName: string, name: string): UsePalette => { 17 | const t = useTranslations('usePalette'); 18 | const { activePalette, setActivePalette } = useSettingsStore(); 19 | const { dismissDialog, setDialog } = useDialogsStore(); 20 | const { updateLastSyncLocalNow } = useStores(); 21 | const { deletePalette } = useItemsStore(); 22 | const { editPalette, clonePalette } = useEditPalette(); 23 | const isActive = activePalette === shortName; 24 | 25 | 26 | return { 27 | isActive, 28 | setActive: () => setActivePalette(shortName), 29 | deletePalette: () => { 30 | setDialog({ 31 | message: t('deletePaletteMessage', { name: name || 'no name' }), 32 | confirm: async () => { 33 | if (isActive) { 34 | setActivePalette('dsh'); 35 | } 36 | 37 | updateLastSyncLocalNow(); 38 | deletePalette(shortName); 39 | dismissDialog(0); 40 | }, 41 | deny: async () => dismissDialog(0), 42 | }); 43 | }, 44 | editPalette: () => editPalette(shortName), 45 | clonePalette: () => clonePalette(shortName), 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /src/hooks/usePrinter.ts: -------------------------------------------------------------------------------- 1 | import type { PrinterFunction } from '@/consts/printerFunction'; 2 | import { useRemotePrinterContext } from '@/contexts/remotePrinter'; 3 | import useInteractionsStore from '@/stores/interactionsStore'; 4 | import type { PrinterInfo } from '@/types/Printer'; 5 | 6 | interface UsePrinter { 7 | printerData: PrinterInfo | null, 8 | printerFunctions: PrinterFunction[], 9 | printerConnected: boolean, 10 | printerBusy: boolean, 11 | callRemoteFunction: (name: PrinterFunction) => void, 12 | } 13 | 14 | export const usePrinter = (): UsePrinter => { 15 | const { printerData, printerFunctions, printerBusy } = useInteractionsStore(); 16 | const { callRemoteFunction } = useRemotePrinterContext(); 17 | 18 | const printerConnected = printerFunctions.length > 0; 19 | 20 | return { 21 | printerData, 22 | printerFunctions, 23 | printerBusy, 24 | printerConnected, 25 | callRemoteFunction, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/hooks/useProcessMarkdownLinks.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | 3 | export default function useProcessMarkdownLinks(markdown: string): string { 4 | return useMemo(() => { 5 | const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ''; 6 | 7 | return markdown.replace(/\[([^\]]+)]\(\/([^)]*)\)/g, (_, linkText, path) => { 8 | if (path === '') { 9 | // Handle root path "/" 10 | return `[${linkText}](${basePath}/)`; 11 | } else { 12 | // Handle paths like "/import", "/gallery", etc. 13 | return `[${linkText}](${basePath}/${path})`; 14 | } 15 | }); 16 | }, [markdown]); 17 | } 18 | -------------------------------------------------------------------------------- /src/hooks/useProgressLog.ts: -------------------------------------------------------------------------------- 1 | import useProgressStore, { type LogItem } from '@/stores/progressStore'; 2 | import useStoragesStore from '@/stores/storagesStore'; 3 | 4 | interface UseProgressLog { 5 | git: { 6 | messages: LogItem[], 7 | repoUrl: string, 8 | repo?: string, 9 | branch?: string, 10 | }, 11 | dropbox: { 12 | messages: LogItem[], 13 | path: string, 14 | }, 15 | confirm: () => void, 16 | } 17 | 18 | export const useProgressLog = (): UseProgressLog => { 19 | const { progressLog, resetProgressLog } = useProgressStore(); 20 | const { gitStorage, dropboxStorage } = useStoragesStore(); 21 | 22 | return { 23 | git: { 24 | messages: progressLog.git || [], 25 | repoUrl: `https://github.com/${gitStorage.owner}/${gitStorage.repo}/tree/${gitStorage.branch}`, 26 | repo: gitStorage.repo, 27 | branch: gitStorage.branch, 28 | }, 29 | dropbox: { 30 | messages: progressLog.dropbox || [], 31 | path: dropboxStorage.path || '', 32 | }, 33 | confirm: resetProgressLog, 34 | }; 35 | }; 36 | 37 | -------------------------------------------------------------------------------- /src/hooks/useSaveRGBNImages.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { defaultRGBNPalette } from '@/consts/defaults'; 3 | import { useStores } from '@/hooks/useStores'; 4 | import useFiltersStore from '@/stores/filtersStore'; 5 | import { toCreationDate } from '@/tools/toCreationDate'; 6 | import type { RGBNHashes, RGBNImage } from '@/types/Image'; 7 | 8 | 9 | interface UseSaveRGBNImages { 10 | saveRGBNImage: (hashes: RGBNHashes[]) => Promise, 11 | } 12 | 13 | const useSaveRGBNImages = (): UseSaveRGBNImages => { 14 | const { setImageSelection } = useFiltersStore(); 15 | const { addImages } = useStores(); 16 | 17 | const saveRGBNImage = useCallback(async (hashes: RGBNHashes[]): Promise => { 18 | const { default: hash } = await import(/* webpackChunkName: "obh" */ 'object-hash'); 19 | 20 | const now = Date.now(); 21 | 22 | const images = hashes.map((rgbnHashes: RGBNHashes, index: number): RGBNImage => { 23 | const image: RGBNImage = { 24 | palette: defaultRGBNPalette, 25 | hashes: rgbnHashes, 26 | hash: hash(rgbnHashes), 27 | created: toCreationDate(now + index), 28 | title: '', 29 | tags: [], 30 | }; 31 | 32 | return image; 33 | }); 34 | 35 | addImages(images); 36 | setImageSelection(images.map((i) => i.hash)); 37 | }, [addImages, setImageSelection]); 38 | 39 | return { saveRGBNImage }; 40 | }; 41 | 42 | export default useSaveRGBNImages; 43 | -------------------------------------------------------------------------------- /src/hooks/useScreenDimensions.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export interface ScreenDimensions { 4 | ddpx: number, 5 | width: number, 6 | height: number, 7 | layoutWidth: number, 8 | } 9 | 10 | const getLayoutWidth = (windowWidth: number): number => { 11 | const layoutPadding = 40; 12 | 13 | if (windowWidth > 1195 + 25) { 14 | return 1195 - layoutPadding; 15 | } 16 | 17 | if (windowWidth > 1000 + 25) { 18 | return 1000 - layoutPadding; 19 | } 20 | 21 | if (windowWidth > 805 + 25) { 22 | return 805 - layoutPadding; 23 | } 24 | 25 | if (windowWidth > 610 + 25) { 26 | return 610 - layoutPadding; 27 | } 28 | 29 | if (windowWidth > 415 + 25) { 30 | return 415 - layoutPadding; 31 | } 32 | 33 | return windowWidth - layoutPadding; 34 | }; 35 | 36 | const getScreenDimensions = (): ScreenDimensions => { 37 | if (typeof window !== 'undefined') { 38 | return ({ 39 | width: window.innerWidth, 40 | height: window.innerHeight, 41 | ddpx: window.devicePixelRatio <= 2 ? window.devicePixelRatio : window.devicePixelRatio / 2, 42 | layoutWidth: getLayoutWidth(window.innerWidth), 43 | }); 44 | } 45 | 46 | return ({ 47 | width: Infinity, 48 | height: Infinity, 49 | ddpx: 1, 50 | layoutWidth: getLayoutWidth(Infinity), 51 | }); 52 | }; 53 | 54 | export const useScreenDimensions = (): ScreenDimensions => { 55 | const [dimensions, setDimensions] = useState(getScreenDimensions()); 56 | 57 | useEffect(() => { 58 | const resizeHandler = () => { 59 | setDimensions(getScreenDimensions()); 60 | }; 61 | 62 | window.addEventListener('resize', resizeHandler); 63 | return () => window.removeEventListener('resize', resizeHandler); 64 | }, [setDimensions]); 65 | 66 | return dimensions; 67 | }; 68 | -------------------------------------------------------------------------------- /src/hooks/useSortForm.ts: -------------------------------------------------------------------------------- 1 | import useFiltersStore from '@/stores/filtersStore'; 2 | import { type SortDirection } from '@/tools/sortby'; 3 | 4 | interface UseSortForm { 5 | visible: boolean, 6 | sortBy: string, 7 | sortOrder: SortDirection, 8 | setSortBy: (sortBy: string) => void, 9 | hideSortForm: () => void, 10 | } 11 | 12 | export const useSortForm = (): UseSortForm => { 13 | const { setSortBy, setSortOptionsVisible, sortOptionsVisible, sortBy: stateSortBy } = useFiltersStore(); 14 | 15 | const [sortBy = '', sortOrder = ''] = stateSortBy.split('_'); 16 | 17 | return { 18 | visible: sortOptionsVisible, 19 | sortBy, 20 | sortOrder: sortOrder as SortDirection, 21 | setSortBy, 22 | hideSortForm: () => setSortOptionsVisible(false), 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /src/hooks/useStoragePersist.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export enum PersistState { 4 | PERSISTED = 'persisted', 5 | NOT_PERSISTED = 'not-persisted', 6 | FAILED = 'failed', 7 | NO_API = 'no_api', 8 | } 9 | 10 | interface UseStoragePersist { 11 | persistAPIAvailable: boolean, 12 | persisted: PersistState, 13 | requestPersist: () => void, 14 | } 15 | 16 | const persistAPIAvailable = !!(typeof navigator !== 'undefined' && navigator.storage && navigator.storage.persist); 17 | 18 | const getPersistState = async (set: (state: PersistState) => void) => { 19 | if (persistAPIAvailable) { 20 | const success = await navigator.storage.persisted(); 21 | set(success ? PersistState.PERSISTED : PersistState.NOT_PERSISTED); 22 | } else { 23 | set(PersistState.NO_API); 24 | } 25 | }; 26 | 27 | const setPersistState = async (set: (state: PersistState) => void) => { 28 | if (persistAPIAvailable) { 29 | const success = await navigator.storage.persist(); 30 | set(success ? PersistState.PERSISTED : PersistState.FAILED); 31 | } else { 32 | set(PersistState.NO_API); 33 | } 34 | }; 35 | 36 | const useStoragePersist = (): UseStoragePersist => { 37 | const [persisted, setPersisted] = useState(PersistState.NOT_PERSISTED); 38 | 39 | useEffect(() => { 40 | getPersistState(setPersisted); 41 | }, []); 42 | 43 | const requestPersist = async () => { 44 | setPersistState(setPersisted); 45 | }; 46 | 47 | return { 48 | persistAPIAvailable, 49 | persisted, 50 | requestPersist, 51 | }; 52 | }; 53 | 54 | export default useStoragePersist; 55 | -------------------------------------------------------------------------------- /src/hooks/useVideoForm.ts: -------------------------------------------------------------------------------- 1 | import useInteractionsStore from '@/stores/interactionsStore'; 2 | import useSettingsStore from '@/stores/settingsStore'; 3 | import { createAnimation, videoParamsWithDefaults } from '@/tools/createAnimation'; 4 | import type { VideoParams } from '@/types/VideoParams'; 5 | 6 | interface UseVideoForm { 7 | imageCount: number, 8 | videoParams: VideoParams, 9 | update: (params: Partial) => void, 10 | cancel: () => void, 11 | animate: () => void, 12 | } 13 | 14 | export const useVideoForm = (): UseVideoForm => { 15 | const { videoParams: stateVideoParams, setVideoParams } = useSettingsStore(); 16 | const { videoSelection, setVideoSelection } = useInteractionsStore(); 17 | const videoParams = videoParamsWithDefaults(stateVideoParams); 18 | const imageCount = videoSelection.length || 0; 19 | 20 | return { 21 | imageCount, 22 | videoParams, 23 | update: setVideoParams, 24 | cancel: () => { 25 | setVideoSelection([]); // Hide dialog 26 | }, 27 | animate: () => { 28 | createAnimation(); 29 | setVideoSelection([]); // Hide dialog 30 | }, 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /src/i18n/formats.ts: -------------------------------------------------------------------------------- 1 | import { Formats } from 'next-intl'; 2 | 3 | export const formats: Formats = { 4 | dateTime: { 5 | full: { 6 | day: '2-digit', 7 | month: '2-digit', 8 | year: 'numeric', 9 | hour: '2-digit', 10 | minute: '2-digit', 11 | second: '2-digit', 12 | hourCycle: 'h24', 13 | }, 14 | short: { 15 | day: '2-digit', 16 | month: '2-digit', 17 | year: 'numeric', 18 | hour: '2-digit', 19 | minute: '2-digit', 20 | hourCycle: 'h24', 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/i18n/locales.ts: -------------------------------------------------------------------------------- 1 | import unique from '@/tools/unique'; 2 | 3 | export const defaultLocale = 'en-GB'; 4 | 5 | export const locales = [ 6 | 'de-DE', 7 | 'en-CA', 8 | defaultLocale, 9 | 'en-US', 10 | 'fr-FR', 11 | ]; 12 | 13 | export const shortLocales = unique((locales.map((locale: string) => ( 14 | locale.split('-')[0] 15 | )))); 16 | -------------------------------------------------------------------------------- /src/i18n/markdown/WebUSB/en.md: -------------------------------------------------------------------------------- 1 | ## Using a WebUSB or WebSerial device: 2 | After connecting a WebUSB or WebSerial device, you can continue to the [Gallery Page](/gallery) or [Home Page](/) now. 3 | The USB-Symbol will pulse as soon as data is being received. 4 | You can also access these options via the USB-Symbol in the navigation and the [Settings Page](/settings). 5 | 6 | ## Note: WebUSB and Web Serial are similar but not the same: 7 | * While Web Serial has access to a regular COM-port and does not require special devices, the feature is not available on mobile devices. 8 | * WebUSB requires certain microcontrollers where the processor has direct access to the USB interface (e.g. an Arduino Leonardo). See [webusb for arduino on GitHub](https://github.com/webusb/arduino) for more information. 9 | 10 | ## Supported Projects 11 | This gallery web app currently supports two specific serial devices dedicated to the GameBoy Camera: 12 | * Receiving tiledata and captured packages printed from a physical GameBoy through the [Arduino Gameboy Printer Emulator](https://github.com/mofosyne/arduino-gameboy-printer-emulator/) 13 | * Sending tiledata to be printed on a physical GameBoy Printer through the [Super Printer Interface](https://github.com/Raphael-Boichot/Yet-another-PC-to-Game-Boy-Printer-interface/) 14 | 15 | ## Recommended Devices (microcontrollers) 16 | Most generic Arduinos (e.g. "Arduino Nano") should work without problems if you're working on a desktop PC. 17 | Being able to communicate with a serial device on Android phones as well, requires a device programmed with WebUSB enabled (e.g. an "Arduino Leonardo" - aka "Arduino Pro Micro"). 18 | Neither WebUSB nor WebSerial is available when you're using Firefox or any iOS device. 19 | -------------------------------------------------------------------------------- /src/i18n/request.ts: -------------------------------------------------------------------------------- 1 | import { getRequestConfig } from 'next-intl/server'; 2 | import messagesEn from '@/i18n/messages/en.json'; 3 | 4 | export default getRequestConfig(async () => { 5 | return { 6 | locale: 'en', 7 | messages: messagesEn, 8 | timeZone: 'UTC', 9 | }; 10 | }); 11 | -------------------------------------------------------------------------------- /src/scss/generic.scss: -------------------------------------------------------------------------------- 1 | html { 2 | @supports (scrollbar-width: auto) { 3 | --scrollbar-color: #b2b2b2; 4 | --scrollbar-track-color: #cccccc; 5 | 6 | &.theme-dark { 7 | --scrollbar-color: #4c4c4c; 8 | --scrollbar-track-color: #333333; 9 | } 10 | 11 | scrollbar-gutter: stable; 12 | scrollbar-color: var(--scrollbar-color) var(--scrollbar-track-color); 13 | scrollbar-width: thin; 14 | } 15 | } 16 | 17 | body { 18 | font-family: "Trebuchet MS", Helvetica, sans-serif; 19 | 20 | line-height: 1.3em; 21 | min-width: 340px; 22 | overflow-y: scroll; 23 | 24 | &.has-overlay { 25 | position: fixed; 26 | width: 100vw; 27 | } 28 | } 29 | 30 | pre { 31 | font-family: monospace; 32 | font-size: 14px; 33 | height: auto; 34 | overflow-y: scroll; 35 | display: block; 36 | flex-grow: 1; 37 | } 38 | 39 | .init-error { 40 | margin: 60px; 41 | padding: 20px 30px; 42 | border: 8px solid; 43 | overflow-x: auto; 44 | 45 | border-color: #292037; 46 | background-color: #4c4c4c; 47 | color: #b2b2b2; 48 | 49 | html.theme-dark & { 50 | border-color: #455c63; 51 | background-color: #b2b2b2; 52 | color: #333333; 53 | } 54 | 55 | @media (max-width: 570px) { 56 | margin: 60px 10px; 57 | padding: 20px 10px; 58 | } 59 | 60 | small { 61 | display: block; 62 | font-size: 12px; 63 | line-height: 16px; 64 | margin-top: 30px; 65 | white-space: pre; 66 | } 67 | } 68 | 69 | * { 70 | box-sizing: border-box; 71 | } 72 | -------------------------------------------------------------------------------- /src/stores/constants.ts: -------------------------------------------------------------------------------- 1 | export const PROJECT_PREFIX = 'gbp-z-web'; 2 | -------------------------------------------------------------------------------- /src/stores/dialogsStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import type { Dialog } from '@/types/Dialog'; 3 | 4 | interface Values { 5 | dialogs: Dialog[], 6 | } 7 | 8 | interface Actions { 9 | setDialog: (dialog: Dialog) => void, 10 | dismissDialog: (index: number) => void, 11 | } 12 | 13 | export type DialogsState = Values & Actions; 14 | 15 | const useDialogsStore = create((set) => ({ 16 | dialogs: [], 17 | dismissDialog: (index: number) => set(({ dialogs }) => ({ dialogs: dialogs.filter((_, i) => index !== i) })), 18 | setDialog: (dialog: Dialog) => set(({ dialogs }) => ({ dialogs: [dialog, ...dialogs] })), 19 | })); 20 | 21 | export default useDialogsStore; 22 | -------------------------------------------------------------------------------- /src/stores/migrations/cleanupItems.ts: -------------------------------------------------------------------------------- 1 | import { type ItemsState } from '@/stores/itemsStore'; 2 | import { loadFrameData } from '@/tools/applyFrame/frameData'; 3 | import { reduceImagesMonochrome } from '@/tools/isRGBNImage'; 4 | import { load } from '@/tools/storage'; 5 | import { Frame } from '@/types/Frame'; 6 | import { Image } from '@/types/Image'; 7 | 8 | /* 9 | Cleanups should do modification to state objects which _do not_ require any update to the type structure 10 | */ 11 | export const cleanupItems = async (persistedItemsState: ItemsState): Promise => { 12 | const { 13 | images, 14 | updateImages, 15 | frames, 16 | updateFrames, 17 | } = persistedItemsState; 18 | const imagesUpdateRequired: Image[] = []; 19 | 20 | const monochromeImages = images.reduce(reduceImagesMonochrome, []); 21 | 22 | for (const monochromeImage of monochromeImages) { 23 | if (!monochromeImage.lines) { 24 | const tiles = await load(monochromeImage.hash, '', true); 25 | if (tiles?.length) { 26 | imagesUpdateRequired.push({ 27 | ...monochromeImage, 28 | lines: tiles.length, 29 | }); 30 | } 31 | } 32 | } 33 | 34 | updateImages(imagesUpdateRequired); 35 | 36 | const framesUpdateRequired: Frame[] = []; 37 | for (const frame of frames) { 38 | if (!frame.lines) { 39 | const frameData = await loadFrameData(frame.hash); 40 | if (frameData) { 41 | framesUpdateRequired.push({ 42 | ...frame, 43 | lines: frameData.upper.length + (frameData.left.length * 20) + frameData.lower.length, 44 | }); 45 | } 46 | } 47 | } 48 | 49 | updateFrames(framesUpdateRequired); 50 | }; 51 | -------------------------------------------------------------------------------- /src/stores/migrations/history/0/State.d.ts: -------------------------------------------------------------------------------- 1 | import type { Frame } from '@/types/Frame'; 2 | import type { FrameGroup } from '@/types/FrameGroup'; 3 | import type { Image } from '@/types/Image'; 4 | import type { SerializableImageGroup } from '@/types/ImageGroup'; 5 | import type { Palette } from '@/types/Palette'; 6 | import type { Plugin } from '@/types/Plugin'; 7 | import type { DropBoxSettings, GitStorageSettings, RecentImport, SyncLastUpdate } from '@/types/Sync'; 8 | import type { VideoParams } from '@/types/VideoParams'; 9 | 10 | export interface ReduxState { 11 | // ItemsState 12 | frameGroupNames: FrameGroup[], 13 | frames: Frame[], 14 | images: Image[], 15 | imageGroups: SerializableImageGroup[], 16 | palettes: Palette[], 17 | plugins: Plugin[], 18 | 19 | // SettingsState 20 | activePalette: string, 21 | enableDebug: boolean, 22 | exportFileTypes: string[], 23 | exportScaleFactors: number[], 24 | forceMagicCheck: boolean, 25 | galleryView: string, 26 | handleExportFrame: string, 27 | hideDates: boolean, 28 | importDeleted: boolean, 29 | importLastSeen: boolean, 30 | importPad: boolean, 31 | pageSize: number, 32 | preferredLocale: string, 33 | printerParams: string, 34 | printerUrl: string, 35 | savFrameTypes: string, 36 | sortPalettes: string, 37 | useSerials: boolean, 38 | videoParams: VideoParams, 39 | 40 | // FiltersState 41 | imageSelection: string[], 42 | recentImports: RecentImport[], 43 | sortBy: string, 44 | 45 | // StoragesState 46 | dropboxStorage: DropBoxSettings, 47 | gitStorage: GitStorageSettings, 48 | syncLastUpdate: SyncLastUpdate, 49 | } 50 | -------------------------------------------------------------------------------- /src/stores/migrations/history/0/migrateItems.ts: -------------------------------------------------------------------------------- 1 | import type { ItemsState } from '@/stores/itemsStore'; 2 | import { cleanImages } from './cleanImages'; 3 | import { hashStoredFrames } from './hashFrames'; 4 | import type { ReduxState } from './State'; 5 | 6 | export const migrateItems = async (persistedState: unknown): Promise> => { 7 | const v0state = persistedState as Partial; 8 | const result: Partial = {}; 9 | 10 | if (v0state.frames?.length) { 11 | result.frames = await hashStoredFrames(v0state.frames); 12 | } 13 | 14 | if (v0state.frameGroupNames?.length) { 15 | result.frameGroups = v0state.frameGroupNames; 16 | } 17 | 18 | if (v0state.palettes?.length) { 19 | result.palettes = v0state.palettes; 20 | } 21 | 22 | if (v0state.plugins?.length) { 23 | result.plugins = v0state.plugins; 24 | } 25 | 26 | if (v0state.imageGroups?.length) { 27 | result.imageGroups = v0state.imageGroups; 28 | } 29 | 30 | if (v0state.images?.length) { 31 | result.images = cleanImages(v0state.images); 32 | } 33 | 34 | return result; 35 | }; 36 | -------------------------------------------------------------------------------- /src/styles/components/appBar.ts: -------------------------------------------------------------------------------- 1 | import type { Components } from '@mui/material/styles'; 2 | import type { Theme } from '@mui/system'; 3 | 4 | export const appBar = (theme: Theme): Components['MuiAppBar'] => ({ 5 | variants: [ 6 | ...(['primary', 'secondary'] as ('primary' | 'secondary')[]).map((color) => ({ 7 | props: { color }, 8 | 9 | style: { 10 | '--AppBar-background': theme.palette[color].dark, 11 | '--AppBar-color': theme.palette[color].contrastText, 12 | 13 | '& .MuiButton-root.active': { 14 | backgroundColor: theme.palette[color === 'primary' ? 'secondary' : 'primary'].main, 15 | }, 16 | 17 | '& .MuiButton-root:hover': { 18 | backgroundColor: theme.palette[color].light, 19 | color: theme.palette[color].contrastText, 20 | }, 21 | 22 | '& .MuiIconButton-root:hover': { 23 | backgroundColor: theme.palette[color].light, 24 | color: theme.palette[color].contrastText, 25 | }, 26 | }, 27 | })), 28 | ], 29 | }); 30 | -------------------------------------------------------------------------------- /src/styles/components/button.ts: -------------------------------------------------------------------------------- 1 | import { alpha } from '@mui/material'; 2 | import type { Components } from '@mui/material/styles'; 3 | import type { Theme } from '@mui/system'; 4 | 5 | export const button = (theme: Theme): Components['MuiButton'] => ({ 6 | styleOverrides: { 7 | root: { 8 | fontSize: '13px', 9 | textTransform: 'none', 10 | minHeight: '40px', 11 | 12 | }, 13 | }, 14 | variants: [ 15 | ...(['primary', 'secondary', 'tertiary'] as ('primary' | 'secondary')[]).map((color) => ({ 16 | props: { color }, 17 | style: { 18 | '&.MuiButton-outlined': { 19 | // background: theme.palette[color].main, 20 | color: theme.palette[color].light, 21 | borderColor: theme.palette[color].light, 22 | '&:hover': { 23 | backgroundColor: alpha(theme.palette[color].main, 0.25), 24 | }, 25 | '&.Mui-disabled': { 26 | backgroundColor: alpha(theme.palette.fgtext.main, 0.33), 27 | color: theme.palette.fgtext.main, 28 | borderColor: theme.palette.fgtext.main, 29 | opacity: 0.4, 30 | }, 31 | }, 32 | // '&:hover': { 33 | // backgroundColor: alpha(theme.palette[color].main, 0.4), 34 | // }, 35 | }, 36 | })), 37 | ], 38 | }); 39 | -------------------------------------------------------------------------------- /src/styles/components/cardActionArea.ts: -------------------------------------------------------------------------------- 1 | import type { Components } from '@mui/material/styles'; 2 | 3 | export const cardActionArea = (): Components['MuiCardActionArea'] => ({ 4 | styleOverrides: { 5 | root: { 6 | height: '100%', 7 | '.MuiCardActionArea-focusHighlight': { 8 | background: 'transparent', 9 | }, 10 | }, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /src/styles/components/cardContent.ts: -------------------------------------------------------------------------------- 1 | import type { Components } from '@mui/material/styles'; 2 | 3 | export const cardContent = (): Components['MuiCardContent'] => ({ 4 | styleOverrides: { 5 | root: { 6 | padding: '24px 16px', 7 | }, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/styles/components/dialogTitle.ts: -------------------------------------------------------------------------------- 1 | import type { Components } from '@mui/material/styles'; 2 | import type { Theme } from '@mui/system'; 3 | 4 | export const dialogTitle = (theme: Theme): Components['MuiDialogTitle'] => ({ 5 | styleOverrides: { 6 | root: { 7 | backgroundColor: theme.palette.primary[theme.palette.mode], 8 | color: theme.palette.primary.contrastText, 9 | 10 | // the '&&' is workaround for https://github.com/mui/material-ui/issues/27851 11 | '&& + .MuiDialogContent-root': { 12 | padding: theme.spacing(2), 13 | }, 14 | }, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /src/styles/components/formControl.ts: -------------------------------------------------------------------------------- 1 | import type { Components } from '@mui/material/styles'; 2 | 3 | export const formControl = (): Components['MuiFormControl'] => ({ 4 | styleOverrides: { 5 | // add padding to have space for the shrink inputLabel outside of the inputfield's border 6 | root: { 7 | paddingTop: 16, 8 | 9 | '.MuiStack-root &:first-of-type': { 10 | marginTop: 12, 11 | }, 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /src/styles/components/inputLabel.ts: -------------------------------------------------------------------------------- 1 | import type { Components } from '@mui/material/styles'; 2 | 3 | export const inputLabel = (): Components['MuiInputLabel'] => ({ 4 | styleOverrides: { 5 | root: { 6 | // changed transform moves the shrink inputLabel outside of the inputfield's border 7 | transform: 'translate(14px, 25px) scale(1)', 8 | '&.MuiInputLabel-shrink': { 9 | transform: 'translate(2px, -12px) scale(1)', 10 | }, 11 | }, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/styles/components/link.ts: -------------------------------------------------------------------------------- 1 | import type { Components } from '@mui/material/styles'; 2 | 3 | export const link = (): Components['MuiLink'] => ({ 4 | styleOverrides: { 5 | root: { 6 | color: 'inherit', 7 | }, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/styles/components/list.ts: -------------------------------------------------------------------------------- 1 | import type { Components } from '@mui/material/styles'; 2 | 3 | export const list = (): Components['MuiList'] => ({ 4 | styleOverrides: { 5 | root: { 6 | '.MuiListSubheader-root': { 7 | display: 'flex', 8 | 9 | '.MuiListItemIcon-root': { 10 | minWidth: 32, 11 | flexDirection: 'column', 12 | justifyContent: 'center', 13 | }, 14 | }, 15 | }, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /src/styles/components/menuItem.ts: -------------------------------------------------------------------------------- 1 | import { alpha } from '@mui/material'; 2 | import type { Components } from '@mui/material/styles'; 3 | import type { Theme } from '@mui/system'; 4 | 5 | export const menuItem = (theme: Theme): Components['MuiMenuItem'] => ({ 6 | styleOverrides: { 7 | root: { 8 | '&.Mui-selected': { 9 | backgroundColor: alpha(theme.palette.secondary.main, 0.8), 10 | color: theme.palette.secondary.contrastText, 11 | '&:hover,&.Mui-focusVisible': { 12 | backgroundColor: theme.palette.secondary.main, 13 | }, 14 | }, 15 | }, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /src/styles/components/outlinedInput.ts: -------------------------------------------------------------------------------- 1 | import type { Components } from '@mui/material/styles'; 2 | import type { Theme } from '@mui/system'; 3 | 4 | export const outlinedInput = (theme: Theme): Components['MuiOutlinedInput'] => ({ 5 | styleOverrides: { 6 | root: { 7 | background: theme.palette.mode === 'dark' ? '#000000' : '#ffffff', 8 | color: theme.palette.mode === 'dark' ? '#ffffff' : '#000000', 9 | '.MuiInputBase-inputMultiline': { 10 | fontSize: '13px', 11 | lineHeight: '16px', 12 | fontFamily: 'monospace', 13 | }, 14 | // hide the legend because shrink inputLabel is outside of the inputfield's border 15 | '.MuiOutlinedInput-notchedOutline legend span': { 16 | display: 'none', 17 | }, 18 | }, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /src/styles/components/paper.ts: -------------------------------------------------------------------------------- 1 | import type { Components } from '@mui/material/styles'; 2 | 3 | export const paper = (): Components['MuiPaper'] => ({ 4 | styleOverrides: { 5 | root: { 6 | backgroundImage: 'none', 7 | }, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/styles/components/select.ts: -------------------------------------------------------------------------------- 1 | import type { Components } from '@mui/material/styles'; 2 | 3 | export const select = (): Components['MuiSelect'] => ({ 4 | styleOverrides: { 5 | select: { 6 | display: 'flex', 7 | 8 | '.MuiListItemIcon-root': { 9 | minWidth: 32, 10 | flexDirection: 'column', 11 | justifyContent: 'center', 12 | }, 13 | }, 14 | }, 15 | defaultProps: { 16 | displayEmpty: true, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /src/styles/components/switch.ts: -------------------------------------------------------------------------------- 1 | import type { Components } from '@mui/material/styles'; 2 | 3 | export const switch_ = (): Components['MuiSwitch'] => ({ 4 | defaultProps: { 5 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 6 | // @ts-ignore 7 | color: 'tertiary', 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/styles/components/tab.ts: -------------------------------------------------------------------------------- 1 | import { alpha } from '@mui/material'; 2 | import type { Components } from '@mui/material/styles'; 3 | import type { Theme } from '@mui/system'; 4 | 5 | export const tab = (theme: Theme): Components['MuiTab'] => ({ 6 | styleOverrides: { 7 | root: { 8 | fontSize: 16, 9 | textTransform: 'none', 10 | minHeight: 60, 11 | 12 | '&.Mui-selected': { 13 | backgroundColor: alpha(theme.palette.primary.main, 0.2), 14 | color: 'inherit', 15 | }, 16 | 17 | '&:hover': { 18 | backgroundColor: alpha(theme.palette.secondary.main, 0.35), 19 | }, 20 | }, 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /src/styles/components/tabs.ts: -------------------------------------------------------------------------------- 1 | import type { Components } from '@mui/material/styles'; 2 | import type { Theme } from '@mui/system'; 3 | 4 | export const tabs = (theme: Theme): Components['MuiTabs'] => ({ 5 | styleOverrides: { 6 | root: { 7 | '&~.MuiTabPanel-root': { 8 | padding: 0, 9 | marginTop: theme.spacing(2), 10 | }, 11 | '& .MuiTabScrollButton-root.Mui-disabled': { 12 | opacity: 0.25, 13 | }, 14 | }, 15 | }, 16 | defaultProps: { 17 | scrollButtons: 'auto', 18 | indicatorColor: 'secondary', 19 | variant: 'scrollable', 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /src/styles/components/textField.ts: -------------------------------------------------------------------------------- 1 | import type { Components } from '@mui/material/styles'; 2 | import { textFieldSlotDefaults } from '@/consts/textFieldSlotDefaults'; 3 | 4 | export const textField = (): Components['MuiTextField'] => ({ 5 | defaultProps: { 6 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 7 | // @ts-ignore 8 | color: 'tertiary', 9 | slotProps: textFieldSlotDefaults, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/styles/components/toggleButtonGroup.ts: -------------------------------------------------------------------------------- 1 | import { alpha } from '@mui/material'; 2 | import type { Components } from '@mui/material/styles'; 3 | import type { Theme } from '@mui/system'; 4 | 5 | export const toggleButtonGroup = (theme: Theme): Components['MuiToggleButtonGroup'] => ({ 6 | styleOverrides: { 7 | grouped: { 8 | variants: [ 9 | ...(['primary', 'secondary', 'tertiary'] as ('primary' | 'secondary')[]).map((color) => ({ 10 | props: { color }, 11 | style: { 12 | '&.Mui-selected': { 13 | background: theme.palette[color].main, 14 | color: theme.palette[color].contrastText, 15 | '&:hover': { 16 | backgroundColor: alpha(theme.palette[color].main, 0.7), 17 | }, 18 | }, 19 | '&:hover': { 20 | backgroundColor: alpha(theme.palette[color].main, 0.4), 21 | }, 22 | }, 23 | })), 24 | ], 25 | }, 26 | }, 27 | defaultProps: { 28 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 29 | // @ts-ignore 30 | color: 'tertiary' as ('primary' | 'secondary'), 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /src/styles/components/toolbar.ts: -------------------------------------------------------------------------------- 1 | import type { Theme as ThemedTheme } from '@mui/material'; 2 | import type { Components } from '@mui/material/styles'; 3 | import type { Theme } from '@mui/system'; 4 | 5 | export const toolbar = (theme: Theme): Components['MuiToolbar'] => ({ 6 | styleOverrides: { 7 | root: { 8 | [theme.breakpoints.up('xs')]: { 9 | // Just Override stringer MUI selector 10 | minHeight: 'var(--navigation-height)', 11 | }, 12 | 13 | justifyContent: 'end', 14 | 15 | '& .MuiButton-root': { 16 | height: 'var(--navigation-height)', 17 | padding: '0 20px', 18 | fontSize: (theme as ThemedTheme).typography.body1.fontSize, 19 | }, 20 | 21 | '& .MuiButtonGroup-horizontal': { 22 | paddingRight: '8px', 23 | }, 24 | 25 | '& .MuiSvgIcon-root': { 26 | fontSize: '1.8rem', 27 | }, 28 | }, 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /src/styles/themes.ts: -------------------------------------------------------------------------------- 1 | import type { Theme } from '@mui/system'; 2 | import { generateTheme } from './tools/generateTheme'; 3 | 4 | 5 | const common = { 6 | colorInfo: '#3f769e', 7 | colorSuccess: '#3f9e7c', 8 | colorWarn: '#8b7b2c', 9 | colorError: '#cb0003', 10 | colorPrimary: '#503281', 11 | colorSecondary: '#455c63', 12 | colorTertiary: '#4a6959', 13 | }; 14 | 15 | export const darkTheme: Theme = generateTheme({ 16 | mode: 'dark', 17 | // colorPrimary: '#b3acbe', 18 | // colorSecondary: '#455c63', 19 | // colorTertiary: '#4a6959', 20 | colorPageBackground: '#2a292b', 21 | colorText: '#b2b2b2', 22 | ...common, 23 | }); 24 | export const lightTheme: Theme = generateTheme({ 25 | mode: 'light', 26 | // colorPrimary: '#292037', 27 | // colorSecondary: '#283539', 28 | // colorTertiary: '#273931', 29 | colorPageBackground: '#d6d3dc', 30 | colorText: '#333333', 31 | ...common, 32 | }); 33 | -------------------------------------------------------------------------------- /src/styles/tools/getHoverColor.ts: -------------------------------------------------------------------------------- 1 | import type { Palette, PaletteColor } from '@mui/material/styles'; 2 | 3 | export const getHoverColor = (palette: Partial & Pick, color: PaletteColor) => ( 4 | color[palette.mode] 5 | ); 6 | -------------------------------------------------------------------------------- /src/styles/tools/getPreStyles.ts: -------------------------------------------------------------------------------- 1 | import type { Theme } from '@mui/system'; 2 | 3 | export const getPreStyles = (theme: Theme, additionalStyles: object) => ({ 4 | flexGrow: 0, 5 | flexShrink: 1, 6 | wordBreak: 'break-all', 7 | whiteSpace: 'no-wrap', 8 | overflow: 'hidden', 9 | width: '100%', 10 | borderRadius: '3px', 11 | ...additionalStyles, 12 | }); 13 | -------------------------------------------------------------------------------- /src/tools/appendUint8Arrays/index.ts: -------------------------------------------------------------------------------- 1 | export const appendUint8Arrays = (arrays: (Uint8Array | null | undefined)[]): Uint8Array => { 2 | const validArrays = arrays.filter(Boolean) as Uint8Array[]; 3 | const totalLength = validArrays.reduce((sum, arr) => sum + arr.length, 0); 4 | const result = new Uint8Array(totalLength); 5 | 6 | let offset = 0; 7 | for (const arr of validArrays) { 8 | result.set(arr, offset); 9 | offset += arr.length; 10 | } 11 | 12 | return result; 13 | }; 14 | -------------------------------------------------------------------------------- /src/tools/applyBitmapFilter/generateBaseValues.ts: -------------------------------------------------------------------------------- 1 | import generateValueRange from './generateValueRange'; 2 | 3 | const generateBaseValues = (baseValues: number[]): [number[], number[], number[]] => { 4 | const a = typeof baseValues[0] === 'number' ? baseValues[0] : 0x00; 5 | const b = typeof baseValues[1] === 'number' ? baseValues[1] : 0x55; 6 | const c = typeof baseValues[2] === 'number' ? baseValues[2] : 0xAA; 7 | const d = typeof baseValues[3] === 'number' ? baseValues[3] : 0xFF; 8 | 9 | return ([ 10 | generateValueRange(a, b), 11 | generateValueRange(b, c), 12 | generateValueRange(c, d), 13 | ]); 14 | }; 15 | 16 | export default generateBaseValues; 17 | -------------------------------------------------------------------------------- /src/tools/applyBitmapFilter/generatePattern.ts: -------------------------------------------------------------------------------- 1 | import { GeneratePatternOptions } from '@/types/BitmapFilter'; 2 | 3 | const generatePattern = ({ 4 | baseValues, 5 | orderPatterns, 6 | }: GeneratePatternOptions): number[][][] => { 7 | const pattern: number[][][] = []; 8 | 9 | for (let x = 0; x < 4; x += 1) { 10 | const xDim: number[][] = []; 11 | pattern.push(xDim); 12 | for (let y = 0; y < 4; y += 1) { 13 | const yDim: number[] = []; 14 | xDim.push(yDim); 15 | for (let z = 0; z < 3; z += 1) { 16 | yDim.push(baseValues[z][orderPatterns[z][(x * 4) + y]]); 17 | } 18 | } 19 | } 20 | 21 | return pattern; 22 | }; 23 | 24 | export default generatePattern; 25 | -------------------------------------------------------------------------------- /src/tools/applyBitmapFilter/generateValueRange.ts: -------------------------------------------------------------------------------- 1 | const generateValueRange = (start: number, end: number): number[] => { 2 | const step = (end - start) / 16; 3 | return (new Array(16)) 4 | .fill(null) 5 | .map((_, index) => ( 6 | Math.floor(start + (step * index)) 7 | )); 8 | }; 9 | 10 | export default generateValueRange; 11 | -------------------------------------------------------------------------------- /src/tools/applyBitmapFilter/orderPatterns.ts: -------------------------------------------------------------------------------- 1 | 2 | export const orderPatternDither = [ 3 | 0x00, 0x0c, 0x03, 0x0f, 4 | 0x08, 0x04, 0x0b, 0x07, 5 | 0x02, 0x0e, 0x01, 0x0d, 6 | 0x0a, 0x06, 0x09, 0x05, 7 | ]; 8 | 9 | export const orderPatternNoDither = [ 10 | 0x00, 0x00, 0x00, 0x00, 11 | 0x00, 0x00, 0x00, 0x00, 12 | 0x00, 0x00, 0x00, 0x00, 13 | 0x00, 0x00, 0x00, 0x00, 14 | ]; 15 | -------------------------------------------------------------------------------- /src/tools/applyFrame/index.ts: -------------------------------------------------------------------------------- 1 | import { ExportFrameMode, tileIndexIsPartOfFrame } from 'gb-image-decoder'; 2 | import { loadFrameData } from './frameData'; 3 | 4 | const applyFrame = async (tiles: string[], frameHash: string): Promise => { 5 | 6 | // image must be "default" dimensions 7 | if (tiles.length !== 360) { 8 | return tiles; 9 | } 10 | 11 | const frameData = await loadFrameData(frameHash); 12 | 13 | if (!frameData) { 14 | return tiles; 15 | } 16 | 17 | if (frameData.left.length !== 14 || frameData.right.length !== 14) { 18 | console.error('Invalid frameData'); 19 | return tiles; 20 | } 21 | 22 | const unframedImage: string[] = tiles.reduce((acc: string[], tile: string, index: number): string[] => ( 23 | tileIndexIsPartOfFrame(index, 2, ExportFrameMode.FRAMEMODE_KEEP) ? acc : [...acc, tile] 24 | ), []); 25 | 26 | const result: string[] = [...frameData.upper]; 27 | 28 | for (let line = 0; line < 14; line += 1) { 29 | result.push(...frameData.left[line], ...unframedImage.slice(line * 16, (line + 1) * 16), ...frameData.right[line]); 30 | } 31 | 32 | result.push(...frameData.lower); 33 | 34 | return result; 35 | }; 36 | 37 | export default applyFrame; 38 | -------------------------------------------------------------------------------- /src/tools/applyTagChanges/index.ts: -------------------------------------------------------------------------------- 1 | import type { TagUpdates } from '@/tools/modifyTagChanges'; 2 | import unique from '../unique'; 3 | 4 | export interface TagChange extends TagUpdates { 5 | initial: readonly string[], 6 | } 7 | 8 | const applyTagChanges = ({ initial, add, remove }: TagChange): string[] => ( 9 | unique([...initial, ...add]) 10 | .filter((tag) => !remove.includes(tag)) 11 | ); 12 | 13 | export default applyTagChanges; 14 | -------------------------------------------------------------------------------- /src/tools/blobToArrayBuffer/index.ts: -------------------------------------------------------------------------------- 1 | const blobToArrayBuffer = (blob: Blob): Promise => { 2 | if (typeof blob.arrayBuffer === 'function') { 3 | return blob.arrayBuffer(); 4 | } 5 | 6 | return new Promise(((resolve) => { 7 | const fileReader = new FileReader(); 8 | fileReader.onload = (ev) => { 9 | if (!ev.target) { 10 | throw new Error('no file'); 11 | } 12 | 13 | const result = ev.target.result; 14 | 15 | if (typeof result === 'string' || result === null) { 16 | throw new Error('unexpected filereader result'); 17 | } 18 | 19 | resolve(result); 20 | }; 21 | 22 | fileReader.readAsArrayBuffer(blob); 23 | })); 24 | }; 25 | 26 | export default blobToArrayBuffer; 27 | -------------------------------------------------------------------------------- /src/tools/canShare/index.ts: -------------------------------------------------------------------------------- 1 | let canShareValue: boolean | null = null; 2 | 3 | export const canShare = (): boolean => { 4 | if (canShareValue === null) { 5 | try { 6 | canShareValue = window.navigator.canShare({ 7 | files: [new File([new Blob(['t', 'e', 's', 't'])], 'test.txt', { type: 'text/plain', lastModified: Date.now() })], 8 | }); 9 | } catch { 10 | canShareValue = false; 11 | } 12 | } 13 | 14 | return canShareValue || false; 15 | }; 16 | -------------------------------------------------------------------------------- /src/tools/cleanPath/index.ts: -------------------------------------------------------------------------------- 1 | const cleanPath = (path: string): string => ( 2 | path 3 | .replace(/[^a-z0-9/\\._-]/gi, '') // remove invalid chars 4 | .replace(/[\\/]+/gi, '/') // replace '\' with '/' 5 | .replace(/^\//, '') // remove starting '/' 6 | .replace(/\/$/, '') // remove trailing '/' 7 | ); 8 | 9 | export default cleanPath; 10 | -------------------------------------------------------------------------------- /src/tools/cleanUrl/index.ts: -------------------------------------------------------------------------------- 1 | const cleanUrl = (dirtyUrl: string, protocol: string): string => { 2 | if (!dirtyUrl.trim()) { 3 | return ''; 4 | } 5 | 6 | if (dirtyUrl === '/' && protocol !== 'ws') { 7 | return dirtyUrl; 8 | } 9 | 10 | const hasProtocol = !!dirtyUrl.match(new RegExp(`^${protocol}(s)?:\\/\\/`, 'gi')); 11 | return `${hasProtocol ? '' : `${protocol}://`}${dirtyUrl}${dirtyUrl.endsWith('/') ? '' : '/'}`; 12 | }; 13 | 14 | export default cleanUrl; 15 | -------------------------------------------------------------------------------- /src/tools/comms/DeviceAPIs/BaseCommsDevice.ts: -------------------------------------------------------------------------------- 1 | import { PortDeviceType, PortType } from '@/consts/ports'; 2 | 3 | export abstract class BaseCommsDevice { 4 | public abstract readonly portType: PortType; 5 | public abstract readonly portDeviceType: PortDeviceType; 6 | public abstract readonly id: string; 7 | public abstract readonly description: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/tools/comms/DeviceAPIs/InactiveCommsDevice.ts: -------------------------------------------------------------------------------- 1 | import { PortDeviceType, PortType } from '@/consts/ports'; 2 | import { CommonPort } from '@/tools/comms/CommonPort'; 3 | import { BaseCommsDevice } from '@/tools/comms/DeviceAPIs/BaseCommsDevice'; 4 | import { randomId } from '@/tools/randomId'; 5 | 6 | export class InactiveCommsDevice implements BaseCommsDevice { 7 | public readonly id: string; 8 | public readonly description: string; 9 | public readonly portDeviceType = PortDeviceType.INACTIVE; 10 | public readonly portType: PortType; 11 | private bannerBytes: Uint8Array; 12 | 13 | constructor(device: CommonPort, bannerBytes: Uint8Array, reason?: string) { 14 | this.bannerBytes = bannerBytes; 15 | this.portType = device.portType; 16 | this.description = [ 17 | device.getDescription(), 18 | reason, 19 | ] 20 | .filter(Boolean) 21 | .join(' '); 22 | this.id = randomId(); 23 | } 24 | 25 | async getBanner(): Promise { 26 | return this.bannerBytes; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/tools/createTreeRoot/index.ts: -------------------------------------------------------------------------------- 1 | import { Image } from '@/types/Image'; 2 | import { type TreeImageGroup } from '@/types/ImageGroup'; 3 | 4 | export const ROOT_ID = 'ROOT'; 5 | 6 | export const createTreeRoot = (allImages: Image[]): TreeImageGroup => ({ 7 | id: ROOT_ID, 8 | slug: '', 9 | created: '', 10 | title: 'Home', 11 | coverImage: '', 12 | images: [], 13 | groups: [], 14 | tags: [], 15 | allImages, 16 | }); 17 | -------------------------------------------------------------------------------- /src/tools/database/dbGetSet.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | 3 | export interface KV { 4 | key: string, 5 | value: T, 6 | } 7 | 8 | export const dbGetByKey = async (store: IDBObjectStore, key: string): Promise => new Promise((resolve) => { 9 | const request = store.get(key); 10 | request.onsuccess = (ev) => { 11 | // @ts-ignore 12 | resolve(ev.target?.result as string[]); 13 | }; 14 | }); 15 | 16 | export const dbGetAllFromStore = async (request: IDBOpenDBRequest, storeName: string): Promise[]> => ( 17 | new Promise((resolve) => { 18 | const objectStore = request.result.transaction(storeName).objectStore(storeName); 19 | const reqest = objectStore.getAllKeys(); 20 | reqest.onsuccess = async (ev) => { 21 | // @ts-ignore 22 | const keys = ev.target?.result as string[]; 23 | 24 | const dbData: KV[] = []; 25 | 26 | for (const key of keys) { 27 | dbData.push({ 28 | key, 29 | value: await dbGetByKey(objectStore, key), 30 | }); 31 | } 32 | 33 | resolve(dbData); 34 | }; 35 | }) 36 | ); 37 | 38 | export const dbClearAndSetAll = async ( 39 | request: IDBOpenDBRequest, 40 | storeName: string, 41 | data: KV[], 42 | ): Promise => { 43 | const objectStore = request.result.transaction(storeName, 'readwrite').objectStore(storeName); 44 | 45 | const clearRequest = objectStore.clear(); 46 | 47 | clearRequest.onsuccess = async () => { 48 | for (const { key, value } of data) { 49 | objectStore.add(value, key); 50 | } 51 | }; 52 | 53 | }; 54 | -------------------------------------------------------------------------------- /src/tools/database/lsGetSet.ts: -------------------------------------------------------------------------------- 1 | import type { KV } from './dbGetSet'; 2 | 3 | export const localStorageGetAll = async (): Promise[]> => { 4 | const keys = Object.keys(localStorage) 5 | .filter((key) => key.startsWith('gbp')); 6 | 7 | const dbData: KV[] = []; 8 | 9 | for (const key of keys) { 10 | dbData.push({ 11 | key, 12 | value: localStorage.getItem(key) as string, 13 | }); 14 | } 15 | 16 | return dbData; 17 | }; 18 | 19 | export const localStorageClear = () => { 20 | for (const key of Object.keys(localStorage)) { 21 | localStorage.removeItem(key); 22 | } 23 | }; 24 | 25 | export const localStorageSet = (data: KV[]) => { 26 | for (const { key, value } of data) { 27 | localStorage.setItem(key, value); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/tools/delay/index.ts: -------------------------------------------------------------------------------- 1 | export const delay = async (duration: number) => ( 2 | new Promise((resolve) => { 3 | self.setTimeout(resolve, duration); 4 | }) 5 | ); 6 | -------------------------------------------------------------------------------- /src/tools/download/download.ts: -------------------------------------------------------------------------------- 1 | import { saveAs } from 'file-saver'; 2 | import JSZip from 'jszip'; 3 | import blobToArrayBuffer from '@/tools/blobToArrayBuffer'; 4 | import replaceDuplicateFilenames from '@/tools/replaceDuplicateFilenames'; 5 | import { DownloadArrayBuffer, DownloadBlob } from '@/types/download'; 6 | 7 | const download = (zipFileName: string | null) => async (files: DownloadBlob[]): Promise => { 8 | 9 | // if only one file, 10 | if (files.length === 1) { 11 | const image = files[0]; 12 | return saveAs(image.blob, image.filename); 13 | } 14 | 15 | const zip = new JSZip(); 16 | 17 | const buffers: DownloadArrayBuffer[] = await Promise.all(files.map(({ filename, blob }) => ( 18 | blobToArrayBuffer(blob) 19 | .then((arrayBuffer) => ({ 20 | filename, 21 | arrayBuffer, 22 | })) 23 | ))); 24 | 25 | const uniqueBufferFiles: DownloadArrayBuffer[] = replaceDuplicateFilenames(buffers); 26 | 27 | uniqueBufferFiles.forEach(({ filename, arrayBuffer }: DownloadArrayBuffer) => { 28 | zip.file(filename, arrayBuffer); 29 | }); 30 | 31 | const content: Blob = await zip.generateAsync({ 32 | type: 'blob', 33 | compression: 'DEFLATE', 34 | }); 35 | 36 | if (!zipFileName) { 37 | throw new Error('missing filename'); 38 | } 39 | 40 | return saveAs(content, `${zipFileName}.zip`); 41 | }; 42 | 43 | export default download; 44 | -------------------------------------------------------------------------------- /src/tools/download/getTxtFile.ts: -------------------------------------------------------------------------------- 1 | import { finalLine, initLine, moreLine, terminatorLine } from '@/consts/defaults'; 2 | import { load } from '@/tools/storage'; 3 | import type { DownloadInfo } from '@/types/Sync'; 4 | 5 | export const getTxtFile = async (hash: string, title: string, filename: string): Promise => { 6 | const plainTiles = await load(hash); 7 | 8 | const transformedTiles = (plainTiles || []) 9 | // add spaces between every second char 10 | .map((line) => ( 11 | line.match(/.{1,2}/g) 12 | ?.join(' ') || '' 13 | )) 14 | .reduce((acc: string[], line: string, index: number): string[] => { 15 | if (index % 40) { 16 | return [...acc, line]; 17 | } 18 | 19 | return [...acc, moreLine, line]; 20 | }, []); 21 | 22 | const textContent = [ 23 | initLine, 24 | ...transformedTiles, 25 | finalLine, 26 | terminatorLine, 27 | ].join('\n'); 28 | 29 | // toDownload 30 | return { 31 | folder: 'images', // used for Git-Sync 32 | filename: `${filename}.txt`, 33 | blob: new Blob(new Array(textContent), { type: 'text/plain' }), 34 | title, 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /src/tools/download/index.ts: -------------------------------------------------------------------------------- 1 | import download from './download'; 2 | import getPrepareFiles from './getPrepareFiles'; 3 | 4 | export { 5 | download, 6 | getPrepareFiles, 7 | }; 8 | -------------------------------------------------------------------------------- /src/tools/dropboxStorage/DropboxClient/dropboxContentHasher.ts: -------------------------------------------------------------------------------- 1 | import chunk from 'chunk'; 2 | // See https://github.com/dropbox/dropbox-api-content-hasher/blob/master/js-node/dropbox-content-hasher.js 3 | 4 | const BLOCK_SIZE = 4 * 1024 * 1024; 5 | 6 | export async function hasher(buffer: ArrayBuffer): Promise { 7 | const blocks = chunk(new Uint8Array(buffer), BLOCK_SIZE); 8 | 9 | const blockHashes = await Promise.all(blocks.map(async (block) => ( 10 | [...new Uint8Array(await crypto.subtle.digest('SHA-256', new Uint8Array(block)))] 11 | ))); 12 | 13 | const overallHashes = new Uint8Array(blockHashes.flat()); 14 | 15 | const resultHash = new Uint8Array(await crypto.subtle.digest('SHA-256', overallHashes)); 16 | 17 | return [...resultHash].map((bytes: number): string => (bytes.toString(16).padStart(2, '0'))).join(''); 18 | } 19 | -------------------------------------------------------------------------------- /src/tools/ensureSingleUsage/index.ts: -------------------------------------------------------------------------------- 1 | import type { SerializableImageGroup } from '@/types/ImageGroup'; 2 | 3 | export interface SingleUsageResult { 4 | groups: SerializableImageGroup[], 5 | usedImageHashes: string[], 6 | usedGroupIDs: string[], 7 | } 8 | 9 | export const ensureSingleUsage = (groups: SerializableImageGroup[]): SingleUsageResult => { 10 | const usedImageHashes = new Set(); 11 | const usedGroupIDs = new Set(); 12 | 13 | const cleanGroup = (checkGroup: SerializableImageGroup): SerializableImageGroup => { 14 | const filteredImages: string[] = []; 15 | for (const imageHash of checkGroup.images) { 16 | if (!usedImageHashes.has(imageHash)) { 17 | usedImageHashes.add(imageHash); 18 | filteredImages.push(imageHash); 19 | } 20 | } 21 | 22 | const filteredGroups: string[] = []; 23 | for (const groupId of checkGroup.groups) { 24 | if (!usedGroupIDs.has(groupId)) { 25 | usedGroupIDs.add(groupId); 26 | filteredGroups.push(groupId); 27 | } 28 | } 29 | 30 | return { 31 | ...checkGroup, 32 | images: filteredImages, 33 | groups: filteredGroups, 34 | }; 35 | }; 36 | 37 | return { 38 | groups: groups.map(cleanGroup), 39 | usedImageHashes: Array.from(usedImageHashes), 40 | usedGroupIDs: Array.from(usedGroupIDs), 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /src/tools/filterDeleteNew/index.ts: -------------------------------------------------------------------------------- 1 | import type { RepoContents, RepoTasks } from '@/types/Export'; 2 | import type { KeepFile, UploadFile } from '@/types/Sync'; 3 | 4 | const filterDeleteNew = ( 5 | { images, frames }: RepoContents, 6 | toUpload: UploadFile[], 7 | toKeep: KeepFile[], 8 | missingLocally: string[], 9 | ): RepoTasks => { 10 | 11 | const delImages = images.filter(({ path }) => ( 12 | !toKeep.find(({ destination }) => path === destination) && 13 | !toUpload.find(({ destination }) => path === destination) && 14 | !missingLocally.find((hash) => path.indexOf(hash) >= -1) 15 | )); 16 | 17 | const delFrames = frames.filter(({ path }) => ( 18 | !toKeep.find(({ destination }) => path === destination) && 19 | !toUpload.find(({ destination }) => path === destination) 20 | )); 21 | 22 | return ({ 23 | // remove all files from upload queue if they already exist remotely 24 | upload: toUpload.filter(({ destination }) => ( 25 | !images.find(({ path }) => path === destination) && 26 | // !palettes.find(({ path }) => path === destination) && 27 | !frames.find(({ path }) => path === destination) 28 | )), 29 | del: [ 30 | ...delImages, 31 | ...delFrames, 32 | ], 33 | }); 34 | }; 35 | 36 | export default filterDeleteNew; 37 | -------------------------------------------------------------------------------- /src/tools/findSubarray/index.ts: -------------------------------------------------------------------------------- 1 | export const findSubarray = (haystack: Uint8Array, needle: Uint8Array): number => { 2 | const len = needle.length; 3 | const limit = haystack.length - len + 1; 4 | for (let i = 0; i < limit; i++) { 5 | let match = true; 6 | for (let j = 0; j < len; j++) { 7 | if (haystack[i + j] !== needle[j]) { 8 | match = false; 9 | break; 10 | } 11 | } 12 | if (match) return i; 13 | } 14 | return -1; 15 | }; 16 | -------------------------------------------------------------------------------- /src/tools/generateGradient/index.ts: -------------------------------------------------------------------------------- 1 | import type { SxProps, Theme } from '@mui/material/styles'; 2 | import { hexToRgbString } from '@/tools/hexToRgbString'; 3 | 4 | export enum GradientType { 5 | HARD = 'HARD', 6 | SMOOTH = 'SMOOTH', 7 | CONIC = 'CONIC', 8 | } 9 | 10 | export const generateGradient = (paletteColors: string[], type: GradientType): SxProps => { 11 | const opacity = 0.25; 12 | const g1 = hexToRgbString(paletteColors[0] || '#000000'); 13 | const g2 = hexToRgbString(paletteColors[1] || '#000000'); 14 | const g3 = hexToRgbString(paletteColors[2] || '#000000'); 15 | const g4 = hexToRgbString(paletteColors[3] || '#000000'); 16 | 17 | switch (type) { 18 | case GradientType.SMOOTH: { 19 | return { 20 | backgroundImage: `linear-gradient(to right, 21 | rgba(${g1}, ${opacity}) 0%, 22 | rgba(${g2}, ${opacity}) 33.3%, 23 | rgba(${g3}, ${opacity}) 66.6%, 24 | rgba(${g4}, ${opacity}) 100% 25 | )`, 26 | }; 27 | } 28 | 29 | case GradientType.HARD: { 30 | return { 31 | backgroundImage: `linear-gradient(to right, 32 | rgb(${g1}) 0% 25%, 33 | rgb(${g2}) 25% 50%, 34 | rgb(${g3}) 50% 75%, 35 | rgb(${g4}) 75% 100% 36 | )`, 37 | }; 38 | } 39 | 40 | case GradientType.CONIC: { 41 | return { 42 | background: `conic-gradient( 43 | rgb(${g1}) 0% 25%, 44 | rgb(${g2}) 25% 50%, 45 | rgb(${g3}) 50% 75%, 46 | rgb(${g4}) 75% 100% 47 | )`, 48 | }; 49 | } 50 | 51 | default: 52 | return {}; 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /src/tools/getChannelColor/index.ts: -------------------------------------------------------------------------------- 1 | import type { RGBNHashes } from '@/types/Image'; 2 | 3 | export const getChannelColor = (colorKey: keyof RGBNHashes) => { 4 | switch (colorKey) { 5 | case 'r': 6 | return '#ff0000'; 7 | case 'g': 8 | return '#00aa00'; 9 | case 'b': 10 | return '#0000ff'; 11 | case 'n': 12 | default: 13 | return '#444444'; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/tools/getFilteredImages/count.ts: -------------------------------------------------------------------------------- 1 | import { TreeImageGroup } from '@/types/ImageGroup'; 2 | import type { RecentImport } from '@/types/Sync'; 3 | import filterSpecial from './filterSpecial'; 4 | import filterTags from './filterTags'; 5 | 6 | const getFilteredImagesCount = ( 7 | { 8 | images, 9 | groups, 10 | }: TreeImageGroup, 11 | filtersTags: string[], 12 | filtersFrames: string[], 13 | filtersPalettes: string[], 14 | recentImports: RecentImport[], 15 | ): number => ( 16 | [...images] 17 | .filter(filterSpecial(filtersTags, filtersFrames, filtersPalettes, recentImports, groups)) 18 | .filter(filterTags(filtersTags)) 19 | .length 20 | ); 21 | 22 | export default getFilteredImagesCount; 23 | -------------------------------------------------------------------------------- /src/tools/getFilteredImages/filterTags.ts: -------------------------------------------------------------------------------- 1 | import { SpecialTags } from '@/consts/SpecialTags'; 2 | import type { Image } from '@/types/Image'; 3 | 4 | const filterTags = (activeTags: (SpecialTags | string)[]) => (image: Image): boolean => { 5 | 6 | const normalTags = activeTags.filter((tag) => ( 7 | ![ 8 | SpecialTags.FILTER_MONOCHROME, 9 | SpecialTags.FILTER_NEW, 10 | SpecialTags.FILTER_RGB, 11 | SpecialTags.FILTER_UNTAGGED, 12 | SpecialTags.FILTER_RECENT, 13 | SpecialTags.FILTER_FAVOURITE, 14 | SpecialTags.FILTER_COMMENTS, 15 | SpecialTags.FILTER_USERNAME, 16 | ] 17 | .includes(tag as SpecialTags) 18 | )); 19 | 20 | if (normalTags.length) { 21 | return !!image.tags.find((tag) => normalTags.includes(tag)); 22 | } 23 | 24 | // Not filtering for tags 25 | return true; 26 | }; 27 | 28 | export default filterTags; 29 | -------------------------------------------------------------------------------- /src/tools/getFilteredImages/index.ts: -------------------------------------------------------------------------------- 1 | import type { FiltersState } from '@/stores/filtersStore'; 2 | import { addSortIndex, removeSortIndex, sortImages } from '@/tools/sortImages'; 3 | import type { Image } from '@/types/Image'; 4 | import { TreeImageGroup } from '@/types/ImageGroup'; 5 | import filterSpecial from './filterSpecial'; 6 | import filterTags from './filterTags'; 7 | 8 | export type FilteredImagesState = Pick 9 | 10 | export const getFilteredImages = ( 11 | { 12 | images, 13 | groups, 14 | }: Pick, 15 | { 16 | filtersTags, 17 | filtersFrames, 18 | filtersPalettes, 19 | sortBy, 20 | recentImports, 21 | }: FilteredImagesState, 22 | ): Image[] => ( 23 | [...images] 24 | .map(addSortIndex) 25 | .sort(sortImages(sortBy)) 26 | .map(removeSortIndex) 27 | .filter(filterSpecial(filtersTags, filtersFrames, filtersPalettes, recentImports, groups)) 28 | .filter(filterTags(filtersTags)) 29 | ); 30 | -------------------------------------------------------------------------------- /src/tools/getFrameFromFullTiles/index.ts: -------------------------------------------------------------------------------- 1 | import type { FrameData } from '@/tools/applyFrame/frameData'; 2 | 3 | const toUpper = (s: string): string => s.toUpperCase(); 4 | 5 | export const getFrameFromFullTiles = (tiles: string[], imageStartLine: number): FrameData => { 6 | 7 | const upper = tiles.slice(0, imageStartLine * 20).map(toUpper); 8 | const lower = tiles.slice((imageStartLine + 14) * 20).map(toUpper); 9 | const left = Array(14).fill(0).map((_, line) => { 10 | const offsetStart = (line + imageStartLine) * 20; 11 | return tiles.slice(offsetStart, offsetStart + 2).map(toUpper); 12 | }); 13 | const right = Array(14).fill(0).map((_, line) => { 14 | const offsetStart = (line + imageStartLine) * 20; 15 | return tiles.slice(offsetStart + 18, offsetStart + 20).map(toUpper); 16 | }); 17 | 18 | return { 19 | upper, 20 | left, 21 | right, 22 | lower, 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /src/tools/getFramesForGroup/index.ts: -------------------------------------------------------------------------------- 1 | import type { Frame } from '@/types/Frame'; 2 | 3 | export const getFramesForGroup = (frames: Frame[], groupName: string): Frame[] => ( 4 | frames.reduce((acc: Frame[], frame: Frame): Frame[] => { 5 | const frameGroupIdRegex = /^(?[a-z]+)(?[0-9]+)/g; 6 | const group = frameGroupIdRegex.exec(frame.id)?.groups?.group; 7 | return (groupName === group) ? [...acc, frame] : acc; 8 | }, []) 9 | ); 10 | -------------------------------------------------------------------------------- /src/tools/getHandleFileImport/prepareFile.ts: -------------------------------------------------------------------------------- 1 | interface OldFile { 2 | blob: Blob, 3 | blobName?: string, 4 | contentType?: string, 5 | ok?: boolean, 6 | } 7 | 8 | export interface PreparedFile { 9 | file: File, 10 | contentType: string, 11 | } 12 | 13 | const prepareFile = (fileData: File | OldFile): PreparedFile => { 14 | let file: File; 15 | let contentType: string; 16 | 17 | if ((fileData as OldFile).blob) { 18 | // As of version 1.16.4 the filedata is an object like { blob, contentType } 19 | // earlier versions (and the NeoGB Printer) directly provide a blob 20 | const oldFile: OldFile = (fileData as OldFile); 21 | file = new File([oldFile.blob], oldFile.blobName || 'blob'); 22 | 23 | contentType = oldFile.contentType || 'application/unknown'; 24 | 25 | // v1.16.4 is missing the 'ok' property, hence the explicit check (may be removed in future versions if v0.3.5+ is successful) 26 | if (oldFile.ok !== undefined && !oldFile.ok) { 27 | throw new Error('Invalid file received from printer'); 28 | } 29 | 30 | } else { 31 | file = fileData as File; 32 | contentType = file.type; 33 | } 34 | 35 | if (!file.size) { 36 | throw new Error('empty file'); 37 | } 38 | 39 | return { 40 | file, 41 | contentType, 42 | }; 43 | }; 44 | 45 | export default prepareFile; 46 | -------------------------------------------------------------------------------- /src/tools/getImagePalettes/index.ts: -------------------------------------------------------------------------------- 1 | import type { RGBNPalette } from 'gb-image-decoder'; 2 | import { missingGreyPalette } from '@/consts/defaults'; 3 | import { isRGBNImage } from '@/tools/isRGBNImage'; 4 | import type { Image, MonochromeImage } from '@/types/Image'; 5 | import type { Palette } from '@/types/Palette'; 6 | 7 | interface GetImagePalettes { 8 | palette?: RGBNPalette | Palette, 9 | framePalette?: Palette 10 | } 11 | 12 | export const getImagePalettes = (palettes: Palette[], image: Image): GetImagePalettes => { 13 | if (isRGBNImage(image)) { 14 | const { palette } = image; 15 | return { 16 | palette: palette as RGBNPalette, 17 | }; 18 | } 19 | 20 | const monoImage = image as MonochromeImage; 21 | 22 | const palette = palettes.find(({ shortName }) => shortName === monoImage.palette) || missingGreyPalette; 23 | const framePalette = palettes.find(({ shortName }) => shortName === monoImage.framePalette) || missingGreyPalette; 24 | 25 | return { 26 | palette, 27 | framePalette: monoImage.lockFrame ? framePalette : palette, 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /src/tools/getMonochromeImageCreationParams/index.ts: -------------------------------------------------------------------------------- 1 | import type { MonochromeImageCreationParams } from 'gb-image-decoder'; 2 | import { BW_PALETTE_HEX } from 'gb-image-decoder'; 3 | 4 | interface Params { 5 | imagePalette?: string[], 6 | invertPalette?: boolean, 7 | framePalette?: string[], 8 | invertFramePalette?: boolean, 9 | } 10 | 11 | 12 | export const hexToNumeric = (hex: string): number => { 13 | const cleanHex = hex.replace('#', ''); 14 | if (!/^([0-9a-fA-F]{6})$/.test(cleanHex)) { 15 | throw new Error('Invalid hex color'); 16 | } 17 | 18 | return parseInt(cleanHex, 16); 19 | }; 20 | 21 | export const getMonochromeImageCreationParams = ({ 22 | imagePalette, 23 | framePalette, 24 | invertPalette = false, 25 | invertFramePalette = false, 26 | }: Params): Pick => { 27 | 28 | const iPalette = ((imagePalette || BW_PALETTE_HEX) as string[]).map(hexToNumeric); 29 | const fPalette = (framePalette?.map(hexToNumeric) || iPalette); 30 | 31 | return { 32 | imagePalette: invertPalette ? [...iPalette].reverse() : [...iPalette], 33 | framePalette: invertFramePalette ? [...fPalette].reverse() : [...fPalette], 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /src/tools/getPaletteSettings/index.ts: -------------------------------------------------------------------------------- 1 | import type { MonochromeImage } from '@/types/Image'; 2 | 3 | export interface PaletteSettings { 4 | invertPalette: boolean, 5 | invertFramePalette: boolean, 6 | } 7 | 8 | export const getPaletteSettings = (image: MonochromeImage): PaletteSettings => { 9 | const invertPalette = image.invertPalette || false; 10 | 11 | if (!image.lockFrame) { 12 | return { 13 | invertPalette, 14 | invertFramePalette: invertPalette, 15 | }; 16 | } 17 | 18 | const unsafeInvertFramePalette: boolean | undefined = image.invertFramePalette; 19 | const invertFramePalette = typeof unsafeInvertFramePalette === 'undefined' ? invertPalette : unsafeInvertFramePalette; 20 | 21 | 22 | return { 23 | invertPalette, 24 | invertFramePalette, 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /src/tools/getScrollParent/index.ts: -------------------------------------------------------------------------------- 1 | export const getScrollParent = (element: Element): Element => { 2 | let parent = element.parentElement; 3 | 4 | while (parent) { 5 | const style = getComputedStyle(parent); 6 | const overflowY = style.overflowY; 7 | const overflowX = style.overflowX; 8 | 9 | const isScrollableY = (overflowY === 'auto' || overflowY === 'scroll') && parent.scrollHeight > parent.clientHeight; 10 | const isScrollableX = (overflowX === 'auto' || overflowX === 'scroll') && parent.scrollWidth > parent.clientWidth; 11 | 12 | if (isScrollableY || isScrollableX) { 13 | return parent; 14 | } 15 | 16 | parent = parent.parentElement; 17 | } 18 | 19 | return document.scrollingElement || document.documentElement; 20 | }; 21 | -------------------------------------------------------------------------------- /src/tools/getSettings/getFrames.ts: -------------------------------------------------------------------------------- 1 | import { localforageFrames } from '@/tools/localforageInstance'; 2 | 3 | const getFrames = async (exportFrameHashes: string[]): Promise> => { 4 | const result = await Promise.all(exportFrameHashes.map(async (hash) => { 5 | const data = await localforageFrames.getItem(hash); 6 | return { 7 | hash, 8 | data, 9 | }; 10 | })); 11 | 12 | const frames: Record = {}; 13 | result.forEach(({ hash, data }) => { 14 | if (data) { 15 | frames[`frame-${hash}`] = data; 16 | } 17 | }); 18 | 19 | return frames; 20 | }; 21 | 22 | export default getFrames; 23 | -------------------------------------------------------------------------------- /src/tools/getSettings/getFramesForExport.ts: -------------------------------------------------------------------------------- 1 | import type { ExportTypes } from '@/consts/exportTypes'; 2 | import type { Frame } from '@/types/Frame'; 3 | 4 | const getFramesForExport = ( 5 | what: ExportTypes.CURRENT_FRAMEGROUP | ExportTypes.FRAMES, 6 | frames: Frame[], 7 | frameSetID = '', 8 | ): Frame[] => { 9 | 10 | switch (what) { 11 | case 'frames': 12 | // export all frames 13 | return frames; 14 | case 'current_framegroup': 15 | // export selected only 16 | return frames 17 | .filter(({ id }) => id.startsWith(frameSetID)); 18 | default: 19 | return []; 20 | } 21 | }; 22 | 23 | export default getFramesForExport; 24 | -------------------------------------------------------------------------------- /src/tools/getSettings/getImageHashesForExport.ts: -------------------------------------------------------------------------------- 1 | import type { Image } from '@/types/Image'; 2 | 3 | const getImageHashesForExport = (what: 'images' | 'selected_images', images: Image[], imageSelection: string[]): string[] => { 4 | 5 | switch (what) { 6 | case 'images': 7 | // export all images 8 | return images.map(({ hash }) => hash); 9 | case 'selected_images': 10 | // export selected only 11 | return imageSelection; 12 | default: 13 | return []; 14 | } 15 | }; 16 | 17 | export default getImageHashesForExport; 18 | -------------------------------------------------------------------------------- /src/tools/getSettings/getImages.ts: -------------------------------------------------------------------------------- 1 | import { isRGBNImage } from '@/tools/isRGBNImage'; 2 | import { localforageImages } from '@/tools/localforageInstance'; 3 | import unique from '@/tools/unique'; 4 | import type { Image, RGBNImage } from '@/types/Image'; 5 | 6 | const getImages = async (exportImages: Image[]): Promise> => { 7 | 8 | const exportImageHashes = exportImages.reduce((acc: string[], exportImage: Image): string[] => { 9 | const exportHashes: string[] = isRGBNImage(exportImage) ? 10 | unique(Object.values((exportImage as RGBNImage).hashes)) : 11 | [exportImage.hash]; 12 | 13 | return [ 14 | ...acc, 15 | ...exportHashes, 16 | ]; 17 | }, []); 18 | 19 | const result = await Promise.all(exportImageHashes.map(async (hash) => { 20 | const data = await localforageImages.getItem(hash); 21 | return ({ 22 | hash, 23 | data, 24 | }); 25 | })); 26 | 27 | const images: Record = {}; 28 | result.forEach(({ 29 | hash, 30 | data, 31 | }) => { 32 | if (data) { 33 | images[hash] = data; 34 | } 35 | }); 36 | 37 | return images; 38 | }; 39 | 40 | export default getImages; 41 | -------------------------------------------------------------------------------- /src/tools/getUploadFiles/index.ts: -------------------------------------------------------------------------------- 1 | import filterDeleteNew from '@/tools/filterDeleteNew'; 2 | import getPrepareRemoteFiles from '@/tools/getPrepareRemoteFiles'; 3 | import type { RepoContents, RepoTasks, SyncFile } from '@/types/Export'; 4 | import type { AddToQueueFn } from '@/types/Sync'; 5 | import { getUploadFrames } from './getUploadFrames'; 6 | import { getUploadImages } from './getUploadImages'; 7 | 8 | const getUploadFiles = async ( 9 | repoContents: RepoContents, 10 | lastUpdateUTC: number, 11 | addToQueue: AddToQueueFn, 12 | ): Promise => { 13 | const prepareRemoteFiles = getPrepareRemoteFiles(); 14 | const missingLocally: string[] = []; // ToDo: is this always empty? 15 | 16 | const { 17 | syncImages, 18 | missingLocally: missingImageHashes, 19 | } = await getUploadImages(repoContents, addToQueue as AddToQueueFn); 20 | 21 | missingLocally.push(...missingImageHashes); 22 | 23 | const { 24 | syncFrames, 25 | missingLocally: missingFrameHashes, 26 | } = await getUploadFrames(repoContents, addToQueue as AddToQueueFn); 27 | 28 | missingLocally.push(...missingFrameHashes); 29 | 30 | 31 | const syncFiles: SyncFile[] = [ 32 | ...syncImages, 33 | ...syncFrames, 34 | ]; 35 | 36 | const { toUpload, toKeep } = await prepareRemoteFiles(syncFiles, lastUpdateUTC); 37 | 38 | return filterDeleteNew(repoContents, toUpload, toKeep, missingLocally); 39 | }; 40 | 41 | export default getUploadFiles; 42 | -------------------------------------------------------------------------------- /src/tools/gitStorage/index.ts: -------------------------------------------------------------------------------- 1 | import { SyncDirection } from '@/consts/sync'; 2 | import useStoragesStore from '@/stores/storagesStore'; 3 | import type { JSONExportState } from '@/types/ExportState'; 4 | import type { GitStorageSettings } from '@/types/Sync'; 5 | 6 | export interface GitSyncTool { 7 | startSyncData: (direction: SyncDirection) => Promise, 8 | updateSettings: (gitSettings: GitStorageSettings) => Promise, 9 | } 10 | 11 | 12 | let gitSyncTool: GitSyncTool; 13 | 14 | export const gitStorageTool = ( 15 | remoteImport: (repoContents: JSONExportState) => Promise, 16 | ): GitSyncTool => { 17 | const loadAndInitMiddleware = async (): Promise => { 18 | if (!gitSyncTool) { 19 | const { init, gitSyncTool: tool } = await import(/* webpackChunkName: "syn" */ './main'); 20 | init(); 21 | gitSyncTool = gitSyncTool || tool(remoteImport); 22 | } 23 | 24 | return gitSyncTool; 25 | }; 26 | 27 | const enabled = (): boolean => { 28 | const { gitStorage: { 29 | use, 30 | owner, 31 | repo, 32 | branch, 33 | token, 34 | } } = useStoragesStore.getState(); 35 | 36 | return !!(use && owner && repo && branch && token); 37 | }; 38 | 39 | if (enabled()) { 40 | loadAndInitMiddleware(); 41 | } 42 | 43 | return { 44 | startSyncData: async (direction: SyncDirection) => ( 45 | (await loadAndInitMiddleware()).startSyncData(direction) 46 | ), 47 | updateSettings: async (gitSettings: GitStorageSettings) => ( 48 | (await loadAndInitMiddleware()).updateSettings(gitSettings) 49 | ), 50 | }; 51 | }; 52 | 53 | -------------------------------------------------------------------------------- /src/tools/handleLines/index.ts: -------------------------------------------------------------------------------- 1 | import { HandleLine } from '@/consts/handleLine'; 2 | import { ImportLine } from '@/types/handleLine'; 3 | 4 | 5 | const handleLines = (rawLine: string): ImportLine | null => { 6 | // commented lines are not saved 7 | if ( 8 | (rawLine.charAt(0) === '#') || 9 | (rawLine.charAt(0) === '/') 10 | ) { 11 | return null; 12 | } 13 | 14 | // ! or { indicates a command 15 | if ( 16 | (rawLine.charAt(0) === '!') || 17 | (rawLine.charAt(0) === '{') 18 | ) { 19 | try { 20 | const { command, margin_lower: marginLower } = JSON.parse(rawLine.slice(rawLine.indexOf('{')).trim()); 21 | // if (command === 'INIT') { 22 | // return { 23 | // type: CLEAR_LINES, 24 | // }; 25 | // } 26 | 27 | if (command === 'PRNT' && marginLower > 0) { 28 | return { 29 | type: HandleLine.IMAGE_COMPLETE, 30 | }; 31 | } 32 | } catch { 33 | return { 34 | type: HandleLine.PARSE_ERROR, 35 | payload: 'Error while trying to parse JSON data command block', 36 | }; 37 | } 38 | 39 | return null; 40 | } 41 | 42 | const cleanLine = rawLine.replace(/[^0-9A-F]/ig, ''); 43 | 44 | if (!cleanLine.length) { 45 | return null; 46 | } 47 | 48 | return { 49 | type: HandleLine.NEW_LINES, 50 | payload: [cleanLine], 51 | }; 52 | }; 53 | 54 | export default handleLines; 55 | -------------------------------------------------------------------------------- /src/tools/hexToRgbString/index.ts: -------------------------------------------------------------------------------- 1 | export const hexToRgbString = (hex: string): string => { 2 | const cleanHex = hex.replace('#', ''); 3 | if (!/^([0-9a-fA-F]{6})$/.test(cleanHex)) { 4 | throw new Error('Invalid hex color'); 5 | } 6 | 7 | const r = parseInt(cleanHex.slice(0, 2), 16); 8 | const g = parseInt(cleanHex.slice(2, 4), 16); 9 | const b = parseInt(cleanHex.slice(4, 6), 16); 10 | return `${r}, ${g}, ${b}`; 11 | }; 12 | -------------------------------------------------------------------------------- /src/tools/importExportSettings/getImportJSON.ts: -------------------------------------------------------------------------------- 1 | import type { ImportFn } from '@/hooks/useImportExportSettings'; 2 | import readFileAs, { ReadAs } from '@/tools/readFileAs'; 3 | import type { JSONExport } from '@/types/ExportState'; 4 | 5 | export const getImportJSON = (importFn: ImportFn) => async (file: File) => { 6 | const data = await readFileAs(file, ReadAs.TEXT); 7 | let settingsDump: JSONExport; 8 | 9 | try { 10 | settingsDump = JSON.parse(data); 11 | } catch { 12 | throw new Error('Not a valid .json file'); 13 | } 14 | 15 | if (!settingsDump?.state) { 16 | throw new Error('Not a settings .json file'); 17 | } 18 | 19 | importFn(settingsDump); 20 | 21 | return true; 22 | }; 23 | -------------------------------------------------------------------------------- /src/tools/isGoodScaleFactor/index.ts: -------------------------------------------------------------------------------- 1 | // noinspection JSBitwiseOperatorUsage 2 | const isPowerOfTwo = (v: number): boolean => ( 3 | // eslint-disable-next-line no-bitwise 4 | Boolean(v && !(v & (v - 1))) 5 | ); 6 | 7 | const isGoodScaleFactor = (scaleFactor: number): boolean => ( 8 | Math.floor(scaleFactor) === scaleFactor && 9 | isPowerOfTwo(scaleFactor) 10 | ); 11 | 12 | export default isGoodScaleFactor; 13 | -------------------------------------------------------------------------------- /src/tools/isRGBNImage/index.ts: -------------------------------------------------------------------------------- 1 | import type { RGBNPalette } from 'gb-image-decoder'; 2 | import type { Image, MonochromeImage, RGBNImage } from '@/types/Image'; 3 | 4 | export const isRGBNImage = (image: Image): boolean => Boolean( 5 | image.hasOwnProperty('hashes') && (image as RGBNImage).hashes, 6 | ); 7 | 8 | export const reduceImagesMonochrome = (acc: MonochromeImage[], image?: Image | null): MonochromeImage[] => ( 9 | (!image || isRGBNImage(image)) ? acc : [...acc, (image as MonochromeImage)] 10 | ); 11 | 12 | export const reduceImagesRGBN = (acc: RGBNImage[], image?: Image | null): RGBNImage[] => ( 13 | (!image || !isRGBNImage(image)) ? acc : [...acc, (image as RGBNImage)] 14 | ); 15 | 16 | export const isRGBNPalette = (palette: RGBNPalette | string): boolean => Boolean( 17 | (palette.hasOwnProperty('r') && (palette as RGBNPalette).r) || 18 | (palette.hasOwnProperty('g') && (palette as RGBNPalette).g) || 19 | (palette.hasOwnProperty('b') && (palette as RGBNPalette).b) || 20 | (palette.hasOwnProperty('n') && (palette as RGBNPalette).n) || 21 | (palette.hasOwnProperty('blend') && (palette as RGBNPalette).blend), 22 | ); 23 | -------------------------------------------------------------------------------- /src/tools/localforageInstance/index.ts: -------------------------------------------------------------------------------- 1 | import createWrappedInstance from './createWrappedInstance'; 2 | 3 | const localforageImages = createWrappedInstance({ 4 | name: 'GB Printer Web', 5 | storeName: 'gb-printer-web-images', 6 | }); 7 | 8 | const localforageFrames = createWrappedInstance({ 9 | name: 'GB Printer Web', 10 | storeName: 'gb-printer-web-frames', 11 | }); 12 | 13 | if (typeof window !== 'undefined') { 14 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 15 | // @ts-ignore 16 | window.lfi = localforageImages; 17 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 18 | // @ts-ignore 19 | window.lff = localforageFrames; 20 | } 21 | 22 | const localforageReady = async (): Promise => { 23 | await localforageFrames.ready(); 24 | await localforageImages.ready(); 25 | 26 | // Wait 5ms until "dummy" item is possibly removed 27 | await new Promise(((resolve) => { 28 | window.setTimeout(resolve, 5); 29 | })); 30 | }; 31 | 32 | export { 33 | localforageImages, 34 | localforageFrames, 35 | localforageReady, 36 | }; 37 | -------------------------------------------------------------------------------- /src/tools/modifyTagChanges/index.ts: -------------------------------------------------------------------------------- 1 | export enum TagUpdateMode { 2 | ADD = 'add', 3 | REMOVE = 'remove', 4 | } 5 | 6 | export interface TagUpdates { 7 | add: string[], 8 | remove: string[], 9 | } 10 | 11 | const modifyTagChanges = (initial: TagUpdates, { mode, tag }: { mode: TagUpdateMode, tag: string }): TagUpdates => { 12 | switch (mode) { 13 | case 'add': 14 | return { 15 | add: [...initial.add, tag], 16 | remove: [...initial.remove.filter((t) => t !== tag)], 17 | }; 18 | case 'remove': 19 | return { 20 | remove: [...initial.remove, tag], 21 | add: [...initial.add.filter((t) => t !== tag)], 22 | }; 23 | default: 24 | return initial; 25 | } 26 | }; 27 | 28 | export default modifyTagChanges; 29 | -------------------------------------------------------------------------------- /src/tools/pack/index.ts: -------------------------------------------------------------------------------- 1 | import pako from 'pako'; 2 | 3 | export const inflate = async (data: string): Promise => { 4 | // ToDo: @types/pako wrong? 5 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 6 | // @ts-ignore 7 | const inflated = pako.inflate(data, { to: 'string' }); 8 | 9 | return inflated; 10 | }; 11 | 12 | export const deflate = async (data: string): Promise => { 13 | // ToDo: @types/pako wrong? 14 | const compressed = pako.deflate(data, { 15 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 16 | // @ts-ignore 17 | to: 'string', 18 | strategy: 1, 19 | level: 8, 20 | }) as unknown as string; 21 | 22 | return compressed; 23 | }; 24 | 25 | -------------------------------------------------------------------------------- /src/tools/padToHeight/index.ts: -------------------------------------------------------------------------------- 1 | const pad = 'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'; 2 | 3 | const padToHeight = (tiles: string[]): string[] => { 4 | 5 | // Fill up to a full line 6 | while (tiles.length % 20) { 7 | tiles.push(pad); 8 | } 9 | 10 | const padLine = (new Array(20)).fill(pad); 11 | 12 | while (tiles.length < 360) { 13 | tiles.push(...padLine); 14 | 15 | if (tiles.length < 360) { 16 | tiles.unshift(...padLine); 17 | } 18 | } 19 | 20 | return tiles; 21 | }; 22 | 23 | export default padToHeight; 24 | -------------------------------------------------------------------------------- /src/tools/parseAuthParams/index.ts: -------------------------------------------------------------------------------- 1 | const parseAuthParams = (): { dropboxCode?: string } => { 2 | const searchParams = new URLSearchParams(window.location.search); 3 | 4 | // for now there's only dropbox oauth support, and dropbox redirects with the param 'code' 5 | const dropboxCode = searchParams.get('code'); 6 | 7 | if (dropboxCode) { 8 | window.history.replaceState({}, '', window.location.pathname); 9 | return { 10 | dropboxCode, 11 | }; 12 | } 13 | 14 | return {}; 15 | }; 16 | 17 | export default parseAuthParams; 18 | -------------------------------------------------------------------------------- /src/tools/randomId/index.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | 3 | export const randomId = uuidv4; 4 | -------------------------------------------------------------------------------- /src/tools/readFileAs/index.ts: -------------------------------------------------------------------------------- 1 | export enum ReadAs { 2 | UINT8_ARRAY = 'uint8array', 3 | TEXT = 'text', 4 | DATA_URL = 'dataURL', 5 | } 6 | 7 | function readFileAs(file: File | Blob, readAs: ReadAs.UINT8_ARRAY): Promise; 8 | function readFileAs(file: File | Blob, readAs: ReadAs.TEXT): Promise; 9 | function readFileAs(file: File | Blob, readAs: ReadAs.DATA_URL): Promise; 10 | 11 | function readFileAs(file: File | Blob, readAs: ReadAs): Promise { 12 | return new Promise((resolve, reject) => { 13 | const reader = new FileReader(); 14 | 15 | reader.onload = ({ target }) => { 16 | if (target?.result) { 17 | if (readAs === ReadAs.UINT8_ARRAY) { 18 | resolve(Buffer.from(target.result as ArrayBuffer)); 19 | return; 20 | } 21 | 22 | resolve(target.result as string); 23 | return; 24 | } 25 | 26 | reject(new Error('Filereader has no result')); 27 | }; 28 | 29 | reader.onerror = (error) => { 30 | reject(error); 31 | }; 32 | 33 | switch (readAs) { 34 | case ReadAs.UINT8_ARRAY: 35 | reader.readAsArrayBuffer(file); 36 | break; 37 | case ReadAs.TEXT: 38 | reader.readAsText(file); 39 | break; 40 | case ReadAs.DATA_URL: 41 | reader.readAsDataURL(file); 42 | break; 43 | default: 44 | reject(new Error(`readAs param missing or unknown type "${readAs}"`)); 45 | } 46 | }); 47 | } 48 | 49 | export default readFileAs; 50 | -------------------------------------------------------------------------------- /src/tools/reduceArray/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export const reduceItems = (acc: T[], item?: T | null): T[] => ( 3 | !item ? acc : [...acc, item] 4 | ); 5 | -------------------------------------------------------------------------------- /src/tools/remote/commands/checkPrinter.ts: -------------------------------------------------------------------------------- 1 | import type { CheckPrinterStatus, PrinterInfo } from '@/types/Printer'; 2 | 3 | const checkPrinter = async (): Promise => { 4 | const res = await fetch('/dumps/list'); 5 | const data = await res.json() as PrinterInfo; 6 | 7 | if (data.fs && (data.fs.dumpcount !== data.dumps.length)) { 8 | throw new Error('Inconststent image count received from printer.'); 9 | } 10 | 11 | return { 12 | printerData: { 13 | ...data, 14 | dumps: [...data.dumps].sort(), 15 | }, 16 | }; 17 | }; 18 | 19 | export default checkPrinter; 20 | -------------------------------------------------------------------------------- /src/tools/remote/commands/clearPrinter.ts: -------------------------------------------------------------------------------- 1 | import { type CheckPrinterStatus } from '@/types/Printer'; 2 | import checkPrinter from './checkPrinter'; 3 | 4 | const clearPrinter = async (): Promise => { 5 | const res = await fetch('/dumps/clear'); 6 | const { deleted } = await res.json(); 7 | 8 | if (deleted !== undefined) { 9 | return checkPrinter(); 10 | } 11 | 12 | throw new Error('error while deleting images'); 13 | }; 14 | 15 | export default clearPrinter; 16 | -------------------------------------------------------------------------------- /src/tools/remote/commands/testFile.ts: -------------------------------------------------------------------------------- 1 | import dummyImage from '@/components/Import/dummy'; 2 | import type { PrinterTestFile } from '@/types/Printer'; 3 | 4 | const testFile = async (): Promise => { 5 | return { lines: dummyImage }; 6 | }; 7 | 8 | export default testFile; 9 | -------------------------------------------------------------------------------- /src/tools/remote/fetchDumpRetry.ts: -------------------------------------------------------------------------------- 1 | import type { BlobResponse } from '@/types/Printer'; 2 | 3 | const fetchDumpRetry = async (url: string, retries: number): Promise => { 4 | let res: Response; 5 | 6 | try { 7 | res = await fetch(url); 8 | } catch (error) { 9 | if (retries <= 1) { 10 | throw error; 11 | } 12 | 13 | return fetchDumpRetry(url, retries - 1); 14 | } 15 | 16 | const headers: Record = {}; 17 | res.headers.forEach((value: string, key: string) => { 18 | headers[key] = value; 19 | }); 20 | 21 | return ( 22 | res.blob() 23 | .then((blob: Blob): BlobResponse => ({ 24 | blob, 25 | contentType: res.headers.get('content-type') || undefined, 26 | meta: { 27 | headers, 28 | }, 29 | status: res.status, 30 | ok: res.ok, 31 | })) 32 | ); 33 | }; 34 | 35 | export default fetchDumpRetry; 36 | -------------------------------------------------------------------------------- /src/tools/remote/startHeartbeat.ts: -------------------------------------------------------------------------------- 1 | import type { RemoteEnv, RemotePrinterEvent } from '@/types/Printer'; 2 | 3 | type cleanupFn = () => void; 4 | 5 | const startHeartbeat = ({ targetWindow }: RemoteEnv, commands: string[]): cleanupFn => { 6 | 7 | const heartBeat = () => { 8 | if (!targetWindow) { return; } 9 | 10 | targetWindow.postMessage({ 11 | fromRemotePrinter: { 12 | height: document.body.getBoundingClientRect().height, 13 | commands, 14 | }, 15 | } as RemotePrinterEvent, '*'); 16 | }; 17 | 18 | const interval = window.setInterval(heartBeat, 500); 19 | 20 | return () => { 21 | window.clearInterval(interval); 22 | }; 23 | }; 24 | 25 | export default startHeartbeat; 26 | -------------------------------------------------------------------------------- /src/tools/replaceDuplicateFilenames/index.ts: -------------------------------------------------------------------------------- 1 | import type { DownloadArrayBuffer } from '@/types/download'; 2 | 3 | const replaceDuplicateFilenames = (files: DownloadArrayBuffer[]): DownloadArrayBuffer[] => { 4 | 5 | const filenames: string[] = []; 6 | 7 | return files.map((file): DownloadArrayBuffer => { 8 | const { filename } = file; 9 | const fnParts = filename.split('.'); 10 | const ext = fnParts.pop(); 11 | 12 | if (!ext) { 13 | throw new Error('unknown file type'); 14 | } 15 | 16 | const baseName = fnParts.join('.'); 17 | let uFilename = filename; 18 | let tries = 0; 19 | 20 | while (filenames.includes(uFilename)) { 21 | tries += 1; 22 | uFilename = `${baseName}_(${tries}).${ext}`; 23 | } 24 | 25 | if (!filenames.includes(uFilename)) { 26 | filenames.push(uFilename); 27 | } 28 | 29 | return { 30 | ...file, 31 | filename: uFilename, 32 | }; 33 | }); 34 | }; 35 | 36 | export default replaceDuplicateFilenames; 37 | -------------------------------------------------------------------------------- /src/tools/saveNewImage/index.ts: -------------------------------------------------------------------------------- 1 | import { save } from '@/tools/storage'; 2 | import { toCreationDate } from '@/tools/toCreationDate'; 3 | import type { MonochromeImage } from '@/types/Image'; 4 | 5 | interface ImageRawData extends Pick { 6 | lines: string[], 7 | filename: string, 8 | } 9 | 10 | 11 | const saveNewImage = async ({ 12 | lines, 13 | filename, 14 | palette, 15 | frame, 16 | tags = [], 17 | meta, 18 | created = toCreationDate(), 19 | }: ImageRawData): Promise => { 20 | const dataHash = await save(lines); 21 | 22 | return { 23 | hash: dataHash, 24 | created, 25 | title: filename || '', 26 | lines: lines.length, 27 | tags, 28 | palette, 29 | framePalette: palette, 30 | invertFramePalette: false, 31 | invertPalette: false, 32 | frame, 33 | meta, 34 | }; 35 | }; 36 | 37 | export default saveNewImage; 38 | -------------------------------------------------------------------------------- /src/tools/shorten/index.ts: -------------------------------------------------------------------------------- 1 | export const shorten = (text: string, length: number): string => { 2 | if (text.length < length) { 3 | return text; 4 | } 5 | 6 | return `${text.slice(0, length)}…`; 7 | }; 8 | -------------------------------------------------------------------------------- /src/tools/sortImages/index.ts: -------------------------------------------------------------------------------- 1 | import type { Image } from '@/types/Image'; 2 | 3 | interface Sortable { 4 | sortIndex: number, 5 | } 6 | 7 | interface UnSortable { 8 | sortIndex?: number, 9 | } 10 | 11 | type SortableImage = Sortable & Image; 12 | type UnSortableImage = UnSortable & Image; 13 | 14 | const addSortIndex = (image: Image, sortIndex: number): SortableImage => ({ 15 | ...image, 16 | sortIndex, 17 | }); 18 | 19 | const removeSortIndex = (image: SortableImage): Image => ({ 20 | ...image, 21 | sortIndex: undefined, 22 | } as UnSortableImage as Image); 23 | 24 | const sortImages = (sortBy?: string) => (a: SortableImage, b: SortableImage) => { 25 | 26 | if (!sortBy) { 27 | return 0; 28 | } 29 | 30 | const [sortByKey, sortByDirection] = sortBy.split('_'); 31 | 32 | const sortA = a[sortByKey as ('created' | 'title' | 'palette')]; 33 | const sortB = b[sortByKey as ('created' | 'title' | 'palette')]; 34 | const sortDirection = sortByDirection === 'desc' ? -1 : 1; 35 | 36 | if (!sortA || !sortB) { 37 | return 0; 38 | } 39 | 40 | if (sortA > sortB) { 41 | return sortDirection; 42 | } 43 | 44 | if (sortA < sortB) { 45 | return -sortDirection; 46 | } 47 | 48 | return a.sortIndex < b.sortIndex ? sortDirection : -sortDirection; 49 | }; 50 | 51 | 52 | export { 53 | addSortIndex, 54 | removeSortIndex, 55 | sortImages, 56 | }; 57 | -------------------------------------------------------------------------------- /src/tools/sortby/index.ts: -------------------------------------------------------------------------------- 1 | export enum SortDirection { 2 | ASC = 'asc', 3 | DESC = 'desc', 4 | } 5 | 6 | const sortBy = (key: keyof T, direction = SortDirection.ASC) => (arr: T[]): T[] => { 7 | 8 | const dir = direction === SortDirection.DESC ? -1 : 1; 9 | 10 | return ( 11 | [...arr].sort((a, b) => { 12 | if (typeof a[key] === 'string') { 13 | return (a[key] as string).localeCompare(b[key] as string); 14 | } 15 | 16 | if (a[key] > b[key]) { 17 | return dir; 18 | } 19 | 20 | if (a[key] < b[key]) { 21 | return -dir; 22 | } 23 | 24 | return 0; 25 | }) 26 | ); 27 | }; 28 | 29 | export default sortBy; 30 | -------------------------------------------------------------------------------- /src/tools/storage/dummyImage.ts: -------------------------------------------------------------------------------- 1 | import textToTiles from '@/tools/textToTiles'; 2 | 3 | const dummyImage = (hash: string): string[] => { 4 | const text = ` 5 | The following hash is missing in 6 | your indexedDb: 7 | ${hash} 8 | 9 | Either you imported a debug dump 10 | or your browser decided to do a 11 | cleanup. 12 | 13 | This image might be able to be 14 | recovered if you have set up git 15 | or dropbox sync. 16 | `.trim(); 17 | 18 | return textToTiles(text); 19 | }; 20 | 21 | export default dummyImage; 22 | -------------------------------------------------------------------------------- /src/tools/supportedCanvasImageFormats/index.ts: -------------------------------------------------------------------------------- 1 | const supports: Record = {}; 2 | 3 | const supportsFileType = (fileType: string) => { 4 | if (typeof document === 'undefined') { 5 | return false; 6 | } 7 | 8 | if (supports[fileType] !== undefined) { 9 | return supports[fileType]; 10 | } 11 | 12 | const canvas = document.createElement('canvas'); 13 | canvas.width = 2; 14 | canvas.height = 2; 15 | const imgData = canvas.toDataURL(`image/${fileType}`); 16 | 17 | supports[fileType] = imgData.indexOf(`data:image/${fileType};base64`) === 0; 18 | return supports[fileType]; 19 | }; 20 | 21 | const supportedCanvasImageFormats = () => ( 22 | ['jpeg', 'jpg', 'png', 'webp', 'bmp', 'gif'].filter((fileType) => supportsFileType(fileType)) 23 | ); 24 | 25 | export default supportedCanvasImageFormats; 26 | -------------------------------------------------------------------------------- /src/tools/textToTiles/index.ts: -------------------------------------------------------------------------------- 1 | import getChars from '@/tools/chars'; 2 | 3 | const black = 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\n'; 4 | 5 | const textToTiles = (text: string): string[] => { 6 | const result = []; 7 | const lines = text.split('\n') 8 | .map((line) => line.match(/.{1,32}/g)) 9 | .flat() 10 | .map((line) => line || '') 11 | .map((line = '') => line.padEnd(32, ' ')) 12 | .map((line) => ( 13 | line.match(/.{1,2}/g) 14 | ?.map((chars) => getChars(chars)) || 15 | [] 16 | )) 17 | .map((line) => [black, black, ...line, black, black]) 18 | .flat(); 19 | 20 | result.push(...[...Array(40)].map(() => black)); 21 | 22 | // Actual Text 23 | result.push(...lines); 24 | 25 | while (result.length < 360) { 26 | result.push(black); 27 | } 28 | 29 | return result; 30 | }; 31 | 32 | export default textToTiles; 33 | -------------------------------------------------------------------------------- /src/tools/toCreationDate/index.ts: -------------------------------------------------------------------------------- 1 | 2 | // Outputs a date in YYYY-MM-DD HH:mm:ss:SSS format 3 | export const toCreationDate = (date?: number | string | Date): string => { 4 | const creationDate = date ? new Date(date) : new Date(); 5 | 6 | const pad = (num: number, size = 2) => String(num).padStart(size, '0'); 7 | 8 | return [ 9 | creationDate.getFullYear(), 10 | '-', 11 | pad(creationDate.getMonth() + 1), 12 | '-', 13 | pad(creationDate.getDate()), 14 | ' ', 15 | pad(creationDate.getHours()), 16 | ':', 17 | pad(creationDate.getMinutes()), 18 | ':', 19 | pad(creationDate.getSeconds()), 20 | ':', 21 | pad(creationDate.getMilliseconds(), 3), 22 | ] 23 | .join(''); 24 | }; 25 | -------------------------------------------------------------------------------- /src/tools/transferLocalStorage/index.ts: -------------------------------------------------------------------------------- 1 | import { localforageImages, localforageFrames } from '@/tools/localforageInstance'; 2 | 3 | const transferLocalStorage = (): Promise => ( 4 | new Promise(((resolve) => { 5 | 6 | // Migrate Frames 7 | Object.keys(localStorage) 8 | .filter((key) => ( 9 | key.startsWith('gbp-web-frame-') 10 | )) 11 | .map((key) => ({ 12 | key, 13 | newKey: key.replace(/^gbp-web-frame-/gi, ''), 14 | })) 15 | .forEach(({ key, newKey }) => { 16 | const data = localStorage.getItem(key); 17 | if (data) { 18 | localforageFrames.setItem(newKey, data); 19 | localStorage.removeItem(key); 20 | } 21 | }); 22 | 23 | // Migrate Images 24 | Object.keys(localStorage) 25 | .filter((key) => ( 26 | key !== 'gbp-web-state' && 27 | key !== 'gbp-web-theme' && 28 | key.startsWith('gbp-web-') && 29 | !key.startsWith('gbp-web-frame-') 30 | )) 31 | .map((key) => ({ 32 | key, 33 | newKey: key.replace(/^gbp-web-/gi, ''), 34 | })) 35 | .forEach(({ key, newKey }) => { 36 | const data = localStorage.getItem(key); 37 | if (data) { 38 | localforageImages.setItem(newKey, data); 39 | localStorage.removeItem(key); 40 | } 41 | }); 42 | 43 | resolve(); 44 | })) 45 | ); 46 | 47 | export default transferLocalStorage; 48 | -------------------------------------------------------------------------------- /src/tools/transformBitmaps/index.ts: -------------------------------------------------------------------------------- 1 | import useImportsStore from '@/stores/importsStore'; 2 | import { moveBitmapsToImport } from '@/tools/moveBitmapsToImport'; 3 | import getImageData from './getImageData'; 4 | 5 | export const transformBitmaps = async (file: File, fromPrinter = false): Promise => { 6 | const image = await getImageData(file); 7 | 8 | if (fromPrinter) { 9 | moveBitmapsToImport({ 10 | bitmapQueue: [image], 11 | dither: false, 12 | contrastBaseValues: [0x00, 0x55, 0xAA, 0xFF], 13 | importQueueAdd: useImportsStore.getState().importQueueAdd, 14 | }); 15 | } else { 16 | useImportsStore.getState().bitmapQueueAdd([image]); 17 | } 18 | 19 | return true; 20 | }; 21 | 22 | -------------------------------------------------------------------------------- /src/tools/transformCapture/index.ts: -------------------------------------------------------------------------------- 1 | import { parseDefaultToClassic } from 'gbp-decode'; 2 | 3 | const transformCapture = (dumpText: string): string[][] => { 4 | 5 | const bytes: number[] = dumpText 6 | .split('\n') 7 | .map((line) => line.trim()) 8 | .filter((line) => ( 9 | line.length && 10 | line.indexOf('//') !== 0 && 11 | line.indexOf('/*') !== 0 12 | )) 13 | .map((line) => line.split(' ')) 14 | .flat() 15 | .map((cc) => parseInt(cc, 16)) 16 | .filter((n) => !isNaN(n)); 17 | 18 | return parseDefaultToClassic(bytes); 19 | }; 20 | 21 | export default transformCapture; 22 | -------------------------------------------------------------------------------- /src/tools/transformClassic/index.ts: -------------------------------------------------------------------------------- 1 | import { terminatorLine } from '@/consts/defaults'; 2 | import { HandleLine } from '@/consts/handleLine'; 3 | import handleLines from '@/tools/handleLines'; 4 | import { ImportLine } from '@/types/handleLine'; 5 | 6 | const transformClassic = (data: string, filename: string): string[][] => { 7 | const imagesFromFile = `${data}\n${terminatorLine}`.split('\n') 8 | .map(handleLines) 9 | .reduce((acc: string[][], lineAction: ImportLine | null) => { 10 | switch (lineAction?.type) { 11 | case HandleLine.NEW_LINES: { 12 | acc[acc.length - 1].push(...lineAction.payload); 13 | return acc; 14 | } 15 | 16 | case HandleLine.IMAGE_COMPLETE: { 17 | return [...acc, []]; 18 | } 19 | 20 | default: 21 | return acc; 22 | } 23 | 24 | }, [[]]) 25 | .filter((image: string[]) => ( 26 | image.length > 0 27 | )); 28 | 29 | if (!imagesFromFile.length) { 30 | console.warn(`File ${filename} did not contain images`); 31 | } 32 | 33 | return imagesFromFile; 34 | }; 35 | 36 | export default transformClassic; 37 | -------------------------------------------------------------------------------- /src/tools/transformPlainText/index.ts: -------------------------------------------------------------------------------- 1 | import useImportsStore from '@/stores/importsStore'; 2 | import { randomId } from '@/tools/randomId'; 3 | import readFileAs, { ReadAs } from '@/tools/readFileAs'; 4 | import { compressAndHash } from '@/tools/storage'; 5 | import transformCapture from '@/tools/transformCapture'; 6 | import transformClassic from '@/tools/transformClassic'; 7 | 8 | export const transformPlainText = async (file: File) => { 9 | const { importQueueAdd } = useImportsStore.getState(); 10 | const data: string = await readFileAs(file, ReadAs.TEXT); 11 | let result: string[][]; 12 | 13 | // file must contain something that resembles a gb printer command 14 | if (data.indexOf('{"command"') !== -1) { 15 | result = await transformClassic(data, file.name); 16 | } else { 17 | result = await transformCapture(data); 18 | } 19 | 20 | await Promise.all(result.map(async (tiles: string[], index: number): Promise => { 21 | const { dataHash: imageHash } = await compressAndHash(tiles); 22 | 23 | const indexCount = result.length < 2 ? '' : ` ${(index + 1).toString(10) 24 | .padStart(2, '0')}`; 25 | 26 | importQueueAdd([{ 27 | fileName: `${file.name}${indexCount}`, 28 | imageHash, 29 | tiles, 30 | lastModified: file.lastModified ? (file.lastModified + index) : undefined, 31 | tempId: randomId(), 32 | }]); 33 | 34 | return true; 35 | })); 36 | 37 | return true; 38 | }; 39 | -------------------------------------------------------------------------------- /src/tools/transformReduced/index.ts: -------------------------------------------------------------------------------- 1 | import { parsePicoToClassic } from 'gbp-decode'; 2 | import useImportsStore from '@/stores/importsStore'; 3 | import { randomId } from '@/tools/randomId'; 4 | import readFileAs, { ReadAs } from '@/tools/readFileAs'; 5 | import { compressAndHash } from '@/tools/storage'; 6 | 7 | export const transformReduced = async (file: File): Promise => { 8 | const { importQueueAdd } = useImportsStore.getState(); 9 | const data = await readFileAs(file, ReadAs.UINT8_ARRAY); 10 | 11 | const result: string[][] = await parsePicoToClassic(data); 12 | 13 | await Promise.all(result.map(async (tiles: string[], index: number) => { 14 | const { dataHash: imageHash } = await compressAndHash(tiles); 15 | 16 | const indexCount = result.length < 2 ? '' : ` (${index + 1})`; 17 | 18 | importQueueAdd([{ 19 | fileName: `${file.name}${indexCount}`, 20 | imageHash, 21 | tiles, 22 | lastModified: file.lastModified ? (file.lastModified + index) : undefined, 23 | tempId: randomId(), 24 | }]); 25 | 26 | return true; 27 | })); 28 | 29 | return true; 30 | }; 31 | -------------------------------------------------------------------------------- /src/tools/transformSav/mapCartFrameToHash.ts: -------------------------------------------------------------------------------- 1 | import type { Frame } from '@/types/Frame'; 2 | 3 | const mapCartFrameToHash = (frameNumber: number, savFrameTypes: string, frames: Frame[]): string => { 4 | if (!savFrameTypes) { 5 | return ''; 6 | } 7 | 8 | const findFrame = (frameId: string) => frames.find(({ id }) => id === frameId); 9 | 10 | const paddedFrameNumber = (frameNumber + 1).toString(10).padStart(2, '0'); 11 | 12 | const exactFrameId = `${savFrameTypes}${paddedFrameNumber}`; 13 | const foundExactFrame = findFrame(exactFrameId); 14 | if (foundExactFrame?.hash) { 15 | return foundExactFrame.hash; 16 | } 17 | 18 | // for jp frame, try to fall back to int frames, as jp/int share a lot 19 | if (savFrameTypes === 'jp') { 20 | const intFrameId = `int${paddedFrameNumber}`; 21 | const foundIntframe = findFrame(intFrameId); 22 | if (foundIntframe?.hash) { 23 | return foundIntframe.hash; 24 | } 25 | } 26 | 27 | // for custom frames first fall back to the xxx01 frame... 28 | const firstFrameId = `${savFrameTypes}01`; 29 | const foundFirstFrame = findFrame(firstFrameId); 30 | if (foundFirstFrame?.hash) { 31 | return foundFirstFrame.hash; 32 | } 33 | 34 | // ... and try the int frames last 35 | if (savFrameTypes !== 'jp') { 36 | const intFrameId = `int${paddedFrameNumber}`; 37 | const foundIntFrame = findFrame(intFrameId); 38 | if (foundIntFrame?.hash) { 39 | return foundIntFrame.hash; 40 | } 41 | } 42 | 43 | return findFrame('int01')?.hash || ''; 44 | }; 45 | 46 | export default mapCartFrameToHash; 47 | -------------------------------------------------------------------------------- /src/tools/transformSav/transformImage.ts: -------------------------------------------------------------------------------- 1 | const black = 'ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff'; 2 | const white = '00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'; 3 | 4 | const transformImage = (data: Uint8Array, baseAddress: number): string[] | null => { 5 | const transformed: string[] = []; 6 | let currentLine = ''; 7 | let hasData = false; 8 | 9 | // add black upper frame placeholder 10 | transformed.push(...[...Array(40)].map(() => black)); 11 | 12 | for (let i = 0; i < 0x0E00; i += 1) { 13 | if (i % 256 === 0) { 14 | // add left frame placeholder 15 | transformed.push(...[...Array(2)].map(() => black)); 16 | } 17 | 18 | currentLine += ` ${data[baseAddress + i].toString(16) 19 | .padStart(2, '0')}`; 20 | 21 | if (i % 16 === 15) { 22 | transformed.push(currentLine.trim()); 23 | 24 | // track if an image has actual data inside to prevent importing the white or black image all the time 25 | if (!hasData && currentLine.trim() !== white && currentLine.trim() !== black) { 26 | hasData = true; 27 | } 28 | 29 | currentLine = ''; 30 | } 31 | 32 | if (i % 256 === 255) { 33 | // add right frame placeholder 34 | transformed.push(...[...Array(2)].map(() => black)); 35 | } 36 | } 37 | 38 | // add lower frame placeholder 39 | transformed.push(...[...Array(40)].map(() => black)); 40 | 41 | return hasData ? transformed : null; 42 | }; 43 | 44 | export default transformImage; 45 | -------------------------------------------------------------------------------- /src/tools/unique/by.ts: -------------------------------------------------------------------------------- 1 | const uniqueBy = (key: keyof T) => (arr: T[]): T[] => { 2 | const seen = new Set(); 3 | return arr.filter((item) => { 4 | const k = item[key]; 5 | if (seen.has(k)) return false; 6 | seen.add(k); 7 | return true; 8 | }); 9 | }; 10 | 11 | export default uniqueBy; 12 | -------------------------------------------------------------------------------- /src/tools/unique/index.ts: -------------------------------------------------------------------------------- 1 | const unique = (arr: string[]): string[] => Array.from(new Set(arr)); 2 | 3 | export default unique; 4 | -------------------------------------------------------------------------------- /src/types/BitmapFilter.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface FilterColor { 3 | r: number, 4 | g: number, 5 | b: number, 6 | } 7 | 8 | export interface ApplyBitmapFilterOptions{ 9 | targetCanvas: HTMLCanvasElement, 10 | originalCanvas: HTMLCanvasElement, 11 | imageData: ImageData, 12 | contrastBaseValues: number[], 13 | dither: boolean, 14 | palette: string[], 15 | } 16 | 17 | export interface DitherFilterOptions { 18 | imageData: ImageData, 19 | contrastBaseValues: number[], 20 | dither: boolean, 21 | colors: FilterColor[], 22 | } 23 | 24 | export interface GeneratePatternOptions { 25 | baseValues: [number[], number[], number[]], 26 | orderPatterns: [number[], number[], number[]], 27 | } 28 | -------------------------------------------------------------------------------- /src/types/Export.d.ts: -------------------------------------------------------------------------------- 1 | import type { JSONExportState } from './ExportState'; 2 | import type { DownloadInfo, UploadFile } from './Sync'; 3 | 4 | export interface RepoFile { 5 | hash: string, 6 | name: string, 7 | path: string, 8 | getFileContent: () => Promise, 9 | } 10 | 11 | export interface DropBoxRepoFile extends RepoFile { 12 | contentHash: string, 13 | } 14 | 15 | export interface RepoContents { 16 | images: RepoFile[], 17 | frames: RepoFile[], 18 | settings: JSONExportState 19 | } 20 | 21 | export interface SyncFile { 22 | hash: string, 23 | files: DownloadInfo[], 24 | inRepo: RepoFile[], 25 | } 26 | 27 | export interface RepoTasks { 28 | upload: UploadFile[], 29 | del: RepoFile[], 30 | } 31 | -------------------------------------------------------------------------------- /src/types/ExportState.d.ts: -------------------------------------------------------------------------------- 1 | import type { Values } from '@/stores/itemsStore'; 2 | 3 | interface ExportableState extends Partial { 4 | lastUpdateUTC: number, 5 | version: number, 6 | } 7 | 8 | export interface JSONExportState { 9 | state: ExportableState, 10 | } 11 | 12 | export interface JSONExportBinary { 13 | [k: string]: string, 14 | } 15 | 16 | 17 | export type JSONExport = JSONExportState & JSONExportBinary; 18 | -------------------------------------------------------------------------------- /src/types/Frame.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * On Type-Changes, a history for migration must be kept in /src/javascript/app/stores/migrations/history/ 3 | * */ 4 | export interface Frame { 5 | id: string, 6 | hash: string, 7 | name: string, 8 | lines: number, 9 | tempId?: string, 10 | } 11 | -------------------------------------------------------------------------------- /src/types/FrameGroup.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * On Type-Changes, a history for migration must be kept in /src/javascript/app/stores/migrations/history/ 3 | * */ 4 | export interface FrameGroup { 5 | id: string, 6 | name: string, 7 | } 8 | -------------------------------------------------------------------------------- /src/types/Image.d.ts: -------------------------------------------------------------------------------- 1 | import type { RGBNPalette, Rotation } from 'gb-image-decoder'; 2 | 3 | export interface ImageMetadata extends Record{ 4 | romType?: string, 5 | userId?: string, 6 | birthDate?: string, 7 | userName?: string, 8 | gender?: string, 9 | bloodType?: string, 10 | comment?: string, 11 | isCopy?: boolean, 12 | exposure?: string, 13 | captureMode?: string, 14 | edgeExclusive?: string, 15 | edgeOperation?: string, 16 | gain?: string, 17 | edgeMode?: string, 18 | invertOut?: string, 19 | voltageRef?: string, 20 | zeroPoint?: string, 21 | vOut?: string, 22 | } 23 | 24 | interface CommonImage { 25 | hash: string, 26 | created: string, 27 | title: string, 28 | frame?: string, 29 | tags: string[], 30 | lockFrame?: boolean, 31 | rotation?: Rotation, 32 | meta?: ImageMetadata 33 | } 34 | 35 | export interface RGBNHashes { 36 | r?: string, 37 | g?: string, 38 | b?: string, 39 | n?: string, 40 | } 41 | 42 | export interface RGBNImage extends CommonImage { 43 | palette: RGBNPalette, 44 | hashes: RGBNHashes, 45 | } 46 | 47 | export interface MonochromeImage extends CommonImage { 48 | lines: number, 49 | palette: string, 50 | invertPalette: boolean, 51 | framePalette: string, 52 | invertFramePalette: boolean, 53 | } 54 | 55 | /* 56 | * On Type-Changes, a history for migration must be kept in /src/javascript/app/stores/migrations/history/ 57 | * */ 58 | export type Image = MonochromeImage | RGBNImage 59 | 60 | export interface CurrentEditBatch { 61 | batch?: string[], 62 | tags?: string[], 63 | } 64 | -------------------------------------------------------------------------------- /src/types/ImageActions.d.ts: -------------------------------------------------------------------------------- 1 | import type { Image, MonochromeImage } from './Image'; 2 | 3 | export type ImageUpdates = 4 | Pick & 5 | Pick; 6 | -------------------------------------------------------------------------------- /src/types/ImageGroup.d.ts: -------------------------------------------------------------------------------- 1 | import type { Image } from './Image'; 2 | 3 | export interface BaseImageGroup { 4 | id: string, 5 | slug: string, 6 | created: string, 7 | title: string, 8 | coverImage: string, 9 | } 10 | 11 | /* 12 | * On Type-Changes, a history for migration must be kept in /src/javascript/app/stores/migrations/history/ 13 | * */ 14 | export interface SerializableImageGroup extends BaseImageGroup { 15 | groups: string[], 16 | images: string[], 17 | } 18 | 19 | export interface TreeImageGroup extends BaseImageGroup { 20 | groups: TreeImageGroup[], 21 | images: Image[], 22 | tags: string[], 23 | allImages: Image[], 24 | } 25 | -------------------------------------------------------------------------------- /src/types/ImportItem.d.ts: -------------------------------------------------------------------------------- 1 | import { Image, ImageMetadata } from './Image'; 2 | 3 | export interface ImportItem { 4 | fileName:string, 5 | imageHash:string, 6 | tiles: string[], 7 | lastModified?: number, 8 | tempId: string, 9 | meta?: ImageMetadata, 10 | } 11 | 12 | export interface FlaggedImportItem extends ImportItem { 13 | isDuplicateInQueue: boolean; 14 | alreadyImported: Image | null; 15 | } 16 | -------------------------------------------------------------------------------- /src/types/Palette.d.ts: -------------------------------------------------------------------------------- 1 | import type { GbPalette } from 'gb-palettes'; 2 | 3 | /* 4 | * On Type-Changes, a history for migration must be kept in /src/javascript/app/stores/migrations/history/ 5 | * */ 6 | export interface Palette extends GbPalette { 7 | isPredefined: boolean, 8 | } 9 | -------------------------------------------------------------------------------- /src/types/PickColors.d.ts: -------------------------------------------------------------------------------- 1 | export interface PickColors { 2 | colors: number[][], 3 | fileName: string, 4 | } 5 | -------------------------------------------------------------------------------- /src/types/PluginCompatibility.ts: -------------------------------------------------------------------------------- 1 | import { CompatibilityActionType } from '@/consts/plugins'; 2 | import type { Dialog } from './Dialog'; 3 | import type { Image } from './Image'; 4 | 5 | interface CompatibilityActionBase { 6 | type: CompatibilityActionType, 7 | payload?: unknown, 8 | } 9 | 10 | export interface CompatibilityActionConfirmAsk extends CompatibilityActionBase { 11 | type: CompatibilityActionType.CONFIRM_ASK, 12 | payload: Dialog 13 | } 14 | 15 | export interface CompatibilityActionConfirmAnswered extends CompatibilityActionBase { 16 | type: CompatibilityActionType.CONFIRM_ANSWERED, 17 | payload: undefined 18 | } 19 | 20 | export interface CompatibilityActionAddImages extends CompatibilityActionBase { 21 | type: CompatibilityActionType.ADD_IMAGES, 22 | payload: Image[], 23 | } 24 | 25 | export interface CompatibilityActionImportFiles extends CompatibilityActionBase { 26 | type: CompatibilityActionType.IMPORT_FILES, 27 | payload: { files: File[] }, 28 | } 29 | 30 | export type CompatibilityAction = 31 | CompatibilityActionConfirmAsk | 32 | CompatibilityActionConfirmAnswered | 33 | CompatibilityActionAddImages | 34 | CompatibilityActionImportFiles; 35 | 36 | export interface PluginCompatibilityWrapper { 37 | dispatch: (action: CompatibilityAction) => void, 38 | } 39 | -------------------------------------------------------------------------------- /src/types/QueueImage.d.ts: -------------------------------------------------------------------------------- 1 | export interface QueueImage { 2 | imageData: ImageData, 3 | scaleFactor: number, 4 | width: number, 5 | height: number, 6 | fileName: string, 7 | lastModified?: number, 8 | } 9 | -------------------------------------------------------------------------------- /src/types/ReactCss.d.ts: -------------------------------------------------------------------------------- 1 | import type { CSSProperties } from 'react'; 2 | 3 | declare module 'react' { 4 | interface CSSPropertiesVars extends CSSProperties { 5 | [key: `--${string}`]: string | number 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/types/Sync.d.ts: -------------------------------------------------------------------------------- 1 | import type { files as Files } from 'dropbox/types/dropbox_types'; 2 | 3 | export type DBFolderAll = Files.FileMetadataReference | Files.FolderMetadataReference | Files.DeletedMetadataReference; 4 | export type DBFolderFile = Files.FileMetadataReference; 5 | 6 | export interface UploadDeleteResult { 7 | uploaded: Files.FileMetadata[], 8 | deleted: DBFolderFile[], 9 | } 10 | 11 | export interface GitUploadResult { 12 | uploaded?: string[], 13 | deleted?: string[], 14 | repo?: string, 15 | } 16 | 17 | export interface UploadFile { 18 | destination: string, 19 | blob: Blob, 20 | } 21 | 22 | export interface KeepFile { 23 | destination: string, 24 | } 25 | 26 | export interface DownloadInfo { 27 | folder?: 'images' | 'frames', 28 | filename: string, 29 | blob: Blob, 30 | title: string, 31 | } 32 | 33 | export interface RemoteFiles { 34 | toUpload: UploadFile[], 35 | toKeep: KeepFile[], 36 | } 37 | 38 | export type ExportStats = Record; 39 | 40 | export type AddToQueueFn = (what: string, throttle: number, fn: () => Promise, isSilent?: boolean) => Promise 41 | 42 | export interface GetSettingsOptions { 43 | lastUpdateUTC?: number, 44 | selectedFrameGroup?: string, 45 | } 46 | 47 | export interface RecentImport { 48 | hash: string, 49 | timestamp: number, 50 | } 51 | 52 | export interface DropBoxSettings { 53 | use?: boolean, 54 | refreshToken?: string, 55 | accessToken?: string, 56 | expiresAt?: number, 57 | path?: string, 58 | autoDropboxSync?: boolean, 59 | } 60 | 61 | export interface GitStorageSettings { 62 | use?: boolean, 63 | owner?: string, 64 | repo?: string, 65 | branch?: string, 66 | token?: string, 67 | throttle?: number, 68 | } 69 | 70 | export interface SyncLastUpdate { 71 | dropbox: number, 72 | local: number, 73 | } 74 | -------------------------------------------------------------------------------- /src/types/VideoParams.d.ts: -------------------------------------------------------------------------------- 1 | import type { ExportFrameMode } from 'gb-image-decoder'; 2 | 3 | export interface VideoParams { 4 | exportFrameMode?: ExportFrameMode, 5 | frame?: string, 6 | frameRate?: number, 7 | invertPalette?: boolean, 8 | lockFrame?: boolean, 9 | palette?: string, 10 | reverse?: boolean, 11 | scaleFactor?: number, 12 | yoyo?: boolean, 13 | } 14 | -------------------------------------------------------------------------------- /src/types/download.ts: -------------------------------------------------------------------------------- 1 | export interface DownloadBlob { 2 | blob: Blob, 3 | filename: string, 4 | } 5 | 6 | export interface DownloadArrayBuffer { 7 | filename: string, 8 | arrayBuffer: ArrayBuffer, 9 | } 10 | -------------------------------------------------------------------------------- /src/types/galleryTreeContext.d.ts: -------------------------------------------------------------------------------- 1 | import { DialogOption } from '@/types/Dialog'; 2 | import type { Image } from '@/types/Image'; 3 | import { SerializableImageGroup, TreeImageGroup } from '@/types/ImageGroup'; 4 | 5 | export interface CalculateRootWorkerParams { 6 | imageGroups: SerializableImageGroup[], 7 | stateImages: Image[], 8 | } 9 | 10 | export interface CalculateRootWorkerResult { 11 | root: TreeImageGroup, 12 | paths: PathMap[], 13 | pathsOptions: DialogOption[], 14 | duration: number, 15 | } 16 | 17 | export interface PathMap { 18 | absolutePath: string 19 | group: TreeImageGroup, 20 | } 21 | 22 | export interface GetUrlParams { 23 | pageIndex?: number, 24 | group?: string, 25 | } 26 | 27 | export interface GalleryTreeContextType { 28 | root: TreeImageGroup, // always the root element 29 | view: TreeImageGroup, // 'view' contains images and coverImages (=groups) 30 | images: Image[], // 'images' contains only actual images (without covers/groups) 31 | covers: string[], 32 | paths: PathMap[], 33 | pathsOptions: DialogOption[], 34 | isWorking: boolean, 35 | pageIndex: number, 36 | path: string, 37 | lastGalleryLink: string, 38 | getUrl: (params: GetUrlParams) => string, 39 | } 40 | 41 | export type SetErrorFn = (error: string) => void; 42 | 43 | export interface TreeContextWorkerApi { 44 | calculate: ( 45 | params: CalculateRootWorkerParams, 46 | setError: SetErrorFn, 47 | ) => Promise, 48 | } 49 | -------------------------------------------------------------------------------- /src/types/handleFileImport.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface HandeFileImportOptions { 3 | fromPrinter: boolean 4 | } 5 | 6 | export type HandeFileImportFn = (files: File[], options?: HandeFileImportOptions) => Promise; 7 | -------------------------------------------------------------------------------- /src/types/handleLine.ts: -------------------------------------------------------------------------------- 1 | import { type HandleLine } from '@/consts/handleLine'; 2 | 3 | export interface ImportLineBase { 4 | type: HandleLine, 5 | } 6 | 7 | export interface ImportLineNewLines extends ImportLineBase { 8 | type: HandleLine.NEW_LINES, 9 | payload: string[], 10 | } 11 | 12 | export interface ImportLineImageComplete extends ImportLineBase { 13 | type: HandleLine.IMAGE_COMPLETE, 14 | } 15 | 16 | export interface ImportLineError extends ImportLineBase { 17 | type: HandleLine.PARSE_ERROR, 18 | payload: string, 19 | } 20 | 21 | export type ImportLine = ImportLineNewLines | ImportLineImageComplete | ImportLineError; 22 | -------------------------------------------------------------------------------- /src/types/types.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare module '*.md' { 3 | const content: string; 4 | export default content; 5 | } 6 | 7 | declare module '*.txt' { 8 | const content: string; 9 | export default content; 10 | } 11 | -------------------------------------------------------------------------------- /src/workers/portsContextWorker.ts: -------------------------------------------------------------------------------- 1 | import { expose } from 'comlink'; 2 | import SerialPorts from '@/tools/comms/WebSerial/SerialPorts'; 3 | import USBPorts from '@/tools/comms/WebUSBSerial/USBPorts'; 4 | import { 5 | PortsWorkerRemote, 6 | PortsWorkerClient, 7 | } from '@/types/ports'; 8 | 9 | const portsWorkerRemote: PortsWorkerRemote = { 10 | async openSerial(): Promise { 11 | console.log('open serial'); 12 | await SerialPorts.initPorts(); 13 | }, 14 | 15 | async openUSB(): Promise { 16 | await USBPorts.initPorts(); 17 | }, 18 | 19 | async registerClient(portsWorkerClient: PortsWorkerClient) { 20 | if (SerialPorts.enabled) { 21 | await SerialPorts.registerClient(portsWorkerClient); 22 | await SerialPorts.initPorts(); 23 | } 24 | 25 | if (USBPorts.enabled) { 26 | await USBPorts.registerClient(portsWorkerClient); 27 | await USBPorts.initPorts(); 28 | } 29 | 30 | portsWorkerClient.setStatus(USBPorts.enabled, SerialPorts.enabled); 31 | }, 32 | }; 33 | 34 | expose(portsWorkerRemote); 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "lib": ["dom", "dom.iterable", "esnext", "webworker"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------