├── .env ├── src ├── react-app-env.d.ts ├── components │ ├── FAQ.css │ ├── StatsBar.css │ ├── Explanation.tsx │ ├── StatsBar.tsx │ ├── SQLControl.css │ ├── FAQ.tsx │ └── SQLControl.tsx ├── index.css ├── App.css ├── App.tsx ├── index.tsx ├── lib │ ├── DataWindowManager.ts │ ├── TabManager.ts │ ├── RootWindowManager.ts │ └── Database.ts └── logo.svg ├── public ├── favicon.ico ├── sql-wasm.wasm ├── manifest.json ├── scarr.yml └── index.html ├── .gitignore ├── tsconfig.json ├── package.json └── README.md /.env: -------------------------------------------------------------------------------- 1 | CHOKIDAR_USEPOLLING=true 2 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kkuchta/tabdb/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/sql-wasm.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kkuchta/tabdb/HEAD/public/sql-wasm.wasm -------------------------------------------------------------------------------- /src/components/FAQ.css: -------------------------------------------------------------------------------- 1 | .FAQ .faqHideShow { 2 | font-size: 24px; 3 | } 4 | .FAQ .faqHideShow:hover { 5 | font-weight: bold; 6 | cursor: pointer; 7 | } 8 | .FAQ .question { 9 | font-weight: bold; 10 | display: block; 11 | margin-bottom: 5px; 12 | } 13 | .FAQ li { 14 | list-style: none; 15 | margin-top: 20px; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/StatsBar.css: -------------------------------------------------------------------------------- 1 | .StatsBar { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | } 6 | .availableCharacters { 7 | font-weight: bold; 8 | margin: 20px; 9 | } 10 | .addTab { 11 | margin: 20px; 12 | padding: 10px; 13 | border-radius: 5px; 14 | width: 200px; 15 | font-size: 16px; 16 | } 17 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/components/Explanation.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Explanation: React.FC = () => { 4 | return ( 5 |
6 |

TabDB

7 |

Do you treat your browser tabs like a database? Well make them queryable with TabDB! Another thoroughly useful... thing... by @kkuchta.

8 |
9 | ); 10 | } 11 | 12 | export default Explanation; 13 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | max-width: 800px; 9 | margin: auto; 10 | } 11 | 12 | code { 13 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 14 | monospace; 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "preserve" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /src/components/StatsBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './StatsBar.css'; 3 | 4 | const createTab = () => { 5 | window.windowManager.createNewTab({sqlTextArea: 'dfsfdsa'}); 6 | console.log('here') 7 | } 8 | 9 | const StatsBar: React.FC<{availableChars: number}> = ({ availableChars }) => { 10 | return ( 11 |
12 | Available Characters: {availableChars} 13 | 14 |
15 | ); 16 | } 17 | 18 | export default StatsBar; 19 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | pointer-events: none; 9 | } 10 | 11 | .App-header { 12 | background-color: #282c34; 13 | min-height: 100vh; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | justify-content: center; 18 | font-size: calc(10px + 2vmin); 19 | color: white; 20 | } 21 | 22 | .App-link { 23 | color: #61dafb; 24 | } 25 | 26 | .Explanation { 27 | margin: 40px; 28 | } 29 | 30 | @keyframes App-logo-spin { 31 | from { 32 | transform: rotate(0deg); 33 | } 34 | to { 35 | transform: rotate(360deg); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/scarr.yml: -------------------------------------------------------------------------------- 1 | domain: "tabdb.io" 2 | name: "tabdb" 3 | region: "us-west-1" 4 | 5 | # This section's only used if you use scarr to register a domain. Which fields 6 | # are required depends on what TLD you register. See 7 | # https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/domain-register-values-specify.html 8 | # for details. 9 | domainContact: 10 | address1: 'fillmein' 11 | address2: '' 12 | city: 'fillmein' 13 | contactType: 'PERSON' 14 | countryCode: 'US' 15 | email: 'kevin@kevinkuchta.com' 16 | firstName: 'fillmein' 17 | lastName: 'fillmein' 18 | phoneNumber: '+1.4157582482' 19 | state: 'CA' 20 | zipCode: '94117' 21 | 22 | # A list of regexes to be run against paths in the current directory. Any file path matching any of these regexes will not be synced to s3 23 | exclude: 24 | - "scarr\\.yml" 25 | - "^\\.git" 26 | - "\\.DS_Store" 27 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Explanation from './components/Explanation'; 3 | import StatsBar from './components/StatsBar'; 4 | import SQLControl from './components/SQLControl'; 5 | import FAQ from './components/FAQ'; 6 | import './App.css'; 7 | 8 | const App: React.FC = () => { 9 | // This doesn't change after the page is loaded 10 | const availableCharacters = window.windowManager.tabManager.availableCharacters(); 11 | 12 | return ( 13 |
14 | 15 | 16 | { availableCharacters > 0 && } 17 | { availableCharacters > 0 && } 18 | { availableCharacters <= 0 &&
19 | Click that button a few times to add storage space to your db! 20 |
} 21 |
22 | ); 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import RootWindowManager, { WINDOW_ROOT_NAME } from './lib/RootWindowManager'; 6 | import DataWindowManager from './lib/DataWindowManager'; 7 | 8 | declare global { 9 | interface Window { 10 | windowManager: any; 11 | lastWindowData: any; 12 | } 13 | } 14 | 15 | console.log(RootWindowManager); 16 | 17 | if (window.name === '') window.name = WINDOW_ROOT_NAME; 18 | if (window.name === WINDOW_ROOT_NAME) { 19 | console.log("Starting as root"); 20 | (window.windowManager = new RootWindowManager()).run(); 21 | window.windowManager.recoverTabs(); 22 | 23 | ReactDOM.render(, document.getElementById('root')); 24 | } else { 25 | console.log("Starting as data"); 26 | (window.windowManager = new DataWindowManager()).run(); 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tab_db", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/jest": "24.0.15", 7 | "@types/node": "12.6.8", 8 | "@types/pako": "^1.0.1", 9 | "@types/react": "16.8.23", 10 | "@types/react-dom": "16.8.5", 11 | "@types/sql.js": "^1.0.1", 12 | "pako": "^1.0.10", 13 | "react": "^16.8.6", 14 | "react-dom": "^16.8.6", 15 | "react-scripts": "3.0.1", 16 | "sql.js": "^1.0.0", 17 | "typescript": "3.5.3" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": "react-app" 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/lib/DataWindowManager.ts: -------------------------------------------------------------------------------- 1 | import RootWindowManager from './RootWindowManager' 2 | 3 | export default class DataWindowManager { 4 | run() { 5 | document.body.innerHTML = "

TabDB data window. We're only using this tab's title to store data. You probably want to be on the tab that's displaying some UI (the 'root' tab) instead of here. If you've lost your root tab, try clicking here

."; 6 | } 7 | forceRoot(e: any) { 8 | e.preventDefault(); 9 | window.name = ''; 10 | window.location.reload(); 11 | } 12 | reroot({ textAreaText, tabs }: { textAreaText: string; tabs: Window[] }) { 13 | console.log("rerooting - new tabs = ", tabs); 14 | const textArea = window.document.getElementById('sqlTextArea') as HTMLTextAreaElement 15 | textArea.value = textAreaText; 16 | window.document.body.style.display = 'black'; 17 | (window.windowManager = new RootWindowManager(tabs)) 18 | window.windowManager.run(); 19 | } 20 | submitSQL() { 21 | console.log("submitSQL is a no-op on data pages (UI should be hidden anyway)"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TabDB 2 | 3 | If you feel like you use your browser tabs as a database, why not make it official? 4 | 5 | ![demo gif](https://media.giphy.com/media/cPl0frYGok7Cr8RA7I/giphy.gif) 6 | 7 | This is an in-browser database that uses tab titles for storage. 8 | 9 | Every time you run an SQL query, it grabs all the data stored in the neighboring tabs' titles, concatenates it, unzips it, and loads it into an in-memory sqlite database. It then runs the command, dumps the db state to a string, zips it up, and spreads it out across the available tabs. 10 | 11 | Play with it live at [tabdb.io](https://tabdb.io). 12 | 13 | [![comic about bad ideas](http://www.poorlydrawnlines.com/wp-content/uploads/2017/07/an-idea.png)](http://www.poorlydrawnlines.com/comic/an-idea/) 14 | 15 | The code is awful- I hacked together the first version (which was ugly enough), then glued it onto typescript + react for fun. I didn't go so far as to properly integrate the raw JS logic with react (eg via redux or something), so it's a mishmash of global variables, commented-out code, and TODO comments. If this were production code, I'd deserve to be tarred and feathered. Since it's just a silly one-off thing, I hope you won't judge it too harshly. 16 | -------------------------------------------------------------------------------- /src/components/SQLControl.css: -------------------------------------------------------------------------------- 1 | .SQLControl { 2 | /*border: 1px solid green;*/ 3 | display: flex; 4 | padding: 20px; 5 | } 6 | .SQLControl * { 7 | display: flex; 8 | } 9 | 10 | .SQLControl button { 11 | border-radius: 5px; 12 | justify-content: center; 13 | } 14 | .input { 15 | flex-direction: column; 16 | width: 50%; 17 | margin-right: 10px; 18 | flex-grow: 1; 19 | flex-basis: 0; 20 | } 21 | 22 | .queryOutput { 23 | margin-left: 10px; 24 | background-color: #f5f5f5; 25 | border-radius: 5px; 26 | padding: 10px; 27 | overflow: scroll; 28 | flex-grow: 1; 29 | flex-basis: 0; 30 | } 31 | .queryOutput table { 32 | display: table; 33 | } 34 | .queryOutput td { 35 | display: table-cell; 36 | } 37 | .queryOutput tr { 38 | display: table-row; 39 | } 40 | .queryOutput th { 41 | display: table-cell; 42 | } 43 | .queryOutput thead { 44 | display: table-header-group; 45 | } 46 | .queryOutput tbody { 47 | display: table-row-group; 48 | } 49 | 50 | textArea { 51 | height: 200px; 52 | padding: 5px; 53 | border-radius: 5px; 54 | } 55 | 56 | .runSQL { 57 | margin-top: 10px; 58 | padding: 5px; 59 | border: 2px solid black; 60 | } 61 | 62 | .prefillButtons { 63 | margin-top: 10px; 64 | justify-content: space-between; 65 | align-items: baseline; 66 | } 67 | .prefillButtons button { 68 | padding: 5px 15px; 69 | } 70 | 71 | @media (max-width: 600px) { 72 | .prefillButtons { 73 | flex-direction: column; 74 | } 75 | .prefillButtons button { 76 | margin-top: 10px; 77 | width:100%; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/lib/TabManager.ts: -------------------------------------------------------------------------------- 1 | const MAX_TAB_CHARS = 100; 2 | 3 | // Reads and writes data from a list of tabs 4 | export default class TabManager { 5 | tabs: Window[] 6 | constructor(tabs = []) { 7 | this.tabs = tabs; 8 | } 9 | 10 | setTabs(tabs: Window[]) { 11 | this.tabs = tabs; 12 | } 13 | canFit(data: any) { 14 | return this.requiredCharacters(data) < this.availableCharacters(); 15 | } 16 | 17 | requiredCharacters(data: any) { return JSON.stringify(data).length } 18 | availableCharacters() { return this.tabs.length * MAX_TAB_CHARS } 19 | 20 | write(data: any) { 21 | let dataString = JSON.stringify(data); 22 | 23 | // Split data into to N chunks, as evenly as possible. 24 | const length = dataString.length; 25 | const chunks = this.tabs.length; 26 | const shortChunkSize = Math.floor(length / chunks); 27 | let longChunks = length % chunks; 28 | for (let i = 0; i < chunks; i++) { 29 | const chunkLength = longChunks > 0 ? shortChunkSize + 1 : shortChunkSize; 30 | longChunks--; 31 | const chunk = dataString.slice(0, chunkLength); 32 | dataString = dataString.slice(chunkLength); 33 | this.tabs[i].document.title = chunk === "" ? document.title : chunk; 34 | } 35 | } 36 | 37 | read() { 38 | // Filter out empty dbs 39 | const dataStrings = this.tabs 40 | .filter((tab) => tab.document.title !== window.document.title) 41 | .map((tab) => tab.document.title); 42 | if (dataStrings.length === 0) { return null } 43 | const json = dataStrings.join(''); 44 | console.log("json=", json); 45 | return JSON.parse(json); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | TabDB 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/RootWindowManager.ts: -------------------------------------------------------------------------------- 1 | import TabManager from './TabManager' 2 | import Database, { QueryResult } from './Database' 3 | import DataWindowManager from './DataWindowManager' 4 | 5 | const WINDOW_NAME_PREFIX = "tab_db_data_window_"; 6 | export const WINDOW_ROOT_NAME = WINDOW_NAME_PREFIX + 'root'; 7 | 8 | export default class RootWindowManager{ 9 | tabManager: any; 10 | database: any; 11 | tabs: any; 12 | constructor(tabList = [] as Window[]) { 13 | console.log("constructing with ", tabList); 14 | this.tabManager = new TabManager(); 15 | this.database = new Database(this.tabManager); 16 | this.setTabList(tabList); 17 | } 18 | run() { 19 | console.log("running"); 20 | if (this.tabs == null) this.setTabList([]); 21 | } 22 | submitSQL(sql: string): QueryResult { 23 | //const sqlArea = document.getElementById('sqlTextArea') as HTMLTextAreaElement 24 | let result: QueryResult; 25 | try { 26 | result = this.database.runQuery(sql); 27 | } catch(err) { 28 | result = { error: err.message }; 29 | } 30 | //if (result.rows == 0) return {}; 31 | //let resultText = result[0].columns.join("\t") + "\n"; 32 | //resultText += result[0].values.map((row: string[]) => row.join("\t")).join("\n"); 33 | //console.log(resultText); 34 | 35 | //document.getElementById('sqlTextArea').value = resultText; 36 | 37 | console.log("result=", result); 38 | return result; 39 | } 40 | recoverTabs() { 41 | console.log("recovering tabs"); 42 | let win = window; 43 | const recoveredTabs = []; 44 | while((win = win.opener) != null) { 45 | console.log("win = ", win.name); 46 | recoveredTabs.unshift(win); 47 | } 48 | console.log("recovered ", recoveredTabs); 49 | this.setTabList(recoveredTabs); 50 | //this.tabs = recoveredTabs; 51 | } 52 | setTabList(tabList: Window[]) { 53 | this.tabs = tabList; 54 | this.tabManager.setTabs(tabList); 55 | } 56 | 57 | createNewTab() { 58 | console.log("this.tabs = ", this.tabs); 59 | const newWindow = window.open('#', WINDOW_NAME_PREFIX + this.tabs.length); 60 | if (newWindow == null) { 61 | alert("Couldn't open new tab for some reason?"); 62 | return; 63 | } 64 | 65 | const sqlTextArea = document.getElementsByTagName('textarea')[0] 66 | const sqlTextAreaText = sqlTextArea == null ? '' : sqlTextArea.value; 67 | 68 | // TODO: handle tab closing - remove from tabarray and update stats 69 | // TODO: do we actually need to pass tabs here? Could just always recover 70 | // tabs. 71 | newWindow.lastWindowData = { 72 | textAreaText: sqlTextAreaText, 73 | } 74 | window.name = newWindow.name; 75 | newWindow.name = WINDOW_ROOT_NAME; 76 | // This window is now a data window. 77 | (window.windowManager = new DataWindowManager()).run(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/components/FAQ.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './FAQ.css'; 3 | 4 | class FAQ extends React.Component { 5 | state = { 6 | showing: false 7 | } 8 | toggleVisibility = () => { 9 | this.setState({ showing: !this.state.showing }); 10 | } 11 | renderFAQ() { 12 | return ( 13 | 51 | ); 52 | } 53 | render() { 54 | const { showing } = this.state; 55 | const arrow = showing ? '↑' : '↓'; 56 | return ( 57 |
58 |
FAQ {arrow}
59 | { showing && this.renderFAQ() } 60 |
61 | ); 62 | } 63 | } 64 | 65 | export default FAQ; 66 | -------------------------------------------------------------------------------- /src/lib/Database.ts: -------------------------------------------------------------------------------- 1 | import pako from 'pako'; 2 | import initSqlJs from 'sql.js'; 3 | 4 | // A database that will, when queried: load the db, execute the query, and then 5 | // write it back out. Expects a persistence mechanism with `read()`, `write(data)`, 6 | // and `canFit(data)` methods. 7 | interface Persistence { 8 | write: (db: string) => void; 9 | canFit: (db: string) => boolean; 10 | read: () => string; 11 | } 12 | 13 | export interface QueryResult { 14 | readonly error?: string; 15 | readonly message?: string; 16 | readonly rows?: { 17 | columns: string[]; 18 | values: string[][]; 19 | } 20 | } 21 | 22 | export default class Database { 23 | persistence: Persistence; 24 | sqlite: any; 25 | constructor(persistence: any) { 26 | this.persistence = persistence; 27 | this.sqlite = null; 28 | // TODO: actually wait for this to load. 29 | const config = { 30 | locateFile: (filename: string) => `/${filename}` 31 | } 32 | initSqlJs(config).then(sqlite => { this.sqlite = sqlite; }); 33 | } 34 | runQuery(query: string): QueryResult { 35 | const db = this.readDB(); 36 | const result = db.exec(query); 37 | this.writeDB(db); 38 | // TODO: free db 39 | // 40 | if (result[0]) { return { rows: result[0] } } 41 | return {}; 42 | } 43 | writeDB(dbObject: any) { 44 | // Dump the entire db state. 45 | const dbDataArray = dbObject.export(); 46 | 47 | // A db with 1 table and 2 rows is 8k long, mosty empty. Let's compress it 48 | // before trying to save it to tabs. 49 | const compressedString = this.toBinString(pako.deflate(dbDataArray)); 50 | 51 | // Base64 encode it because weird utf-8 characters won't save to the dom and 52 | // come back the same. 53 | const base64Data = btoa(compressedString); 54 | if (this.persistence.canFit(base64Data)) { 55 | this.persistence.write(base64Data); 56 | } else { 57 | // TODO: throw this from the persistence object, not from here. 58 | interface Foo { 59 | requiredCharacters: (x: any) => number; 60 | availableCharacters: () => number; 61 | }; 62 | const pers = this.persistence as unknown as Foo; 63 | throw new Error(`Not enough space to execute that query. We'd need ${pers.requiredCharacters(base64Data)} characters of space, but we only have ${pers.availableCharacters()}. Open more tabs to increase space!`); 64 | } 65 | } 66 | 67 | readDB() { 68 | const base64Data = this.persistence.read(); 69 | // If we read nothing from the tabs, create a new db. 70 | if (base64Data == null) { return new this.sqlite.Database() } 71 | 72 | const compressedString = atob(base64Data) 73 | const dbDataArray = pako.inflate(this.toBinArray(compressedString)) 74 | return new this.sqlite.Database(dbDataArray); 75 | } 76 | 77 | // From https://github.com/kripken/sql.js/wiki/Persisting-a-Modified-Database 78 | toBinString(arr: Uint8Array) { 79 | var uarr = new Uint8Array(arr); 80 | var strings = [], chunksize = 0xffff; 81 | // There is a maximum stack size. We cannot call String.fromCharCode with as many arguments as we want 82 | for (var i=0; i*chunksize < uarr.length; i++){ 83 | const subarr = uarr.subarray(i*chunksize, (i+1)*chunksize); 84 | strings.push(String.fromCharCode.apply(null, subarr as unknown as number[])); 85 | } 86 | return strings.join(''); 87 | } 88 | 89 | toBinArray(str: string) { 90 | var l = str.length, 91 | arr = new Uint8Array(l); 92 | for (var i=0; i { 18 | const { value } = this.state; 19 | this.setState({ lastQueryOutput: { message: 'running...' } }); 20 | 21 | // Add a half-second delay so it's clear something's happening. I'm too 22 | // lazy to make the UI reflect this in a better way. 23 | setTimeout( () => { 24 | const result = window.windowManager.submitSQL(value); 25 | this.setState({ lastQueryOutput: result }) 26 | console.log(this.state.value) 27 | }, 300 ); 28 | } 29 | prefill = (key: string) => () => this.setState({ value: PREFILLS[key] }); 30 | onSQLChange = (event: React.ChangeEvent) => { 31 | 32 | // We need this value for when we open a new tab. I don't feel like adding 33 | // a proper state management system like Redux just to move one variable on 34 | // a joke project, so let's just yolo a global variable. 35 | //window.sql = event.target.value; 36 | 37 | this.setState({value: event.target.value}); 38 | }; 39 | renderQueryOutput() { 40 | console.log("Rendering output", this.state.lastQueryOutput) 41 | const output = this.state.lastQueryOutput as QueryResult | null; 42 | if (output == null) { 43 | return
<--- Run a query to get some output
; 44 | } else if (output.error != null) { 45 | return
Error: {output.error}
; 46 | } else if (output.message != null) { 47 | return
{output.message}
; 48 | } else if (output.rows != null && output.rows.values.length > 0) { 49 | return ( 50 | 51 | { output.rows.columns.map((col) => ) } 52 | 53 | { output.rows.values.map( 54 | (row, i) => 55 | { row.map( 56 | (value, i) => 57 | ) } 58 | 59 | ) } 60 | 61 |
{col}
{value}
62 | ); 63 | } else { 64 | return
Query ran successfully
; 65 | } 66 | } 67 | render() { 68 | const { value } = this.state; 69 | return ( 70 |
71 |
72 |